From 2d279432b0c64c8dda20758641f529f87f14ea1f Mon Sep 17 00:00:00 2001 From: Pierre Pichet Date: Sun, 7 Feb 2010 19:50:10 +0000 Subject: [PATCH] MDL-10110 Creating the calculatedmulti question type --- .../edit_calculatedmulti_form.php | 380 ++++ question/type/calculatedmulti/icon.gif | Bin 0 -> 909 bytes .../type/calculatedmulti/questiontype.php | 1601 +++++++++++++++++ 3 files changed, 1981 insertions(+) create mode 100644 question/type/calculatedmulti/edit_calculatedmulti_form.php create mode 100644 question/type/calculatedmulti/icon.gif create mode 100644 question/type/calculatedmulti/questiontype.php diff --git a/question/type/calculatedmulti/edit_calculatedmulti_form.php b/question/type/calculatedmulti/edit_calculatedmulti_form.php new file mode 100644 index 00000000000..8511e9f6b14 --- /dev/null +++ b/question/type/calculatedmulti/edit_calculatedmulti_form.php @@ -0,0 +1,380 @@ +regenerate = true; + $this->question = $question; + $this->qtypeobj =& $QTYPES[$this->question->qtype]; + parent::question_edit_form($submiturl, $question, $category, $contexts, $formeditable); + } + + /** + * Get the list of form elements to repeat, one for each answer. + * @param object $mform the form being built. + * @param $label the label to use for each option. + * @param $gradeoptions the possible grades for each answer. + * @param $repeatedoptions reference to array of repeated options to fill + * @param $answersoption reference to return the name of $question->options field holding an array of answers + * @return array of form fields. + */ + /* function get_per_answer_fields(&$mform, $label, $gradeoptions, &$repeatedoptions, &$answersoption) { + $repeated = array(); + $repeated[] =& $mform->createElement('header', 'answerhdr', $label); + $repeated[] =& $mform->createElement('text', 'answer', get_string('answer', 'quiz'), array('size' => 50)); + $repeated[] =& $mform->createElement('select', 'fraction', get_string('grade'), $gradeoptions); + $repeated[] =& $mform->createElement('htmleditor', 'feedback', get_string('feedback', 'quiz'), + array('course' => $this->coursefilesid)); + $repeatedoptions['answer']['type'] = PARAM_RAW; + $repeatedoptions['fraction']['default'] = 0; + $answersoption = 'answers'; + return $repeated; + }*/ + function get_per_answer_fields(&$mform, $label, $gradeoptions, &$repeatedoptions, &$answersoption) { + // $repeated = parent::get_per_answer_fields($mform, $label, $gradeoptions, $repeatedoptions, $answersoption); + $repeated = array(); + $repeated[] =& $mform->createElement('header', 'answerhdr', $label); + // if ($this->editasmultichoice == 1){ + $repeated[] =& $mform->createElement('text', 'answer', get_string('answer', 'quiz'), array('size' => 50)); + $repeated[] =& $mform->createElement('select', 'fraction', get_string('grade'), $gradeoptions); + $repeated[] =& $mform->createElement('htmleditor', 'feedback', get_string('feedback', 'quiz'), + array('course' => $this->coursefilesid)); + $repeatedoptions['answer']['type'] = PARAM_RAW; + $repeatedoptions['fraction']['default'] = 0; + $answersoption = 'answers'; + + $mform->setType('answer', PARAM_NOTAGS); + + $addrepeated = array(); + $addrepeated[] =& $mform->createElement('hidden', 'tolerance'); + $addrepeated[] =& $mform->createElement('hidden', 'tolerancetype',1); + $repeatedoptions['tolerance']['type'] = PARAM_NUMBER; + $repeatedoptions['tolerance']['default'] = 0.01; + + $addrepeated[] =& $mform->createElement('select', 'correctanswerlength', get_string('correctanswershows', 'qtype_calculated'), range(0, 9)); + $repeatedoptions['correctanswerlength']['default'] = 2; + + $answerlengthformats = array('1' => get_string('decimalformat', 'quiz'), '2' => get_string('significantfiguresformat', 'quiz')); + $addrepeated[] =& $mform->createElement('select', 'correctanswerformat', get_string('correctanswershowsformat', 'qtype_calculated'), $answerlengthformats); + array_splice($repeated, 3, 0, $addrepeated); + $repeated[1]->setLabel('...{={x}+..}...'); + + return $repeated; + } + + /** + * Add question-type specific form fields. + * + * @param MoodleQuickForm $mform the form being built. + */ + function definition_inner(&$mform) { + global $QTYPES; + $this->qtypeobj =& $QTYPES[$this->qtype()]; + // echo code left for testing period + // echo "

question ".optional_param('multichoice', '', PARAM_RAW)." optional

";print_r($this->question);echo "

"; + $label = get_string("sharedwildcards", "qtype_datasetdependent"); + $mform->addElement('hidden', 'initialcategory', 1); + $mform->setType('initialcategory', PARAM_INT); + + // $html2 = $this->qtypeobj->print_dataset_definitions_category($this->question); + $html2 =""; + $mform->insertElementBefore($mform->createElement('static','listcategory',$label,$html2),'name'); + $addfieldsname='updatecategory'; + $addstring=get_string("updatecategory", "qtype_calculated"); + $mform->registerNoSubmitButton($addfieldsname); + $this->editasmultichoice = 1 ; + + + $mform->insertElementBefore( $mform->createElement('submit', $addfieldsname, $addstring),'listcategory'); + $mform->registerNoSubmitButton('createoptionbutton'); + $mform->addElement('hidden', 'multichoice',$this->editasmultichoice); + $mform->setType('multichoice', PARAM_INT); + + + $mform->addElement('header', 'choicehdr',get_string('multichoicecalculatedquestion', 'qtype_calculated')); + $menu = array(get_string('answersingleno', 'qtype_multichoice'), get_string('answersingleyes', 'qtype_multichoice')); + $mform->addElement('select', 'single', get_string('answerhowmany', 'qtype_multichoice'), $menu); + $mform->setDefault('single', 1); + + $mform->addElement('advcheckbox', 'shuffleanswers', get_string('shuffleanswers', 'qtype_multichoice'), null, null, array(0,1)); + $mform->setHelpButton('shuffleanswers', array('multichoiceshuffle', get_string('shuffleanswers','qtype_multichoice'), 'qtype_multichoice')); + $mform->setDefault('shuffleanswers', 1); + + $numberingoptions = $QTYPES['multichoice']->get_numbering_styles(); + $menu = array(); + foreach ($numberingoptions as $numberingoption) { + $menu[$numberingoption] = get_string('answernumbering' . $numberingoption, 'qtype_multichoice'); + } + $mform->addElement('select', 'answernumbering', get_string('answernumbering', 'qtype_multichoice'), $menu); + $mform->setDefault('answernumbering', 'abc'); + + $creategrades = get_grade_options(); + $this->add_per_answer_fields($mform, get_string('choiceno', 'qtype_multichoice', '{no}'), + $creategrades->gradeoptionsfull, max(5, QUESTION_NUMANS_START)); + + + $repeated = array(); + // if ($this->editasmultichoice == 1){ + $nounits = optional_param('nounits', 1, PARAM_INT); + $mform->addElement('hidden', 'nounits', $nounits); + $mform->setType('nounits', PARAM_INT); + $mform->setConstants(array('nounits'=>$nounits)); + for ($i=0; $i< $nounits; $i++) { + $mform->addElement('hidden','unit'."[$i]", optional_param('unit'."[$i]", '', PARAM_NOTAGS)); + $mform->setType('unit'."[$i]", PARAM_NOTAGS); + $mform->addElement('hidden', 'multiplier'."[$i]", optional_param('multiplier'."[$i]", '', PARAM_NUMBER)); + $mform->setType('multiplier'."[$i]", PARAM_NUMBER); + } + $mform->addElement('hidden','unitgradingtype',optional_param('unitgradingtype', '', PARAM_INT)) ; + $mform->addElement('hidden','unitpenalty',optional_param('unitpenalty', '', PARAM_NUMBER)) ; + $mform->addElement('hidden','showunits',optional_param('showunits', '', PARAM_INT)) ; + $mform->addElement('hidden','unitsleft',optional_param('unitsleft', '', PARAM_INT)) ; + $mform->addElement('hidden','instructions',optional_param('instructions', '', PARAM_RAW)) ; + + $mform->setType('addunits','hidden'); + $mform->addElement('header', 'overallfeedbackhdr', get_string('overallfeedback', 'qtype_multichoice')); + + foreach (array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback') as $feedbackname) { + $mform->addElement('htmleditor', $feedbackname, get_string($feedbackname, 'qtype_multichoice'), + array('course' => $this->coursefilesid)); + $mform->setType($feedbackname, PARAM_RAW); + } + //hidden elements + $mform->addElement('hidden', 'synchronize', ''); + $mform->setType('synchronize', PARAM_INT); + if (isset($this->question->options)&& isset($this->question->options->synchronize) ){ + $mform->setDefault("synchronize", $this->question->options->synchronize); + } else { + $mform->setDefault("synchronize", 0 ); + } + $mform->addElement('hidden', 'wizard', 'datasetdefinitions'); + $mform->setType('wizard', PARAM_ALPHA); + + + } + + function set_data($question) { + $default_values['multichoice']= $this->editasmultichoice ; //$this->editasmultichoice ; + if (isset($question->options)){ + $answers = $question->options->answers; + if (count($answers)) { + $key = 0; + foreach ($answers as $answer){ + $default_values['answer['.$key.']'] = $answer->answer; + $default_values['fraction['.$key.']'] = $answer->fraction; + $default_values['tolerance['.$key.']'] = $answer->tolerance; + $default_values['tolerancetype['.$key.']'] = $answer->tolerancetype; + $default_values['correctanswerlength['.$key.']'] = $answer->correctanswerlength; + $default_values['correctanswerformat['.$key.']'] = $answer->correctanswerformat; + $default_values['feedback['.$key.']'] = $answer->feedback; + $key++; + } + } + // $default_values['unitgradingtype'] = $question->options->unitgradingtype ; + // $default_values['unitpenalty'] = $question->options->unitpenalty ; + // $default_values['showunits'] = $question->options->showunits ; + // $default_values['unitsleft'] = $question->options->unitsleft ; + // $default_values['instructions'] = $question->options->instructions ; + $default_values['synchronize'] = $question->options->synchronize ; + + if (isset($question->options->units)){ + $units = array_values($question->options->units); + // make sure the default unit is at index 0 + usort($units, create_function('$a, $b', + 'if (1.0 === (float)$a->multiplier) { return -1; } else '. + 'if (1.0 === (float)$b->multiplier) { return 1; } else { return 0; }')); + if (count($units)) { + $key = 0; + foreach ($units as $unit){ + $default_values['unit['.$key.']'] = $unit->unit; + $default_values['multiplier['.$key.']'] = $unit->multiplier; + $key++; + } + } + } + } + if (isset($question->options->single)){ + $default_values['single'] = $question->options->single; + $default_values['answernumbering'] = $question->options->answernumbering; + $default_values['shuffleanswers'] = $question->options->shuffleanswers; + $default_values['correctfeedback'] = $question->options->correctfeedback; + $default_values['partiallycorrectfeedback'] = $question->options->partiallycorrectfeedback; + $default_values['incorrectfeedback'] = $question->options->incorrectfeedback; + } + $default_values['submitbutton'] = get_string('nextpage', 'qtype_calculated'); + $default_values['makecopy'] = get_string('makecopynextpage', 'qtype_calculated'); + /* set the wild cards category display given that on loading the category element is + unselected when processing this function but have a valid value when processing the + update category button. The value can be obtain by + $qu->category =$this->_form->_elements[$this->_form->_elementIndex['category']]->_values[0]; + but is coded using existing functions + */ + $qu = new stdClass; + $el = new stdClass; + /* no need to call elementExists() here */ + if ($this->_form->elementExists('category')){ + $el=$this->_form->getElement('category'); + } else { + $el=$this->_form->getElement('categorymoveto'); + } + if($value =$el->getSelected()) { + $qu->category =$value[0]; + }else { + $qu->category=$question->category;// on load $question->category is set by question.php + } + $html2 = $this->qtypeobj->print_dataset_definitions_category($qu); + $this->_form->_elements[$this->_form->_elementIndex['listcategory']]->_text = $html2 ; + $question = (object)((array)$question + $default_values); + + parent::set_data($question); + } + + function qtype() { + return 'calculatedmulti'; + } + + function validation($data, $files) { + // echo code left for testing period + + // echo "

question

";print_r($this->question);echo "

"; + // echo "

data

";print_r($data);echo "

"; + + $errors = parent::validation($data, $files); + //verifying for errors in {=...} in question text; + $qtext = ""; + $qtextremaining = $data['questiontext'] ; + $possibledatasets = $this->qtypeobj->find_dataset_names($data['questiontext']); + foreach ($possibledatasets as $name => $value) { + $qtextremaining = str_replace('{'.$name.'}', '1', $qtextremaining); + } + // echo "numericalquestion qtextremaining
";print_r($possibledatasets);
+        while  (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) {
+            $qtextsplits = explode($regs1[0], $qtextremaining, 2);
+            $qtext =$qtext.$qtextsplits[0];
+            $qtextremaining = $qtextsplits[1];
+            if (!empty($regs1[1]) && $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])) {
+                if(!isset($errors['questiontext'])){
+                    $errors['questiontext'] = $formulaerrors.':'.$regs1[1] ;
+                }else {
+                    $errors['questiontext'] .= '
'.$formulaerrors.':'.$regs1[1]; + } + } + } + $answers = $data['answer']; + $answercount = 0; + $maxgrade = false; + $possibledatasets = $this->qtypeobj->find_dataset_names($data['questiontext']); + $mandatorydatasets = array(); + foreach ($answers as $key => $answer){ + $mandatorydatasets += $this->qtypeobj->find_dataset_names($answer); + } + if ( count($mandatorydatasets )==0){ + // $errors['questiontext']=get_string('atleastonewildcard', 'qtype_datasetdependent'); + foreach ($answers as $key => $answer){ + $errors['answer['.$key.']'] = get_string('atleastonewildcard', 'qtype_datasetdependent'); + } + } + if ($data['multichoice']== 1 ){ + foreach ($answers as $key => $answer){ + $trimmedanswer = trim($answer); + if (($trimmedanswer!='')||$answercount==0){ + //verifying for errors in {=...} in answer text; + $qanswer = ""; + $qanswerremaining = $trimmedanswer ; + $possibledatasets = $this->qtypeobj->find_dataset_names($trimmedanswer); + foreach ($possibledatasets as $name => $value) { + $qanswerremaining = str_replace('{'.$name.'}', '1', $qanswerremaining); + } + // echo "numericalquestion qanswerremaining
";print_r($possibledatasets);
+                    while  (preg_match('~\{=([^[:space:]}]*)}~', $qanswerremaining, $regs1)) {
+                        $qanswersplits = explode($regs1[0], $qanswerremaining, 2);
+                        $qanswer =$qanswer.$qanswersplits[0];
+                        $qanswerremaining = $qanswersplits[1];
+                        if (!empty($regs1[1]) && $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])) {
+                            if(!isset($errors['answer['.$key.']'])){
+                                $errors['answer['.$key.']'] = $formulaerrors.':'.$regs1[1] ;
+                            }else {
+                                $errors['answer['.$key.']'] .= '
'.$formulaerrors.':'.$regs1[1]; + } + } + } + } + if ($trimmedanswer!=''){ + if ('2' == $data['correctanswerformat'][$key] + && '0' == $data['correctanswerlength'][$key]) { + $errors['correctanswerlength['.$key.']'] = get_string('zerosignificantfiguresnotallowed','quiz'); + } + if (!is_numeric($data['tolerance'][$key])){ + $errors['tolerance['.$key.']'] = get_string('mustbenumeric', 'qtype_calculated'); + } + if ($data['fraction'][$key] == 1) { + $maxgrade = true; + } + + $answercount++; + } + //check grades + $totalfraction = 0 ; + $maxfraction = 0 ; + if ($answer != '') { + if ($data['fraction'][$key] > 0) { + $totalfraction += $data['fraction'][$key]; + } + if ($data['fraction'][$key] > $maxfraction) { + $maxfraction = $data['fraction'][$key]; + } + } + } + if ($answercount==0){ + $errors['answer[0]'] = get_string('notenoughanswers', 'qtype_multichoice', 2); + $errors['answer[1]'] = get_string('notenoughanswers', 'qtype_multichoice', 2); + } elseif ($answercount==1){ + $errors['answer[1]'] = get_string('notenoughanswers', 'qtype_multichoice', 2); + + } + + /// Perform sanity checks on fractional grades + if ($data['single']) { + if ($maxfraction > 0.999 ) { + $maxfraction = $maxfraction * 100; + $errors['fraction[0]'] = get_string('errfractionsnomax', 'qtype_multichoice', $maxfraction); + } + } else { + $totalfraction = round($totalfraction,2); + if ($totalfraction != 1) { + $totalfraction = $totalfraction * 100; + $errors['fraction[0]'] = get_string('errfractionsaddwrong', 'qtype_multichoice', $totalfraction); + } + } + + + if ($answercount==0){ + $errors['answer[0]'] = get_string('atleastoneanswer', 'qtype_calculated'); + } + if ($maxgrade == false) { + $errors['fraction[0]'] = get_string('fractionsnomax', 'question'); + } + + } + return $errors; + } +} + diff --git a/question/type/calculatedmulti/icon.gif b/question/type/calculatedmulti/icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..8ce706775285af14e886b8acea6b409bb3adb825 GIT binary patch literal 909 zcmZ?wbhEHb6krfw_|5+Z^f#E-c4#vpD8vfAcK7xuSw32>yC{;C2ATH4-^#`tN~XxJn#Sj literal 0 HcmV?d00001 diff --git a/question/type/calculatedmulti/questiontype.php b/question/type/calculatedmulti/questiontype.php new file mode 100644 index 00000000000..a0bb225b8c5 --- /dev/null +++ b/question/type/calculatedmulti/questiontype.php @@ -0,0 +1,1601 @@ +options = $DB->get_record('question_calculated_options', array('question' => $question->id))) { + // echo $OUTPUT->notification('Error: Missing question options for calculated question'.$question->id.'!'); + // return false; + $question->options->synchronize = 0; + // $question->options->multichoice = 1; + + } + // echo "

questionoptions

";print_r($question);echo "

"; + // $QTYPES['numerical']->get_numerical_options($question); + /* $question->options->unitgradingtype = 0; + $question->options->unitpenalty = 0; + $question->options->showunits = 0 ; + $question->options->unitsleft = 0 ; + $question->options->instructions = '' ; + // echo "

questionoptions

";print_r($question);echo "

"; + + if (!$question->options->answers = $DB->get_records_sql( + "SELECT a.*, c.tolerance, c.tolerancetype, c.correctanswerlength, c.correctanswerformat " . + "FROM {question_answers} a, " . + " {question_calculated} c " . + "WHERE a.question = ? " . + "AND a.id = c.answer ". + "ORDER BY a.id ASC", array($question->id))) { + echo $OUTPUT->notification('Error: Missing question answer for calculated question ' . $question->id . '!'); + return false; + } + + + if(false === parent::get_question_options($question)) { + return false; + } + + if (!$options = $DB->get_records('question_calculated', array('question' => $question->id))) { + notify("No options were found for calculated question + #{$question->id}! Proceeding with defaults."); + // $options = new Array(); + $options= new stdClass; + $options->tolerance = 0.01; + $options->tolerancetype = 1; // relative + $options->correctanswerlength = 2; + $options->correctanswerformat = 1; // decimals + } + + // For historic reasons we also need these fields in the answer objects. + // This should eventually be removed and related code changed to use + // the values in $question->options instead. + foreach ($question->options->answers as $key => $answer) { + $answer = &$question->options->answers[$key]; // for PHP 4.x + $answer->calcid = $options->id; + $answer->tolerance = $options->tolerance; + $answer->tolerancetype = $options->tolerancetype; + $answer->correctanswerlength = $options->correctanswerlength; + $answer->correctanswerformat = $options->correctanswerformat; + } + + //$virtualqtype = $this->get_virtual_qtype( $question); + // $QTYPES['numerical']->get_numerical_units($question); + + if( isset($question->export_process)&&$question->export_process){ + $question->options->datasets = $this->get_datasets_for_export($question); + } + return true; + } +*/ + + function save_question_options($question) { + //$options = $question->subtypeoptions; + // Get old answers: + global $CFG, $DB, $QTYPES ; + if (isset($question->answer) && !isset($question->answers)) { + $question->answers = $question->answer; + } + // calculated options + $update = true ; + $options = $DB->get_record("question_calculated_options", array("question" => $question->id)); + if (!$options) { + $update = false; + $options = new stdClass; + $options->question = $question->id; + } + $options->synchronize = $question->synchronize; + // $options->multichoice = $question->multichoice; + $options->single = $question->single; + $options->answernumbering = $question->answernumbering; + $options->shuffleanswers = $question->shuffleanswers; + $options->correctfeedback = trim($question->correctfeedback); + $options->partiallycorrectfeedback = trim($question->partiallycorrectfeedback); + $options->incorrectfeedback = trim($question->incorrectfeedback); + if ($update) { + if (!$DB->update_record("question_calculated_options", $options)) { + $result->error = "Could not update calculated question options! (id=$options->id)"; + return $result; + } + } else { + if (!$DB->insert_record("question_calculated_options", $options)) { + $result->error = "Could not insert calculated question options!"; + return $result; + } + } + + // Get old versions of the objects + if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) { + $oldanswers = array(); + } + + if (!$oldoptions = $DB->get_records('question_calculated', array('question' => $question->id), 'answer ASC')) { + $oldoptions = array(); + } + + // Save the units. + $virtualqtype = $this->get_virtual_qtype( $question); + // $result = $virtualqtype->save_numerical_units($question); + if (isset($result->error)) { + return $result; + } else { + $units = &$result->units; + } + // Insert all the new answers + if (isset($question->answer) && !isset($question->answers)) { + $question->answers=$question->answer; + } + foreach ($question->answers as $key => $dataanswer) { + if ( trim($dataanswer) != '' ) { + $answer = new stdClass; + $answer->question = $question->id; + $answer->answer = trim($dataanswer); + $answer->fraction = $question->fraction[$key]; + $answer->feedback = trim($question->feedback[$key]); + + if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it + $answer->id = $oldanswer->id; + $DB->update_record("question_answers", $answer); + } else { // This is a completely new answer + $answer->id = $DB->insert_record("question_answers", $answer); + } + + // Set up the options object + if (!$options = array_shift($oldoptions)) { + $options = new stdClass; + } + $options->question = $question->id; + $options->answer = $answer->id; + $options->tolerance = trim($question->tolerance[$key]); + $options->tolerancetype = trim($question->tolerancetype[$key]); + $options->correctanswerlength = trim($question->correctanswerlength[$key]); + $options->correctanswerformat = trim($question->correctanswerformat[$key]); + + // Save options + if (isset($options->id)) { // reusing existing record + $DB->update_record('question_calculated', $options); + } else { // new options + $DB->insert_record('question_calculated', $options); + } + } + } + // delete old answer records + if (!empty($oldanswers)) { + foreach($oldanswers as $oa) { + $DB->delete_records('question_answers', array('id' => $oa->id)); + } + } + + // delete old answer records + if (!empty($oldoptions)) { + foreach($oldoptions as $oo) { + $DB->delete_records('question_calculated', array('id' => $oo->id)); + } + } + // $result = $QTYPES['numerical']->save_numerical_options($question); + // if (isset($result->error)) { + // return $result; + // } + + + if( isset($question->import_process)&&$question->import_process){ + $this->import_datasets($question); + } + // Report any problems. + if (!empty($result->notice)) { + return $result; + } + return true; + } + + function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) { + // Find out how many datasets are available + global $CFG, $DB, $QTYPES; + if(!$maxnumber = (int)$DB->get_field_sql( + "SELECT MIN(a.itemcount) + FROM {question_dataset_definitions} a, + {question_datasets} b + WHERE b.question = ? + AND a.id = b.datasetdefinition", array($question->id))) { + print_error('cannotgetdsforquestion', 'question', '', $question->id); + } + $sql = "SELECT i.* + FROM {question_datasets} d, + {question_dataset_definitions} i + WHERE d.question = ? + AND d.datasetdefinition = i.id + AND i.category != 0 + "; + if (!$question->options->synchronize || !$records = $DB->get_records_sql($sql, array($question->id))) { + $synchronize_calculated = false ; + }else { + $synchronize_calculated = true ; + } + + // Choose a random dataset + if ( $synchronize_calculated === false ) { + $state->options->datasetitem = rand(1, $maxnumber); + }else{ + $state->options->datasetitem = intval( $maxnumber * substr($attempt->timestart,-2) /100 ) ; + if ($state->options->datasetitem < 1) { + $state->options->datasetitem =1 ; + } else if ($state->options->datasetitem > $maxnumber){ + $state->options->datasetitem = $maxnumber ; + } + + }; + $state->options->dataset = + $this->pick_question_dataset($question,$state->options->datasetitem); + // create an array of answerids ??? why so complicated ??? + $answerids = array_values(array_map(create_function('$val', + 'return $val->id;'), $question->options->answers)); + // Shuffle the answers if required + if (!empty($cmoptions->shuffleanswers) and !empty($question->options->shuffleanswers)) { + $answerids = swapshuffle($answerids); + } + $state->options->order = $answerids; + // Create empty responses + if ($question->options->single) { + $state->responses = array('' => ''); + } else { + $state->responses = array(); + } + return true; + + } + + function save_session_and_responses(&$question, &$state) { + global $DB; + $responses = 'dataset'.$state->options->datasetitem.'-' ; + $responses .= implode(',', $state->options->order) . ':'; + $responses .= implode(',', $state->responses); + + // Set the legacy answer field + if (!$DB->set_field('question_states', 'answer', $responses, array('id'=> $state->id))) { + return false; + } + return true; + } + + function create_runtime_question($question, $form) { + $question = parent::create_runtime_question($question, $form); + $question->options->answers = array(); + foreach ($form->answers as $key => $answer) { + $a->answer = trim($form->answer[$key]); + $a->fraction = $form->fraction[$key];//new + $a->tolerance = $form->tolerance[$key]; + $a->tolerancetype = $form->tolerancetype[$key]; + $a->correctanswerlength = $form->correctanswerlength[$key]; + $a->correctanswerformat = $form->correctanswerformat[$key]; + $question->options->answers[] = clone($a); + } + + return $question; + } + + function validate_form($form) { + switch($form->wizardpage) { + case 'question': + $calculatedmessages = array(); + if (empty($form->name)) { + $calculatedmessages[] = get_string('missingname', 'quiz'); + } + if (empty($form->questiontext)) { + $calculatedmessages[] = get_string('missingquestiontext', 'quiz'); + } + // Verify formulas + foreach ($form->answers as $key => $answer) { + if ('' === trim($answer)) { + $calculatedmessages[] = + get_string('missingformula', 'quiz'); + } + if ($formulaerrors = + qtype_calculated_find_formula_errors($answer)) { + $calculatedmessages[] = $formulaerrors; + } + if (! isset($form->tolerance[$key])) { + $form->tolerance[$key] = 0.0; + } + if (! is_numeric($form->tolerance[$key])) { + $calculatedmessages[] = + get_string('tolerancemustbenumeric', 'quiz'); + } + } + + if (!empty($calculatedmessages)) { + $errorstring = "The following errors were found:
"; + foreach ($calculatedmessages as $msg) { + $errorstring .= $msg . '
'; + } + print_error($errorstring); + } + + break; + default: + return parent::validate_form($form); + break; + } + return true; + } + function finished_edit_wizard(&$form) { + return isset($form->backtoquiz); + } + // This gets called by editquestion.php after the standard question is saved + function print_next_wizard_page(&$question, &$form, $course) { + global $CFG, $USER, $SESSION, $COURSE; + + // Catch invalid navigation & reloads + if (empty($question->id) && empty($SESSION->calculated)) { + redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired.', 3); + } + + // See where we're coming from + switch($form->wizardpage) { + case 'question': + require("$CFG->dirroot/question/type/calculated/datasetdefinitions.php"); + break; + case 'datasetdefinitions': + case 'datasetitems': + require("$CFG->dirroot/question/type/calculated/datasetitems.php"); + break; + default: + print_error('invalidwizardpage', 'question'); + break; + } + } + + // This gets called by question2.php after the standard question is saved + function &next_wizard_form($submiturl, $question, $wizardnow){ + global $CFG, $SESSION, $COURSE; + + // Catch invalid navigation & reloads + if (empty($question->id) && empty($SESSION->calculated)) { + redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired. Cannot get next wizard form.', 3); + } + if (empty($question->id)){ + $question =& $SESSION->calculated->questionform; + } + + // See where we're coming from + switch($wizardnow) { + case 'datasetdefinitions': + require("$CFG->dirroot/question/type/calculated/datasetdefinitions_form.php"); + $mform =& new question_dataset_dependent_definitions_form("$submiturl?wizardnow=datasetdefinitions", $question); + break; + case 'datasetitems': + require("$CFG->dirroot/question/type/calculated/datasetitems_form.php"); + $regenerate = optional_param('forceregeneration', 0, PARAM_BOOL); + $mform =& new question_dataset_dependent_items_form("$submiturl?wizardnow=datasetitems", $question, $regenerate); + break; + default: + print_error('invalidwizardpage', 'question'); + break; + } + + return $mform; + } + + /** + * This method should be overriden if you want to include a special heading or some other + * html on a question editing page besides the question editing form. + * + * @param question_edit_form $mform a child of question_edit_form + * @param object $question + * @param string $wizardnow is '' for first page. + */ + function display_question_editing_page(&$mform, $question, $wizardnow){ + global $OUTPUT ; + switch ($wizardnow){ + case '': + //on first page default display is fine + parent::display_question_editing_page($mform, $question, $wizardnow); + return; + break; + case 'datasetdefinitions': + echo $OUTPUT->heading_with_help(get_string("choosedatasetproperties", "quiz"), 'questiondatasets', 'quiz'); + + /* $heading = get_string("question", "quiz").": ".$question->name; + $helpicon = new moodle_help_icon(); + $helpicon->text = get_string("choosedatasetproperties", "quiz"); + $helpicon->page = 'questiondatasets'; + $helpicon->module = 'quiz'; + echo $OUTPUT->heading($heading); + echo $OUTPUT->heading_with_help($helpicon);*/ + break; + case 'datasetitems': + echo $OUTPUT->heading_with_help(get_string("editdatasets", "quiz"), 'questiondatasets', 'quiz'); + + /* $heading = get_string("question", "quiz").": ".$question->name; + $helpicon = new moodle_help_icon(); + $helpicon->text = get_string("editdatasets", "quiz"); + $helpicon->page = 'questiondatasets'; + $helpicon->module = 'quiz'; + echo $OUTPUT->heading($heading); + echo $OUTPUT->heading_with_help($helpicon);*/ + break; + } + + + $mform->display(); + + } + + /** + * This method prepare the $datasets in a format similar to dadatesetdefinitions_form.php + * so that they can be saved + * using the function save_dataset_definitions($form) + * when creating a new calculated question or + * whenediting an already existing calculated question + * or by function save_as_new_dataset_definitions($form, $initialid) + * when saving as new an already existing calculated question + * + * @param object $form + * @param int $questionfromid default = '0' + */ + function preparedatasets(&$form , $questionfromid='0'){ + // the dataset names present in the edit_question_form and edit_calculated_form are retrieved + $possibledatasets = $this->find_dataset_names($form->questiontext); + $mandatorydatasets = array(); + foreach ($form->answers as $answer) { + $mandatorydatasets += $this->find_dataset_names($answer); + } + // if there are identical datasetdefs already saved in the original question. + // either when editing a question or saving as new + // they are retrieved using $questionfromid + if ($questionfromid!='0'){ + $form->id = $questionfromid ; + } + $datasets = array(); + $key = 0 ; + // always prepare the mandatorydatasets present in the answers + // the $options are not used here + foreach ($mandatorydatasets as $datasetname) { + if (!isset($datasets[$datasetname])) { + list($options, $selected) = + $this->dataset_options($form, $datasetname); + $datasets[$datasetname]=''; + $form->dataset[$key]=$selected ; + $key++; + } + } + // do not prepare possibledatasets when creating a question + // they will defined and stored with datasetdefinitions_form.php + // the $options are not used here + if ($questionfromid!='0'){ + + foreach ($possibledatasets as $datasetname) { + if (!isset($datasets[$datasetname])) { + list($options, $selected) = + $this->dataset_options($form, $datasetname,false); + $datasets[$datasetname]=''; + $form->dataset[$key]=$selected ; + $key++; + } + } + } + return $datasets ; + } + + /** + * this version save the available data at the different steps of the question editing process + * without using global $SESSION as storage between steps + * at the first step $wizardnow = 'question' + * when creating a new question + * when modifying a question + * when copying as a new question + * the general parameters and answers are saved using parent::save_question + * then the datasets are prepared and saved + * at the second step $wizardnow = 'datasetdefinitions' + * the datadefs final type are defined as private, category or not a datadef + * at the third step $wizardnow = 'datasetitems' + * the datadefs parameters and the data items are created or defined + * + * @param object question + * @param object $form + * @param int $course + * @param PARAM_ALPHA $wizardnow should be added as we are coming from question2.php + */ + function convert_answers (&$question, &$state){ + foreach ($question->options->answers as $key => $answer) { + $answer->answer = $this->substitute_variables($answer->answer, $state->options->dataset); + //evaluate the equations i.e {=5+4) + $qtext = ""; + $qtextremaining = $answer->answer ; + // while (preg_match('~\{(=)|%[[:digit]]\.=([^[:space:]}]*)}~', $qtextremaining, $regs1)) { + while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) { + + $qtextsplits = explode($regs1[0], $qtextremaining, 2); + $qtext =$qtext.$qtextsplits[0]; + $qtextremaining = $qtextsplits[1]; + if (empty($regs1[1])) { + $str = ''; + } else { + if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){ + $str=$formulaerrors ; + }else { + eval('$str = '.$regs1[1].';'); + $texteval= qtype_calculated_calculate_answer( + $str, $state->options->dataset, $answer->tolerance, + $answer->tolerancetype, $answer->correctanswerlength, + $answer->correctanswerformat, ''); + $str = $texteval->answer; + } + } + $qtext = $qtext.$regs1[0].$str ; + } + $answer->answer = $qtext.$qtextremaining ; ; + } + } + + function get_default_numerical_unit($question,$virtualqtype){ + $unit = ''; + return $unit ; + } + function grade_responses(&$question, &$state, $cmoptions) { + // Forward the grading to the virtual qtype + // We modify the question to look like a multichoice question + // for grading nothing to do +/* $numericalquestion = fullclone($question); + foreach ($numericalquestion->options->answers as $key => $answer) { + $answer = $numericalquestion->options->answers[$key]->answer; // for PHP 4.x + $numericalquestion->options->answers[$key]->answer = $this->substitute_variables_and_eval($answer, + $state->options->dataset); + }*/ + $virtualqtype = $this->get_virtual_qtype( $question); + return $virtualqtype->grade_responses($question, $state, $cmoptions) ; + } + + function response_summary($question, $state, $length=80, $formatting=true) { + // The actual response is the bit after the hyphen + return substr($state->answer, strpos($state->answer, '-')+1, $length); + } + + // ULPGC ecastro + function check_response(&$question, &$state) { + // Forward the checking to the virtual qtype + // We modify the question to look like a numerical question + $numericalquestion = clone($question); + $numericalquestion->options = clone($question->options); + foreach ($question->options->answers as $key => $answer) { + $numericalquestion->options->answers[$key] = clone($answer); + } + foreach ($numericalquestion->options->answers as $key => $answer) { + $answer = &$numericalquestion->options->answers[$key]; // for PHP 4.x + $answer->answer = $this->substitute_variables_and_eval($answer->answer, + $state->options->dataset); + } + $virtualqtype = $this->get_virtual_qtype( $question); + return $virtualqtype->check_response($numericalquestion, $state) ; + } + + // ULPGC ecastro + function get_actual_response(&$question, &$state) { + // Substitute variables in questiontext before giving the data to the + // virtual type + $virtualqtype = $this->get_virtual_qtype( $question); + $unit = '' ;//$virtualqtype->get_default_numerical_unit($question); + + // We modify the question to look like a multichoice question + $numericalquestion = clone($question); + $this->convert_answers ($numericalquestion, $state); + $this->convert_questiontext ($numericalquestion, $state); + /* $numericalquestion->questiontext = $this->substitute_variables_and_eval( + $numericalquestion->questiontext, $state->options->dataset);*/ + $responses = $virtualqtype->get_all_responses($numericalquestion, $state); + $response = reset($responses->responses); + $correct = $response->answer.' : '; + + $responses = $virtualqtype->get_actual_response($numericalquestion, $state); + + foreach ($responses as $key=>$response){ + $responses[$key] = $correct.$response; + } + + return $responses; + } + + function create_virtual_qtype() { + global $CFG; + require_once("$CFG->dirroot/question/type/multichoice/questiontype.php"); + return new question_multichoice_qtype(); + } + + + function comment_header($question) { + //$this->get_question_options($question); + $strheader = ''; + $delimiter = ''; + + $answers = $question->options->answers; + + foreach ($answers as $key => $answer) { + if (is_string($answer)) { + $strheader .= $delimiter.$answer; + } else { + $strheader .= $delimiter.$answer->answer; + } + $delimiter = '
'; + } + return $strheader; + } + + function comment_on_datasetitems($qtypeobj,$questionid,$questiontext, $answers,$data, $number) { //multichoice_ + global $DB; + $comment = new stdClass; + $comment->stranswers = array(); + $comment->outsidelimit = false ; + $comment->answers = array(); + /// Find a default unit: + /* if (!empty($questionid) && $unit = $DB->get_record('question_numerical_units', array('question'=> $questionid, 'multiplier' => 1.0))) { + $unit = $unit->unit; + } else { + $unit = ''; + }*/ + + $answers = fullclone($answers); + $strmin = get_string('min', 'quiz'); + $strmax = get_string('max', 'quiz'); + $errors = ''; + $delimiter = ': '; + foreach ($answers as $key => $answer) { + $answer->answer = $this->substitute_variables($answer->answer, $data); + //evaluate the equations i.e {=5+4) + $qtext = ""; + $qtextremaining = $answer->answer ; + while (preg_match('~\{=([^[:space:]}]*)}~', $qtextremaining, $regs1)) { + $qtextsplits = explode($regs1[0], $qtextremaining, 2); + $qtext =$qtext.$qtextsplits[0]; + $qtextremaining = $qtextsplits[1]; + if (empty($regs1[1])) { + $str = ''; + } else { + if( $formulaerrors = qtype_calculated_find_formula_errors($regs1[1])){ + $str=$formulaerrors ; + }else { + eval('$str = '.$regs1[1].';'); + } + } + $qtext = $qtext.$str ; + } + $answer->answer = $qtext.$qtextremaining ; ; + $comment->stranswers[$key]= $answer->answer ; + + + /* $formula = $this->substitute_variables($answer->answer,$data); + $formattedanswer = qtype_calculated_calculate_answer( + $answer->answer, $data, $answer->tolerance, + $answer->tolerancetype, $answer->correctanswerlength, + $answer->correctanswerformat, $unit); + if ( $formula === '*'){ + $answer->min = ' '; + $formattedanswer->answer = $answer->answer ; + }else { + eval('$answer->answer = '.$formula.';') ; + $virtualqtype->get_tolerance_interval($answer); + } + if ($answer->min === '') { + // This should mean that something is wrong + $comment->stranswers[$key] = " $formattedanswer->answer".'

