* @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) { global $CFG; // 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',$CFG->qtype_algebra_method); // 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'; } }