* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package questionbank
* @subpackage questiontypes
*/
require_once($CFG->dirroot . '/question/type/edit_question_form.php');
require_once($CFG->dirroot . '/question/type/algebra/questiontype.php');
require_once($CFG->dirroot . '/question/type/algebra/parser.php');
// Override the default number of answers and the number to add to avoid clutter.
// Algebra questions will likely not have huge number of different answers...
define("SYMB_QUESTION_NUMANS_START", 2);
define("SYMB_QUESTION_NUMANS_ADD", 1);
// Override the default number of answers and the number to add to avoid clutter.
// algebra questions will likely not have huge number of different answers...
define("SYMB_QUESTION_NUMVAR_START", 2);
define("SYMB_QUESTION_NUMVAR_ADD", 1);
/**
* symoblic editing form definition.
*/
class qtype_algebra_edit_form extends question_edit_form {
/**
* Add question-type specific form fields.
*
* @param MoodleQuickForm $mform the form being built.
*/
protected function definition_inner($mform) {
// Add the select control which will select the comparison type to use
$mform->addElement('select', '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->addHelpButton('compareby', 'compareby', 'qtype_algebra');
$mform->setDefault('compareby','eval');
// Add the control to select the number of checks to perform
// First create an array with all the allowed values. We will then use this array
// with the array_combine function to create a single array where the keys are the
// same as the array values
$chk_array=array( '1', '2', '3', '5', '7',
'10', '20', '30', '50', '70',
'100', '200', '300', '500', '700', '1000');
// Add the select element using the array_combine method discussed above
$mform->addElement('select', 'nchecks', get_string('nchecks','qtype_algebra'),
array_combine($chk_array,$chk_array));
$mform->addHelpButton('nchecks', 'nchecks', 'qtype_algebra');
// Set the default number of checks to perform
$mform->setDefault('nchecks','10');
// Add the box to set the tolerance to use when performing evaluation checks
$mform->addElement('text', 'tolerance', get_string('tolerance','qtype_algebra'));
$mform->addHelpButton('tolerance', 'tolerance', 'qtype_algebra');
$mform->setType('tolerance', PARAM_NUMBER);
$mform->setDefault('tolerance','0.001');
// Add an entry for the answer box prefix
$mform->addElement('text', 'answerprefix', get_string('answerprefix','qtype_algebra'),array('size'=>55));
$mform->addHelpButton('answerprefix', 'answerprefix', 'qtype_algebra');
$mform->setType('answerprefix', PARAM_RAW);
// Add an entry for a disallowed expression
$mform->addElement('text', 'disallow', get_string('disallow','qtype_algebra'),array('size'=>55));
$mform->addHelpButton('disallow', 'disallow', 'qtype_algebra');
$mform->setType('disallow', PARAM_RAW);
// Create an array which will store the function checkboxes
$func_group=array();
// Create an array to add spacers between the boxes
$spacers=array('
');
// Add the initial all functions box to the list of check boxes
$func_group[] =& $mform->createElement('checkbox','all','',get_string('allfunctions','qtype_algebra'));
// Create a checkbox element for each function understood by the parser
for($i=0;$icreateElement('checkbox',$func,'',$func);
if(($i % 6) == 5) {
$spacers[]='
';
} else {
$spacers[]=str_repeat(' ',8-strlen($func));
}
}
// Create and add the group of function controls to the form
$mform->addGroup($func_group,'allowedfuncs',get_string('allowedfuncs','qtype_algebra'),$spacers,true);
$mform->addHelpButton('allowedfuncs', 'allowedfuncs', 'qtype_algebra');
$mform->disabledIf('allowedfuncs','allowedfuncs[all]','checked');
$mform->setDefault('allowedfuncs[all]','checked');
$mform->addElement('static', 'variablesinstruct',
get_string('variables', 'qtype_algebra'),
get_string('filloutonevariable', 'qtype_algebra'));
$mform->closeHeaderBefore('variablesinstruct');
// Create the array for the list of variables used in the question
$repeated=array();
// Create the array for the list of repeated options used by the variable subforms
$repeatedoptions = array();
// Add the form elements to enter the variables
$repeated[] =& $mform->createElement('header','variablehdr',get_string('variableno','qtype_algebra','{no}'));
//$repeatedoptions['variablehdr']['helpbutton'] = array('variable',get_string('variable','qtype_algebra'),
// 'qtype_algebra');
$repeated[] =& $mform->createElement('text','variable',get_string('variablename','qtype_algebra'),array('size'=>20));
$mform->setType('variable', PARAM_RAW);
$repeated[] =& $mform->createElement('text','varmin',get_string('varmin','qtype_algebra'),array('size'=>20));
$mform->setType('varmin', PARAM_RAW);
$repeatedoptions['varmin']['default'] = '';
$repeated[] =& $mform->createElement('text','varmax',get_string('varmax','qtype_algebra'),array('size'=>20));
$mform->setType('varmax', PARAM_RAW);
$repeatedoptions['varmax']['default'] = '';
// Get the current number of variables defined, if any
if (isset($this->question->options)) {
$countvars = count($this->question->options->variables);
} else {
$countvars = 0;
}
// Come up with the number of variable entries to add to the form at the start
if ($this->question->formoptions->repeatelements){
$repeatsatstart = (SYMB_QUESTION_NUMVAR_START > ($countvars + SYMB_QUESTION_NUMVAR_ADD))?
SYMB_QUESTION_NUMVAR_START : ($countvars + SYMB_QUESTION_NUMVAR_ADD);
} else {
$repeatsatstart = $countvars;
}
$this->repeat_elements($repeated, $repeatsatstart, $repeatedoptions, 'novariables', 'addvariables',
SYMB_QUESTION_NUMVAR_ADD, get_string('addmorevariableblanks', 'qtype_algebra'));
$mform->addElement('static', 'answersinstruct',
get_string('correctanswers', 'qtype_algebra'),
get_string('filloutoneanswer', 'qtype_algebra'));
$mform->closeHeaderBefore('answersinstruct');
$this->add_per_answer_fields($mform, get_string('answerno', 'qtype_algebra', '{no}'),
question_bank::fraction_options(), SYMB_QUESTION_NUMANS_START, SYMB_QUESTION_NUMANS_ADD);
$this->add_interactive_settings();
}
protected function data_preprocessing($question) {
$question = parent::data_preprocessing($question);
$question = $this->data_preprocessing_answers($question);
$question = $this->data_preprocessing_hints($question);
if (!empty($question->options)) {
$question->compareby = $question->options->compareby;
$question->nchecks = $question->options->nchecks;
$question->tolerance = $question->options->tolerance;
$question->allowedfuncs = $question->options->allowedfuncs;
$question->disallow = $question->options->disallow;
$question->answerprefix = $question->options->answerprefix;
}
return $question;
}
/**
* Sets the existing values into the form for the question specific data.
*
* This method copies the data from the existing database record into the form fields as default
* values for the various elements.
*
* @param $question the question object from the database being used to fill the form
*/
function set_data($question) {
// Check to see if there are any existing question options, if not then just call
// the base class set data method and exit
if (!isset($question->options)) {
return parent::set_data($question);
}
// Now we do exactly the same for the variables...
$vars = $question->options->variables;
// If we found any variables then loop over them using a numerical key to provide an index
// to the arrays we need to access in the form
if (count($vars)) {
$key = 0;
foreach ($vars as $var) {
// For every variable set the default values
$default_values['variable['.$key.']'] = $var->name;
// Only set the min and max defaults if this variable has a range
if($var->min!='') {
$default_values['varmin['.$key.']'] = $var->min;
$default_values['varmax['.$key.']'] = $var->max;
}
$key++;
}
}
// Add the default values for the allowed functions controls
// First check to see if there are any allowed functions defined
if(count($question->options->allowedfuncs)>0) {
// Clear the 'all functions' flag since functions are restricted
$default_values['allowedfuncs[all]']=0;
// Loop over all the functions which the parser understands
foreach(qtype_algebra_parser::$functions as $func) {
// For each function see if the function is in the allowed function
// list and if so set the check box otherwise remove the check box
if(in_array($func,$question->options->allowedfuncs)) {
$default_values['allowedfuncs['.$func.']']=1;
} else {
$default_values['allowedfuncs['.$func.']']=0;
}
}
}
// There are no allowed functions defined so all functions are allowed
else {
$default_values['allowedfuncs[all]']=1;
}
// Add the default values to the question object in a form which the parent
// set data method will be able to use to find the default values
$question = (object)((array)$question + $default_values);
// Finally call the parent set data method to handle everything else
parent::set_data($question);
}
/**
* Validates the form data ensuring there are no obvious errors in the submitted data.
*
* This method performs some basic sanity checks on the form data before it gets converted
* into a database record.
*
* @param $data the data from the form which needs to be checked
* @param $files some files - I don't know what this is for! - files defined in the form??
*/
public function validation($data, $files) {
// Call the base class validation method and keep any errors it generates
$errors = parent::validation($data, $files);
// 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 = $data['variable'];
// Create an array of defined variables
$varlist=array();
foreach ($vars as $key => $var) {
$trimvar = trim($var);
$trimmin = trim($data['varmin'][$key]);
$trimmax = trim($data['varmax'][$key]);
// Check that there is a valid 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['variable['.$key.']'] = get_string('illegalvarname','qtype_algebra',$trimvar);
}
// Check that this variable has not been defined before
if(in_array($trimvar,$varlist)) {
$errors['variable['.$key.']'] = get_string('duplicatevar','qtype_algebra');
} 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($data['compareby']=='eval') {
// Check that a minimum has been defined
if ($trimmin == '') {
$errors['varmin['.$key.']'] = get_string('novarmin','qtype_algebra');
}
// If there is one check that it is a number
else if(!preg_match($renumber,$trimmin)) {
$errors['varmin['.$key.']'] = get_string('notanumber','qtype_algebra');
}
if ($trimmax == '') {
$errors['varmax['.$key.']'] = get_string('novarmax','qtype_algebra');
}
// If there is one check that it is a number
else if(!preg_match($renumber,$trimmax)) {
$errors['varmax['.$key.']'] = get_string('notanumber','qtype_algebra');
}
// Check that the minimum is less that the maximum!
if ((float)$trimmin > (float)$trimmax) {
$errors['varmin['.$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['variable[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 = $data['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['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++;
// Check to see if the answer has the maximum grade
if ($data['fraction'][$key] == 1) {
$maxgrade = true;
}
}
} catch (Exception $e) {
$errors['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['answer[0]'] = get_string('notenoughanswers', 'quiz', 1);
}
// Check that at least one question has the maximum possible grade
if ($maxgrade == false) {
$errors['fraction[0]'] = get_string('fractionsnomax', 'question');
}
// 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['variable['.$key.']'] = get_string('unusedvar','qtype_algebra');
}
}
}
// Check that the tolerance is greater than or equal to zero
if($data['tolerance']<0) {
$errors['tolerance']=get_string('toleranceltzero','qtype_algebra');
}
return $errors;
}
public function qtype() {
return 'algebra';
}
}