diff options
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | combinable/combinable.php | 272 | ||||
-rw-r--r-- | combinable/renderer.php | 31 | ||||
-rw-r--r-- | lang/en/qtype_algebra.php | 1 | ||||
-rw-r--r-- | questiontype.php | 152 |
5 files changed, 331 insertions, 127 deletions
@@ -27,7 +27,7 @@ Updated to Moodle 2.0 by Stefan Raffeiner <stefan.raffeiner@gmail.com> Updated to Moodle 2.1 by Jean-Michel VĂ©drine <vedrine@univ-st-etienne.fr> This plugin is now maintained by Jean-Michel VĂ©drine. This version is upgraded to -work with Moodle 2.8 and ulteriors versions. +work with Moodle 3.0 and ulteriors versions. It has been tested with Moodle versions up to 3.7. For support use the Moodle quiz forum at https://moodle.org/mod/forum/view.php?id=737 diff --git a/combinable/combinable.php b/combinable/combinable.php new file mode 100644 index 0000000..e956073 --- /dev/null +++ b/combinable/combinable.php @@ -0,0 +1,272 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Defines the hooks necessary to make the algebra question type combinable + * + * @package qtype_algebra + * @copyright 2019 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/question/type/algebra/parser.php'); + +define('SYMB_QUESTION_NUMANS_START', 2); +define('SYMB_QUESTION_NUMANS_ADD', 1); + +class qtype_combined_combinable_type_algebra extends qtype_combined_combinable_type_base { + + protected $identifier = 'algebra'; + + protected function extra_question_properties() { + return array('answerprefix' => '', 'allowedfuncs' => array('all' => 1)); + } + + protected function extra_answer_properties() { + return array('fraction' => '1', 'feedback' => array('text' => '', 'format' => FORMAT_PLAIN)); + } + + public function subq_form_fragment_question_option_fields() { + return array('compareby' => null, + 'nchecks' => null, + 'disallow' => null, + 'allowedfuncs' => null); + } +} + + +class qtype_combined_combinable_algebra extends qtype_combined_combinable_text_entry { + + /** + * @param moodleform $combinedform + * @param MoodleQuickForm $mform + * @param $repeatenabled + * @return mixed + */ + public function add_form_fragment(moodleform $combinedform, MoodleQuickForm $mform, $repeatenabled) { + global $CFG; + $mform->addElement('select', $this->form_field_name('compareby'), get_string('compareby', 'qtype_algebra'), + array( "sage" => get_string('comparesage', 'qtype_algebra'), + "eval" => get_string('compareeval', 'qtype_algebra'), + "equiv" => get_string('compareequiv', 'qtype_algebra') + )); + $mform->setDefault($this->form_field_name('compareby'), $CFG->qtype_algebra_method); + $chkarray = array( '1', '2', '3', '5', '7', + '10', '20', '30', '50', '70', + '100', '200', '300', '500', '700', '1000'); + $mform->addElement('select', $this->form_field_name('nchecks'), get_string('nchecks', 'qtype_algebra'), + array_combine($chkarray, $chkarray)); + $mform->setDefault($this->form_field_name('nchecks'), '10'); + $mform->addElement('text', $this->form_field_name('tolerance'), get_string('tolerance', 'qtype_algebra')); + $mform->setType($this->form_field_name('tolerance'), PARAM_NUMBER); + $mform->setDefault($this->form_field_name('tolerance'), '0.001'); + // Add an entry for a disallowed expression. + $mform->addElement('text', $this->form_field_name('disallow'), get_string('disallow', 'qtype_algebra'), array('size' => 55)); + $mform->setType($this->form_field_name('disallow'), PARAM_RAW); + $varels = array(); + $varels[] = $mform->createElement('text', $this->form_field_name('variable[0]'), get_string('variablename', 'qtype_algebra'), array('size' => 10)); + $mform->setType($this->form_field_name('variable'), PARAM_RAW); + $varels[] = $mform->createElement('text', $this->form_field_name('varmin[0]'), get_string('varmin', 'qtype_algebra'), array('size' => 10)); + $mform->setType($this->form_field_name('varmin'), PARAM_RAW); + $varels[] = $mform->createElement('text', $this->form_field_name('varmax[0]'), get_string('varmax', 'qtype_algebra'), array('size' => 10)); + $mform->setType($this->form_field_name('varmax'), PARAM_RAW); + $mform->addGroup($varels, $this->form_field_name('variables'), + get_string('variable', 'qtype_algebra'), '', false); + $mform->setDefault($this->form_field_name('applydictionarycheck'), 1); + $answerel = array($mform->createElement('text', + $this->form_field_name('answer'), + get_string('answerx', 'qtype_algebra'), + array('size' => 57, 'class' => 'tweakcss'))); + + if ($this->questionrec !== null) { + $countanswers = count($this->questionrec->options->answers); + } else { + $countanswers = 0; + } + + if ($repeatenabled) { + $defaultstartnumbers = SYMB_QUESTION_NUMANS_START; + $repeatsatstart = max($defaultstartnumbers, $countanswers + SYMB_QUESTION_NUMANS_ADD); + } else { + $repeatsatstart = $countanswers; + } + + $combinedform->repeat_elements($answerel, + $repeatsatstart, + array(), + $this->form_field_name('noofchoices'), + $this->form_field_name('morechoices'), + SYMB_QUESTION_NUMANS_ADD, + get_string('addmoreanswerblanks', 'qtype_algebra'), + true); + $mform->setType($this->form_field_name('answer'), PARAM_RAW_TRIMMED); + } + + public function data_to_form($context, $fileoptions) { + $answers = array('answer' => array()); + if ($this->questionrec !== null) { + foreach ($this->questionrec->options->answers as $answer) { + $answers['answer'][] = $answer->answer; + } + $variable = array_pop($this->questionrec->options->variables); + $variables['variable'][] = $variable->name; + $variables['varmin'][] = $variable->min; + $variables['varmax'][] = $variable->max; + } + $data = parent::data_to_form($context, $fileoptions) + $answers + $variables; + + + return $data; + } + + + public function validate() { + $errors = array(); + // Regular expression string to match a number. + $renumber = '/([+-]*(([0-9]+\.[0-9]*)|([0-9]+)|(\.[0-9]+))|'. + '(([0-9]+\.[0-9]*)|([0-9]+)|(\.[0-9]+))E([-+]?\d+))/A'; + + // Perform sanity checks on the variables. + $vars = $this->formdata->variable;; + // Create an array of defined variables. + $varlist = array(); + foreach ($vars as $key => $var) { + $trimvar = trim($var); + $trimmin = trim($this->formdata->varmin[$key]); + $trimmax = trim($this->formdata->varmax[$key]); + // Check that there is a non empty variable name otherwise skip. + if ($trimvar == '') { + continue; + } + // Check that this variable does not have the same name as a function. + if (in_array($trimvar, qtype_algebra_parser::$functions) or in_array($trimvar, qtype_algebra_parser::$specials)) { + $errors[$this->form_field_name("variables")] = get_string('illegalvarname', 'qtype_algebra', $trimvar); + } + // Check that this variable has not been defined before. + if (in_array($trimvar, $varlist)) { + $errors[$this->form_field_name("variables")] = get_string('duplicatevar', 'qtype_algebra', $trimvar); + } else { + // Add the variable to the list of defined variables. + $varlist[] = $trimvar; + } + // If the comparison algorithm selected is evaluate then ensure that each variable + // has a valid minimum and maximum defined. For the other types of comparison we can + // ignore the range. + if ($this->formdata->compareby == 'eval') { + // Check that a minimum has been defined. + if ($trimmin == '') { + $errors[$this->form_field_name("variables")] = get_string('novarmin', 'qtype_algebra'); + } else if (!preg_match($renumber, $trimmin)) { + // If there is one check that it's a number. + $errors[$this->form_field_name("variables")] = get_string('notanumber', 'qtype_algebra'); + } + if ($trimmax == '') { + $errors[$this->form_field_name("variables")] = get_string('novarmax', 'qtype_algebra'); + } else if (!preg_match($renumber, $trimmax)) { + // If there is one check that it is a number. + $errors[$this->form_field_name("variables")] = get_string('notanumber', 'qtype_algebra'); + } + // Check that the minimum is less that the maximum! + if ((float)$trimmin > (float)$trimmax) { + $errors[$this->form_field_name("variable")] = get_string('varmingtmax', 'qtype_algebra'); + } + } // End check for eval type. + } // End loop over variables. + // Check that at least one variable is defined. + if (count($varlist) == 0) { + $errors[$this->form_field_name('variables')] = get_string('notenoughvars', 'qtype_algebra'); + } + + // Now perform the sanity checks on the answers. + // Create a parser which we will use to check that the answers are understandable. + $p = new qtype_algebra_parser; + $answers = $this->formdata->answer; + $answercount = 0; + $maxgrade = false; + // Create an empty array to store the used variables. + $ansvars = array(); + // Create an empty array to store the used functions. + $ansfuncs = array(); + // Loop over all the answers in the form. + foreach ($answers as $key => $answer) { + // Try to parse the answer string using the parser. If this fails it will + // throw an exception which we catch to generate the associated error string + // for the expression. + try { + $expr = $p->parse($answer); + // Add any new variables to the list we are keeping. First we get the list + // of variables in this answer. Then we get the array of variables which are + // in this answer that are not in any previous answer (using array_diff). + // Finally we merge this difference array with the list of all variables so far. + $tmpvars = $expr->get_variables(); + $ansvars = array_merge($ansvars, array_diff($tmpvars, $ansvars)); + // Check that all the variables in this answer have been declared. + // Do this by looking for a non-empty array to be returned from the array_diff + // between the list of all declared variables and the variables in this answer. + if ($d = array_diff($tmpvars, $varlist)) { + $errors[$this->form_field_name('answer['.$key.']')] = get_string('undefinedvar', 'qtype_algebra', "'".implode("', '", $d)."'"); + } + // Do the same for functions which we did for variables. + $ansfuncs = array_merge($ansfuncs, array_diff($expr->get_functions(), $ansfuncs)); + // Check that this is not an empty answer. + if (!is_a($expr, "qtype_algebra_parser_nullterm")) { + // Increase the number of answers. + $answercount++; + } + } catch (Exception $e) { + $errors[$this->form_field_name('answer['.$key.']')] = $e->getMessage(); + // Return here because subsequent errors may be wrong due to not counting the answer + // which just failed to parse. + return $errors; + } + } + // Check that we have at least one answer. + if ($answercount == 0) { + $errors[$this->form_field_name('answer[0]')] = get_string('notenoughanswers', 'qtype_algebra'); + } + + // Check for variables which are defined but never used. + // Do this by looking for a non-empty array to be returned from array_diff. + if ($d = array_diff($varlist, $ansvars)) { + // Loop over all the variables in the form. + foreach ($vars as $key => $var) { + $trimvar = trim($var); + // If the variable is in the unused array then add the error message to that variable. + if (in_array($trimvar, $d)) { + $errors[$this->form_field_name('variable['.$key.']')] = get_string('unusedvar', 'qtype_algebra'); + } + } + } + + // Check that the tolerance is greater than or equal to zero. + if ($this->formdata->tolerance < 0) { + $errors[$this->form_field_name('tolerance')] = get_string('toleranceltzero', 'qtype_algebra'); + } + + + return $errors; + } + + public function get_sup_sub_editor_option() { + return null; + } + + public function has_submitted_data() { + return $this->submitted_data_array_not_empty('answer') || parent::has_submitted_data(); + } +} diff --git a/combinable/renderer.php b/combinable/renderer.php new file mode 100644 index 0000000..524c330 --- /dev/null +++ b/combinable/renderer.php @@ -0,0 +1,31 @@ +<?php +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see <http://www.gnu.org/licenses/>. + +/** + * Combined question embedded sub question renderer class. + * + * @package qtype_algebra + * @copyright 2019 Jean-Michel Vedrinr + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +class qtype_algebra_embedded_renderer extends qtype_combined_text_entry_renderer_base { + +} diff --git a/lang/en/qtype_algebra.php b/lang/en/qtype_algebra.php index c1b6f6c..81baad5 100644 --- a/lang/en/qtype_algebra.php +++ b/lang/en/qtype_algebra.php @@ -46,6 +46,7 @@ $string['unknownterm'] = 'Syntax Error: Unknown term found at \'{$a}\' in the ex $string['algebraoptions'] = 'Options'; $string['answermustbegiven'] = 'You must enter an answer if there is a grade or feedback.'; $string['answerno'] = 'Answer {$a}'; +$string['answerx'] = 'Answer {no}'; $string['addmoreanswerblanks'] = 'Blanks for {no} More Answers'; $string['addmorevariableblanks'] = 'Blanks for {no} More Variables'; $string['allfunctions'] = 'All Functions'; diff --git a/questiontype.php b/questiontype.php index c3a0674..263408e 100644 --- a/questiontype.php +++ b/questiontype.php @@ -83,141 +83,40 @@ class qtype_algebra extends question_type { * * @param object $question This holds the information from the editing form, * it is not a standard question object. - * @return array of variable object IDs */ public function save_question_variables($question) { global $DB; - // Create the results class. - $result = new stdClass; - // Get all the old answers from the database as an array. - if (!$oldvars = $DB->get_records('qtype_algebra_variables', array('questionid' => $question->id), 'id ASC')) { - $oldvars = array(); - } - // Create an array of the variable IDs for the question. - $variables = array(); + // Get all the old variables from the database as an array. + $oldvars = $DB->get_records('qtype_algebra_variables', + array('questionid' => $question->id), 'id ASC'); - // Loop over all the answers in the question form and write them to the database. + // Loop over all the variables in the question form and write them to the database. foreach ($question->variable as $key => $varname) { // Check to see that there is a variable and skip any which are empty. if ($varname == '') { continue; } - // Get the old variable from the array and overwrite what is required, if there - // is no old variable then we skip to the 'else' clause. - if ($oldvar = array_shift($oldvars)) { // Existing variable, so reuse it. - $var = $oldvar; - $var->name = trim($varname); - $var->min = trim($question->varmin[$key]); - $var->max = trim($question->varmax[$key]); - // Update the record in the database to denote this change. - if (!$DB->update_record('qtype_algebra_variables', $var)) { - throw new Exception("Could not update algebra question variable (id=$var->id)"); - } - } else { - // This is a completely new variable so we have to create a new record. - $var = new stdClass; - $var->name = trim($varname); - $var->questionid = $question->id; - $var->min = trim($question->varmin[$key]); - $var->max = trim($question->varmax[$key]); - // Insert a new record into the database table. - if (!$var->id = $DB->insert_record('qtype_algebra_variables', $var)) { - throw new Exception("Could not insert algebra question variable '$varname'!"); - } + // Update an existing variable if possible. + $variable = array_shift($oldvars); + if (!$variable) { + $variable = new stdClass(); + $variable->questionid = $question->id; + $variable->name = ''; + $variable->min = ''; + $variable->max = ''; + $variable->id = $DB->insert_record('qtype_algebra_variables', $variable); } - // Add the variable ID to the array of IDs. - $variables[] = $var->id; + $variable->name = trim($varname); + $variable->min = trim($question->varmin[$key]); + $variable->max = trim($question->varmax[$key]); + $DB->update_record('qtype_algebra_variables', $variable); } // End loop over variables. // Delete any left over old variables records. foreach ($oldvars as $oldvar) { $DB->delete_records('qtype_algebra_variables', array('id' => $oldvar->id)); } - // Finally we are all done so return the result! - return $variables; - } - - /** - * Saves the questions answers to the database - * - * This is called by {@link save_question_options()} to save the answers to the question to - * the database from the data in the submitted form. This method should probably be in the - * questin base class rather than in the algebra subclass since the code is common to multiple - * question types and originally comes from the shortanswer question type. The method returns - * a list of the answer ID written to the database or throws an exception if an error is detected. - * - * @param object $question This holds the information from the editing form, - * it is not a standard question object. - * @return array of answer IDs which were written to the database - */ - public function save_question_answers($question) { - global $CFG, $DB; - - $context = $question->context; - // Create the results class. - $result = new stdClass; - - // Get all the old answers from the database as an array. - if (!$oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC')) { - $oldanswers = array(); - } - // Create an array of the answer IDs for the question. - $answers = array(); - // Set the maximum answer fraction to be -1. We will check this at the end of our - // loop over the questions and if it is not 100% (=1.0) then we will flag an error. - $maxfraction = -1; - - // Loop over all the answers in the question form and write them to the database. - foreach ($question->answer as $key => $answerdata) { - // Check for, and ignore, completely blank answer from the form. - if (trim($answerdata) == '' && $question->fraction[$key] == 0 && - html_is_blank($question->feedback[$key]['text'])) { - continue; - } - // Update an existing answer if possible. - $answer = array_shift($oldanswers); - if (!$answer) { - $answer = new stdClass(); - $answer->question = $question->id; - $answer->answer = ''; - $answer->feedback = ''; - $answer->feedbackformat = FORMAT_HTML; - if (!$answer->id = $DB->insert_record('question_answers', $answer)) { - throw new Exception("Could not create new algebra question answer"); - } - } - - $answer->answer = trim($answerdata); - $answer->fraction = $question->fraction[$key]; - $answer->feedback = $this->import_or_save_files($question->feedback[$key], - $context, 'question', 'answerfeedback', $answer->id); - $answer->feedbackformat = $question->feedback[$key]['format']; - if (!$DB->update_record('question_answers', $answer)) { - throw new Exception("Could not update algebra question answer (id=$answer->id)"); - } - - $answers[] = $answer->id; - if ($question->fraction[$key] > $maxfraction) { - $maxfraction = $question->fraction[$key]; - } - } // End loop over answers. - - // Perform sanity check on the maximum fractional grade which should be 100%. - if ($maxfraction != 1) { - $maxfraction = $maxfraction * 100; - throw new Exception(get_string('fractionsnomax', 'quiz', $maxfraction)); - } - - // Delete any left over old answer records. - $fs = get_file_storage(); - foreach ($oldanswers as $oldanswer) { - $fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id); - $DB->delete_records('question_answers', array('id' => $oldanswer->id)); - } - - // Finally we are all done so return the result! - return $answers; } /** @@ -236,7 +135,7 @@ class qtype_algebra extends question_type { // then add the answers and variables to the database. try { // First write out all the variables associated with the question. - $variables = $this->save_question_variables($question); + $this->save_question_variables($question); // Loop over all the answers in the question form and parse them to generate // a parser string. This ensures a constant formatting is stored in the database. @@ -247,11 +146,11 @@ class qtype_algebra extends question_type { } // Now we need to write out all the answers to the question to the database. - $answers = $this->save_question_answers($question); + $this->save_question_answers($question); } catch (Exception $e) { // Error when adding answers or variables to the database so create a result class - // and put the error string in the error member funtion and then return the class + // and put the error string in the error member function and then return the class // This keeps us compatible with the existing save_question_options methods. $result = new stdClass; $result->error = $e->getMessage(); @@ -290,14 +189,15 @@ class qtype_algebra extends question_type { // Get the information from the database table. If this fails then immediately bail. // Note unlike the save_question_options base class method this method DOES get the question's // answers along with any answer extensions. - global $DB; + global $DB, $OUTPUT; if (!parent::get_question_options($question)) { return false; } // Check that we have answers and if not then bail since this question type requires answers. if (count($question->options->answers) == 0) { - notify('Failed to load question answers from the table question_answers for questionid ' . - $question->id); + echo $OUTPUT->notification('Failed to load question answers from the table ' . + 'qtype_algebra_answers for questionid ' . $question->id); + return false; } // Now get the variables from the database as well. @@ -305,8 +205,8 @@ class qtype_algebra extends question_type { // Check that we have variables and if not then bail since this question type requires variables. if (count($question->options->variables) == 0) { - notify('Failed to load question variables from the table qtype_algebra_variables '. - "for questionid $question->id"); + echo $OUTPUT->notification('Failed to load question variables from the table ' . + 'qtype_algebra_variables for questionid ' . $question->id); return false; } |