. /** * 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); define('SYMB_QUESTION_NUMVARS_ADD', 1); define('SYMB_QUESTION_NUMVARS_START', 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 { /** * Get the form fields needed to edit one variable. * @param MoodleQuickForm $mform the form being built. * @return array of form fields. */ protected function variable_group($mform) { $grouparray = array(); $grouparray[] = $mform->createElement('text', $this->form_field_name('variable'), get_string('variablename', 'qtype_algebra'), array('size' => 10)); $grouparray[] = $mform->createElement('text', $this->form_field_name('varmin'), get_string('varmin', 'qtype_algebra'), array('size' => 10)); $grouparray[] = $mform->createElement('text', $this->form_field_name('varmax'), get_string('varmax', 'qtype_algebra'), array('size' => 10)); return $grouparray; } /** * @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); if ($this->questionrec !== null) { $countvars = count($this->questionrec->options->variables); $repeatsatstart = max($countvars + SYMB_QUESTION_NUMVARS_ADD, SYMB_QUESTION_NUMVARS_START); } else { $countvars = 0; $repeatsatstart = SYMB_QUESTION_NUMVARS_START; } $variablefields = array($mform->createElement('group', $this->form_field_name('variables'), get_string('variablex', 'qtype_algebra'), $this->variable_group($mform), null, false)); $repeatedoptions[$this->form_field_name('variable')]['type'] = PARAM_RAW; $repeatedoptions[$this->form_field_name('varmin')]['type'] = PARAM_RAW; $repeatedoptions[$this->form_field_name('varmax')]['type'] = PARAM_RAW; $combinedform->repeat_elements($variablefields, $repeatsatstart, $repeatedoptions, $this->form_field_name('novariables'), $this->form_field_name('addvariables'), SYMB_QUESTION_NUMVARS_ADD, get_string('addmorevariableblanks', 'qtype_algebra'), true); $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()); $variables = array('variable' => array(), 'varmin' => array(), 'varmax' => array()); if ($this->questionrec !== null) { foreach ($this->questionrec->options->answers as $answer) { $answers['answer'][] = $answer->answer; } foreach ($this->questionrec->options->variables as $variable) { $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['.$key.']')] = 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['.$key.']')] = 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['.$key.']')] = 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['.$key.']')] = get_string('notanumber', 'qtype_algebra'); } if ($trimmax == '') { $errors[$this->form_field_name('variables['.$key.']')] = 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['.$key.']')] = get_string('notanumber', 'qtype_algebra'); } // Check that the minimum is less that the maximum! if ((float)$trimmin > (float)$trimmax) { $errors[$this->form_field_name('variables['.$key.']')] = 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|0]')] = 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('variables['.$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(); } }