'; + } else if ($formula === '*'){ + $comment->stranswers[$key] = $formula.' = '.get_string('anyvalue','qtype_calculated').'


'; + }else{ + $comment->stranswers[$key]= $formula.' = '.$formattedanswer->answer.'
' ; + $comment->stranswers[$key] .= $strmin. $delimiter.$answer->min.'---'; + $comment->stranswers[$key] .= $strmax.$delimiter.$answer->max; + $comment->stranswers[$key] .='
'; + $correcttrue->correct = $formattedanswer->answer ; + $correcttrue->true = $answer->answer ; + if ($formattedanswer->answer < $answer->min || $formattedanswer->answer > $answer->max){ + $comment->outsidelimit = true ; + $comment->answers[$key] = $key; + $comment->stranswers[$key] .=get_string('trueansweroutsidelimits','qtype_calculated',$correcttrue);//ERROR True answer '..' outside limits'; + }else { + $comment->stranswers[$key] .=get_string('trueanswerinsidelimits','qtype_calculated',$correcttrue);//' True answer :'.$calculated->trueanswer.' inside limits'; + } + $comment->stranswers[$key] .=''; + }*/ + } + return fullclone($comment); + } + + + + + + function get_correct_responses1(&$question, &$state) { + $virtualqtype = $this->get_virtual_qtype( $question); + /* if ($question->options->multichoice != 1 ) { + if($unit = $virtualqtype->get_default_numerical_unit($question)){ + $unit = $unit->unit; + } else { + $unit = ''; + } + foreach ($question->options->answers as $answer) { + if (((int) $answer->fraction) === 1) { + $answernumerical = qtype_calculated_calculate_answer( + $answer->answer, $state->options->dataset, $answer->tolerance, + $answer->tolerancetype, $answer->correctanswerlength, + $answer->correctanswerformat, ''); // remove unit + $correct = array('' => $answernumerical->answer); + $correct['answer']= $correct['']; + if (isset($correct['']) && $correct[''] != '*' && $unit ) { + $correct[''] .= ' '.$unit; + $correct['unit']= $unit; + } + return $correct; + } + } + }else{**/ + return $virtualqtype->get_correct_responses($question, $state) ; + // } + return null; + } + + function substitute_variables($str, $dataset) { + // testing for wrong numerical values + // all calculations used this function so testing here should be OK + + foreach ($dataset as $name => $value) { + $val = $value ; + if(! is_numeric($val)){ + $a = new stdClass; + $a->name = '{'.$name.'}' ; + $a->value = $value ; + echo $OUTPUT->notification(get_string('notvalidnumber','qtype_calculated',$a)); + $val = 1.0 ; + } + if($val < 0 ){ + $str = str_replace('{'.$name.'}', '('.$val.')', $str); + } else { + $str = str_replace('{'.$name.'}', $val, $str); + } + } + return $str; + } + function evaluate_equations($str, $dataset){ + $formula = $this->substitute_variables($str, $dataset) ; + if ($error = qtype_calculated_find_formula_errors($formula)) { + return $error; + } + return $str; + } + + + function substitute_variables_and_eval($str, $dataset) { + $formula = $this->substitute_variables($str, $dataset) ; + if ($error = qtype_calculated_find_formula_errors($formula)) { + return $error; + } + /// Calculate the correct answer + if (empty($formula)) { + $str = ''; + } else if ($formula === '*'){ + $str = '*'; + } else { + eval('$str = '.$formula.';'); + } + return $str; + } + + function get_dataset_definitions($questionid, $newdatasets) { + global $DB; + //get the existing datasets for this question + $datasetdefs = array(); + if (!empty($questionid)) { + global $CFG; + $sql = "SELECT i.* + FROM {question_datasets} d, + {question_dataset_definitions} i + WHERE d.question = ? + AND d.datasetdefinition = i.id + "; + if ($records = $DB->get_records_sql($sql, array($questionid))) { + foreach ($records as $r) { + $datasetdefs["$r->type-$r->category-$r->name"] = $r; + } + } + } + + foreach ($newdatasets as $dataset) { + if (!$dataset) { + continue; // The no dataset case... + } + + if (!isset($datasetdefs[$dataset])) { + //make new datasetdef + list($type, $category, $name) = explode('-', $dataset, 3); + $datasetdef = new stdClass; + $datasetdef->type = $type; + $datasetdef->name = $name; + $datasetdef->category = $category; + $datasetdef->itemcount = 0; + $datasetdef->options = 'uniform:1.0:10.0:1'; + $datasetdefs[$dataset] = clone($datasetdef); + } + } + return $datasetdefs; + } + + function save_dataset_definitions($form) { + global $DB; + // save synchronize + + // Save datasets + $datasetdefinitions = $this->get_dataset_definitions($form->id, $form->dataset); + $tmpdatasets = array_flip($form->dataset); + $defids = array_keys($datasetdefinitions); + foreach ($defids as $defid) { + $datasetdef = &$datasetdefinitions[$defid]; + if (isset($datasetdef->id)) { + if (!isset($tmpdatasets[$defid])) { + // This dataset is not used any more, delete it + $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $datasetdef->id)); + if ($datasetdef->category == 0) { // Question local dataset + $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id)); + $DB->delete_records('question_dataset_items', array('definition' => $datasetdef->id)); + } + } + // This has already been saved or just got deleted + unset($datasetdefinitions[$defid]); + continue; + } + + $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef); + + if (0 != $datasetdef->category) { + // We need to look for already existing + // datasets in the category. + // By first creating the datasetdefinition above we + // can manage to automatically take care of + // some possible realtime concurrence + if ($olderdatasetdefs = $DB->get_records_select( 'question_dataset_definitions', + "type = ? + AND name = ? + AND category = ? + AND id < ? + ORDER BY id DESC", array($datasetdef->type, $datasetdef->name, $datasetdef->category, $datasetdef->id))) { + + while ($olderdatasetdef = array_shift($olderdatasetdefs)) { + $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id)); + $datasetdef = $olderdatasetdef; + } + } + } + + // Create relation to this dataset: + $questiondataset = new stdClass; + $questiondataset->question = $form->id; + $questiondataset->datasetdefinition = $datasetdef->id; + $DB->insert_record('question_datasets', $questiondataset); + unset($datasetdefinitions[$defid]); + } + + // Remove local obsolete datasets as well as relations + // to datasets in other categories: + if (!empty($datasetdefinitions)) { + foreach ($datasetdefinitions as $def) { + $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $def->id)); + + if ($def->category == 0) { // Question local dataset + $DB->delete_records('question_dataset_definitions', array('id' => $def->id)); + $DB->delete_records('question_dataset_items', array('definition' => $def->id)); + } + } + } + } + /** This function create a copy of the datasets ( definition and dataitems) + * from the preceding question if they remain in the new question + * otherwise its create the datasets that have been added as in the + * save_dataset_definitions() + */ + function save_as_new_dataset_definitions($form, $initialid) { + global $CFG, $DB; + // Get the datasets from the intial question + $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset); + // $tmpdatasets contains those of the new question + $tmpdatasets = array_flip($form->dataset); + $defids = array_keys($datasetdefinitions);// new datasets + foreach ($defids as $defid) { + $datasetdef = &$datasetdefinitions[$defid]; + if (isset($datasetdef->id)) { + // This dataset exist in the initial question + if (!isset($tmpdatasets[$defid])) { + // do not exist in the new question so ignore + unset($datasetdefinitions[$defid]); + continue; + } + // create a copy but not for category one + if (0 == $datasetdef->category) { + $olddatasetid = $datasetdef->id ; + $olditemcount = $datasetdef->itemcount ; + $datasetdef->itemcount =0; + $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef); + //copy the dataitems + $olditems = $this->get_database_dataset_items($olddatasetid); + if (count($olditems) > 0 ) { + $itemcount = 0; + foreach($olditems as $item ){ + $item->definition = $datasetdef->id; + $DB->insert_record('question_dataset_items', $item); + $itemcount++; + } + //update item count + $datasetdef->itemcount =$itemcount; + $DB->update_record('question_dataset_definitions', $datasetdef); + } // end of copy the dataitems + }// end of copy the datasetdef + // Create relation to the new question with this + // copy as new datasetdef from the initial question + $questiondataset = new stdClass; + $questiondataset->question = $form->id; + $questiondataset->datasetdefinition = $datasetdef->id; + $DB->insert_record('question_datasets', $questiondataset); + unset($datasetdefinitions[$defid]); + continue; + }// end of datasetdefs from the initial question + // really new one code similar to save_dataset_definitions() + $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef); + + if (0 != $datasetdef->category) { + // We need to look for already existing + // datasets in the category. + // By first creating the datasetdefinition above we + // can manage to automatically take care of + // some possible realtime concurrence + if ($olderdatasetdefs = $DB->get_records_select( + 'question_dataset_definitions', + "type = ? + AND name = ? + AND category = ? + AND id < ? + ORDER BY id DESC", array($datasetdef->type, $datasetdef->name, $datasetdef->category, $datasetdef->id))) { + + while ($olderdatasetdef = array_shift($olderdatasetdefs)) { + $DB->delete_records('question_dataset_definitions', array('id' => $datasetdef->id)); + $datasetdef = $olderdatasetdef; + } + } + } + + // Create relation to this dataset: + $questiondataset = new stdClass; + $questiondataset->question = $form->id; + $questiondataset->datasetdefinition = $datasetdef->id; + $DB->insert_record('question_datasets', $questiondataset); + unset($datasetdefinitions[$defid]); + } + + // Remove local obsolete datasets as well as relations + // to datasets in other categories: + if (!empty($datasetdefinitions)) { + foreach ($datasetdefinitions as $def) { + $DB->delete_records('question_datasets', array('question' => $form->id, 'datasetdefinition' => $def->id)); + + if ($def->category == 0) { // Question local dataset + $DB->delete_records('question_dataset_definitions', array('id' => $def->id)); + $DB->delete_records('question_dataset_items', array('definition' => $def->id)); + } + } + } + } + +/// Dataset functionality + function pick_question_dataset($question, $datasetitem) { + // Select a dataset in the following format: + // An array indexed by the variable names (d.name) pointing to the value + // to be substituted + global $CFG, $DB; + if (!$dataitems = $DB->get_records_sql( + "SELECT i.id, d.name, i.value + FROM {question_dataset_definitions} d, + {question_dataset_items} i, + {question_datasets} q + WHERE q.question = ? + AND q.datasetdefinition = d.id + AND d.id = i.definition + AND i.itemnumber = ? ORDER by i.id DESC ", array($question->id, $datasetitem))) { + print_error('cannotgetdsfordependent', 'question', '', array($question->id, $datasetitem)); + } + $dataset = Array(); + foreach($dataitems as $id => $dataitem ){ + if (!isset($dataset[$dataitem->name])){ + $dataset[$dataitem->name] = $dataitem->value ; + }else { + // deleting the unused records could be added here + } + } + return $dataset; + } + + function dataset_options_from_database($form, $name,$prefix='',$langfile='quiz') { + + // First options - it is not a dataset... + $options['0'] = get_string($prefix.'nodataset', $langfile); + + // Construct question local options + global $CFG, $DB; + $type = 1 ; // only type = 1 (i.e. old 'LITERAL') has ever been used + if ( ! $currentdatasetdef = $DB->get_record_sql( + "SELECT a.* + FROM {question_dataset_definitions} a, + {question_datasets} b + WHERE a.id = b.datasetdefinition + AND a.type = '1' + AND b.question = ? + AND a.name = ?", array($form->id, $name))){ + $currentdatasetdef->type = '0'; + }; + $key = "$type-0-$name"; + if ($currentdatasetdef->type == $type + and $currentdatasetdef->category == 0) { + $options[$key] = get_string($prefix."keptlocal$type", $langfile); + } else { + $options[$key] = get_string($prefix."newlocal$type", $langfile); + } + // Construct question category options + $categorydatasetdefs = $DB->get_records_sql( + "SELECT b.question, a.* + FROM {question_datasets} b, + {question_dataset_definitions} a + WHERE a.id = b.datasetdefinition + AND a.type = '1' + AND a.category = ? + AND a.name = ?", array($form->category, $name)); + $type = 1 ; + $key = "$type-$form->category-$name"; + if (!empty($categorydatasetdefs)){ // there is at least one with the same name + if (isset($categorydatasetdefs[$form->id])) {// it is already used by this question + $options[$key] = get_string($prefix."keptcategory$type", $langfile); + } else { + $options[$key] = get_string($prefix."existingcategory$type", $langfile); + } + } else { + $options[$key] = get_string($prefix."newcategory$type", $langfile); + } + // All done! + return array($options, $currentdatasetdef->type + ? "$currentdatasetdef->type-$currentdatasetdef->category-$name" + : ''); + } + + function find_dataset_names($text) { + /// Returns the possible dataset names found in the text as an array + /// The array has the dataset name for both key and value + $datasetnames = array(); + while (preg_match('~\\{([[:alpha:]][^>} <{"\']*)\\}~', $text, $regs)) { + $datasetnames[$regs[1]] = $regs[1]; + $text = str_replace($regs[0], '', $text); + } + return $datasetnames; + } + + /** + * This function retrieve the item count of the available category shareable + * wild cards that is added as a comment displayed when a wild card with + * the same name is displayed in datasetdefinitions_form.php + */ + function get_dataset_definitions_category($form) { + global $CFG, $DB; + $datasetdefs = array(); + $lnamemax = 30; + if (!empty($form->category)) { + $sql = "SELECT i.*,d.* + FROM {question_datasets} d, + {question_dataset_definitions} i + WHERE i.id = d.datasetdefinition + AND i.category = ? + ; + "; + if ($records = $DB->get_records_sql($sql, array($form->category))) { + foreach ($records as $r) { + if ( !isset ($datasetdefs["$r->name"])) $datasetdefs["$r->name"] = $r->itemcount; + } + } + } + return $datasetdefs ; + } + + /** + * This function build a table showing the available category shareable + * wild cards, their name, their definition (Min, Max, Decimal) , the item count + * and the name of the question where they are used. + * This table is intended to be add before the question text to help the user use + * these wild cards + */ + + function print_dataset_definitions_category($form) { + global $CFG, $DB; + $datasetdefs = array(); + $lnamemax = 22; + $namestr =get_string('name', 'quiz'); + $minstr=get_string('min', 'quiz'); + $maxstr=get_string('max', 'quiz'); + $rangeofvaluestr=get_string('minmax','qtype_datasetdependent'); + $questionusingstr = get_string('usedinquestion','qtype_calculated'); + $itemscountstr = get_string('itemscount','qtype_datasetdependent'); + $text =''; + if (!empty($form->category)) { + list($category) = explode(',', $form->category); + $sql = "SELECT i.*,d.* + FROM {question_datasets} d, + {question_dataset_definitions} i + WHERE i.id = d.datasetdefinition + AND i.category = ?; + " ; + if ($records = $DB->get_records_sql($sql, array($category))) { + foreach ($records as $r) { + $sql1 = "SELECT q.* + FROM {question} q + WHERE q.id = ? + "; + if ( !isset ($datasetdefs["$r->type-$r->category-$r->name"])){ + $datasetdefs["$r->type-$r->category-$r->name"]= $r; + } + if ($questionb = $DB->get_records_sql($sql1, array($r->question))) { + $datasetdefs["$r->type-$r->category-$r->name"]->questions[$r->question]->name =$questionb[$r->question]->name ; + } + } + } + } + if (!empty ($datasetdefs)){ + + $text =""; + foreach ($datasetdefs as $datasetdef){ + list($distribution, $min, $max,$dec) = explode(':', $datasetdef->options, 4); + $text .=""; + } + $text .="
$namestr$rangeofvaluestr$itemscountstr$questionusingstr
$datasetdef->name $min - $max $datasetdef->itemcount  "; + foreach ($datasetdef->questions as $qu) { + //limit the name length displayed + if (!empty($qu->name)) { + $qu->name = (strlen($qu->name) > $lnamemax) ? + substr($qu->name, 0, $lnamemax).'...' : $qu->name; + } else { + $qu->name = ''; + } + $text .="    $qu->name
"; + } + $text .="
"; + }else{ + $text .=get_string('nosharedwildcard', 'qtype_calculated'); + } + return $text ; + } + function get_virtual_qtype() { + global $QTYPES; + // if ( isset($question->options->multichoice) && $question->options->multichoice == '1'){ + $this->virtualqtype =& $QTYPES['multichoice']; + // }else { + // $this->virtualqtype =& $QTYPES['numerical']; + // } + return $this->virtualqtype; + } + + +/// BACKUP FUNCTIONS //////////////////////////// + + /* + * Backup the data in the question + * + * This is used in question/backuplib.php + */ + function backup($bf,$preferences,$question,$level=6) { + global $DB; + $status = true; + + $calculateds = $DB->get_records("question_calculated",array("question" =>$question,"id")); + //If there are calculated-s + if ($calculateds) { + //Iterate over each calculateds + foreach ($calculateds as $calculated) { + $status = $status &&fwrite ($bf,start_tag("CALCULATED",$level,true)); + //Print calculated contents + fwrite ($bf,full_tag("ANSWER",$level+1,false,$calculated->answer)); + fwrite ($bf,full_tag("TOLERANCE",$level+1,false,$calculated->tolerance)); + fwrite ($bf,full_tag("TOLERANCETYPE",$level+1,false,$calculated->tolerancetype)); + fwrite ($bf,full_tag("CORRECTANSWERLENGTH",$level+1,false,$calculated->correctanswerlength)); + fwrite ($bf,full_tag("CORRECTANSWERFORMAT",$level+1,false,$calculated->correctanswerformat)); + //Now backup numerical_units + $status = question_backup_numerical_units($bf,$preferences,$question,7); + //Now backup required dataset definitions and items... + $status = question_backup_datasets($bf,$preferences,$question,7); + //End calculated data + $status = $status &&fwrite ($bf,end_tag("CALCULATED",$level,true)); + } + $calculated_options = $DB->get_records("question_calculated_options",array("questionid" => $question),"id"); + if ($calculated_options) { + //Iterate over each calculated_option + foreach ($calculated_options as $calculated_option) { + $status = fwrite ($bf,start_tag("CALCULATED_OPTIONS",$level,true)); + //Print calculated_option contents + fwrite ($bf,full_tag("SYNCHRONIZE",$level+1,false,$calculated_option->synchronize)); + fwrite ($bf,full_tag("MULTIPLECHOICE",$level+1,false,$calculated_option->multiplechoice)); + fwrite ($bf,full_tag("SINGLE",$level+1,false,$calculated_option->single)); + fwrite ($bf,full_tag("SHUFFLEANSWERS",$level+1,false,$calculated_option->shuffleanswers)); + fwrite ($bf,full_tag("CORRECTFEEDBACK",$level+1,false,$calculated_option->correctfeedback)); + fwrite ($bf,full_tag("PARTIALLYCORRECTFEEDBACK",$level+1,false,$calculated_option->partiallycorrectfeedback)); + fwrite ($bf,full_tag("INCORRECTFEEDBACK",$level+1,false,$calculated_option->incorrectfeedback)); + fwrite ($bf,full_tag("ANSWERNUMBERING",$level+1,false,$calculated_option->answernumbering)); + $status = fwrite ($bf,end_tag("CALCULATED_OPTIONS",$level,true)); + } + } + $status = question_backup_numerical_options($bf,$preferences,$question,$level); + + } + return $status; + } + +/// RESTORE FUNCTIONS ///////////////// + + /* + * Restores the data in the question + * + * This is used in question/restorelib.php + */ + function restore($old_question_id,$new_question_id,$info,$restore) { + global $DB; + + $status = true; + + //Get the calculated-s array + $calculateds = $info['#']['CALCULATED']; + + //Iterate over calculateds + for($i = 0; $i < sizeof($calculateds); $i++) { + $cal_info = $calculateds[$i]; + //traverse_xmlize($cal_info); //Debug + //print_object ($GLOBALS['traverse_array']); //Debug + //$GLOBALS['traverse_array']=""; //Debug + + //Now, build the question_calculated record structure + $calculated->question = $new_question_id; + $calculated->answer = backup_todb($cal_info['#']['ANSWER']['0']['#']); + $calculated->tolerance = backup_todb($cal_info['#']['TOLERANCE']['0']['#']); + $calculated->tolerancetype = backup_todb($cal_info['#']['TOLERANCETYPE']['0']['#']); + $calculated->correctanswerlength = backup_todb($cal_info['#']['CORRECTANSWERLENGTH']['0']['#']); + $calculated->correctanswerformat = backup_todb($cal_info['#']['CORRECTANSWERFORMAT']['0']['#']); + + ////We have to recode the answer field + $answer = backup_getid($restore->backup_unique_code,"question_answers",$calculated->answer); + if ($answer) { + $calculated->answer = $answer->new_id; + } + + //The structure is equal to the db, so insert the question_calculated + $newid = $DB->insert_record ("question_calculated",$calculated); + + //Do some output + if (($i+1) % 50 == 0) { + if (!defined('RESTORE_SILENTLY')) { + echo "."; + if (($i+1) % 1000 == 0) { + echo "
"; + } + } + backup_flush(300); + } + //Get the calculated_options array + // need to check as old questions don't have calculated_options record + if(isset($info['#']['CALCULATED_OPTIONS'])){ + $calculatedoptions = $info['#']['CALCULATED_OPTIONS']; + + //Iterate over calculated_options + for($i = 0; $i < sizeof($calculatedoptions); $i++){ + $cal_info = $calculatedoptions[$i]; + //traverse_xmlize($cal_info); //Debug + //print_object ($GLOBALS['traverse_array']); //Debug + //$GLOBALS['traverse_array']=""; //Debug + + //Now, build the question_calculated_options record structure + $calculated_options->questionid = $new_question_id; + $calculated_options->synchronize = backup_todb($cal_info['#']['SYNCHRONIZE']['0']['#']); + // $calculated_options->multichoice = backup_todb($cal_info['#']['MULTICHOICE']['0']['#']); + $calculated_options->single = backup_todb($cal_info['#']['SINGLE']['0']['#']); + $calculated_options->shuffleanswers = isset($cal_info['#']['SHUFFLEANSWERS']['0']['#'])?backup_todb($mul_info['#']['SHUFFLEANSWERS']['0']['#']):''; + $calculated_options->correctfeedback = backup_todb($cal_info['#']['CORRECTFEEDBACK']['0']['#']); + $calculated_options->partiallycorrectfeedback = backup_todb($cal_info['#']['PARTIALLYCORRECTFEEDBACK']['0']['#']); + $calculated_options->incorrectfeedback = backup_todb($cal_info['#']['INCORRECTFEEDBACK']['0']['#']); + $calculated_options->answernumbering = backup_todb($cal_info['#']['ANSWERNUMBERING']['0']['#']); + + //The structure is equal to the db, so insert the question_calculated_options + $newid = $DB->insert_record ("question_calculated_options",$calculated_options); + + //Do some output + if (($i+1) % 50 == 0) { + if (!defined('RESTORE_SILENTLY')) { + echo "."; + if (($i+1) % 1000 == 0) { + echo "
"; + } + } + backup_flush(300); + } + } + } + //Now restore numerical_units + $status = question_restore_numerical_units ($old_question_id,$new_question_id,$cal_info,$restore); + $status = question_restore_numerical_options($old_question_id,$new_question_id,$info,$restore); + //Now restore dataset_definitions + if ($status && $newid) { + $status = question_restore_dataset_definitions ($old_question_id,$new_question_id,$cal_info,$restore); + } + + if (!$newid) { + $status = false; + } + } + + return $status; + } + + /** + * Runs all the code required to set up and save an essay question for testing purposes. + * Alternate DB table prefix may be used to facilitate data deletion. + */ + function generate_test($name, $courseid = null) { + global $DB; + list($form, $question) = parent::generate_test($name, $courseid); + $form->feedback = 1; + $form->multiplier = array(1, 1); + $form->shuffleanswers = 1; + $form->noanswers = 1; + $form->qtype ='calculatedmulti'; + $question->qtype ='calculatedmulti'; + $form->answers = array('{a} + {b}'); + $form->fraction = array(1); + $form->tolerance = array(0.01); + $form->tolerancetype = array(1); + $form->correctanswerlength = array(2); + $form->correctanswerformat = array(1); + $form->questiontext = "What is {a} + {b}?"; + + if ($courseid) { + $course = $DB->get_record('course', array('id'=> $courseid)); + } + + $new_question = $this->save_question($question, $form, $course); + + $dataset_form = new stdClass(); + $dataset_form->nextpageparam["forceregeneration"]= 1; + $dataset_form->calcmin = array(1 => 1.0, 2 => 1.0); + $dataset_form->calcmax = array(1 => 10.0, 2 => 10.0); + $dataset_form->calclength = array(1 => 1, 2 => 1); + $dataset_form->number = array(1 => 5.4 , 2 => 4.9); + $dataset_form->itemid = array(1 => '' , 2 => ''); + $dataset_form->calcdistribution = array(1 => 'uniform', 2 => 'uniform'); + $dataset_form->definition = array(1 => "1-0-a", + 2 => "1-0-b"); + $dataset_form->nextpageparam = array('forceregeneration' => false); + $dataset_form->addbutton = 1; + $dataset_form->selectadd = 1; + $dataset_form->courseid = $courseid; + $dataset_form->cmid = 0; + $dataset_form->id = $new_question->id; + $this->save_dataset_items($new_question, $dataset_form); + + return $new_question; + } +} +//// END OF CLASS //// + +////////////////////////////////////////////////////////////////////////// +//// INITIATION - Without this line the question type is not in use... /// +////////////////////////////////////////////////////////////////////////// +question_register_questiontype(new question_calculatedmulti_qtype()); + +function qtype_calculatedmulti_calculate_answer($formula, $individualdata, + $tolerance, $tolerancetype, $answerlength, $answerformat='1', $unit='') { +/// The return value has these properties: +/// ->answer the correct answer +/// ->min the lower bound for an acceptable response +/// ->max the upper bound for an accetpable response + + /// Exchange formula variables with the correct values... + global $QTYPES; + $answer = $QTYPES['calculated']->substitute_variables_and_eval($formula, $individualdata); + if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */ + /*** Adjust to the correct number of decimals ***/ + if (stripos($answer,'e')>0 ){ + $answerlengthadd = strlen($answer)-stripos($answer,'e'); + }else { + $answerlengthadd = 0 ; + } + $calculated->answer = round(floatval($answer), $answerlength+$answerlengthadd); + + if ($answerlength) { + /* Try to include missing zeros at the end */ + + if (preg_match('~^(.*\\.)(.*)$~', $calculated->answer, $regs)) { + $calculated->answer = $regs[1] . substr( + $regs[2] . '00000000000000000000000000000000000000000x', + 0, $answerlength) + . $unit; + } else { + $calculated->answer .= + substr('.00000000000000000000000000000000000000000x', + 0, $answerlength + 1) . $unit; + } + } else { + /* Attach unit */ + $calculated->answer .= $unit; + } + + } else if ($answer) { // Significant figures does only apply if the result is non-zero + + // Convert to positive answer... + if ($answer < 0) { + $answer = -$answer; + $sign = '-'; + } else { + $sign = ''; + } + + // Determine the format 0.[1-9][0-9]* for the answer... + $p10 = 0; + while ($answer < 1) { + --$p10; + $answer *= 10; + } + while ($answer >= 1) { + ++$p10; + $answer /= 10; + } + // ... and have the answer rounded of to the correct length + $answer = round($answer, $answerlength); + + // Have the answer written on a suitable format, + // Either scientific or plain numeric + if (-2 > $p10 || 4 < $p10) { + // Use scientific format: + $eX = 'e'.--$p10; + $answer *= 10; + if (1 == $answerlength) { + $calculated->answer = $sign.$answer.$eX.$unit; + } else { + // Attach additional zeros at the end of $answer, + $answer .= (1==strlen($answer) ? '.' : '') + . '00000000000000000000000000000000000000000x'; + $calculated->answer = $sign + .substr($answer, 0, $answerlength +1).$eX.$unit; + } + } else { + // Stick to plain numeric format + $answer *= "1e$p10"; + if (0.1 <= $answer / "1e$answerlength") { + $calculated->answer = $sign.$answer.$unit; + } else { + // Could be an idea to add some zeros here + $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '') + . '00000000000000000000000000000000000000000x'; + $oklen = $answerlength + ($p10 < 1 ? 2-$p10 : 1); + $calculated->answer = $sign.substr($answer, 0, $oklen).$unit; + } + } + + } else { + $calculated->answer = 0.0; + } + + /// Return the result + return $calculated; +} + + +function qtype_calculatedmulti_find_formula_errors($formula) { +/// Validates the formula submitted from the question edit page. +/// Returns false if everything is alright. +/// Otherwise it constructs an error message + // Strip away dataset names + while (preg_match('~\\{[[:alpha:]][^>} <{"\']*\\}~', $formula, $regs)) { + $formula = str_replace($regs[0], '1', $formula); + } + + // Strip away empty space and lowercase it + $formula = strtolower(str_replace(' ', '', $formula)); + + $safeoperatorchar = '-+/*%>:^\~