aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJean-Michel Vedrine <vedrine@vedrine.org>2019-04-28 07:52:13 +0200
committerJean-Michel Vedrine <vedrine@vedrine.org>2019-04-28 07:52:13 +0200
commit489433f040eb614117786fe1250777777c3f7a36 (patch)
tree8fa8d7e0321437791328a8a5b5af77cf745a1fbb
parenta916d742b3642527100ac0f0487691bfc54a330e (diff)
First try at making algebra combinable
-rw-r--r--README.md2
-rw-r--r--combinable/combinable.php272
-rw-r--r--combinable/renderer.php31
-rw-r--r--lang/en/qtype_algebra.php1
-rw-r--r--questiontype.php152
5 files changed, 331 insertions, 127 deletions
diff --git a/README.md b/README.md
index 34659de..7f77ea8 100644
--- a/README.md
+++ b/README.md
@@ -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;
}