Newer
Older
// This file is part of Stack - http://stack.maths.ed.ac.uk/
// Stack 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.
//
// Stack 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 Stack. If not, see <http://www.gnu.org/licenses/>.
/**
* Question type class for the Stack question type.
*
* @package qtype_stack
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/questionlib.php');
Chris Sangwin
committed
require_once(__DIR__ . '/stack/input/factory.class.php');
require_once(__DIR__ . '/stack/answertest/controller.class.php');
require_once(__DIR__ . '/stack/cas/keyval.class.php');
Matti Harjula
committed
require_once(__DIR__ . '/stack/cas/castext2/castext2_evaluatable.class.php');
Matti Harjula
committed
require_once(__DIR__ . '/stack/cas/castext2/castext2_static_replacer.class.php');
require_once(__DIR__ . '/stack/questiontest.php');
require_once(__DIR__ . '/stack/prt.class.php');
require_once(__DIR__ . '/stack/potentialresponsetreestate.class.php');
require_once(__DIR__ . '/stack/prt.class.php');
require_once(__DIR__ . '/stack/graphlayout/graph.php');
Chris Sangwin
committed
require_once(__DIR__ . '/lang/multilang.php');
require_once(__DIR__ . '/stack/prt.class.php');
/**
* Stack question type class.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_stack extends question_type {
Chris Sangwin
committed
/** @var int array key into the results of get_input_names_from_question_text for the count of input placeholders. */
const INPUTS = 0;
/** @var int array key into the results of get_input_names_from_question_text for the count of validation placeholders. */
const VALIDATIONS = 1;
/** @var int the CAS seed using during validation. */
protected $seed = 1;
/** @var stack_options the CAS options using during validation. */
protected $options;
/**
* @var array prt name => stack_abstract_graph caches the result of
* {@link get_prt_graph()}.
*/
protected $prtgraph = array();
public function save_question($question, $fromform) {
if (!empty($fromform->fixdollars)) {
$this->fix_dollars_in_form_data($fromform);
}
$fromform->penalty = stack_utils::fix_approximate_thirds($fromform->penalty);
Tim Hunt
committed
// If this questions is being derived from another one (either duplicate in any
// Moodle version, or editing making a new version in Moodle 4.0+) then we need
// to get the deployed variants and question tests, so they can be copied too.
// The property used seems to be the reliable way to get the old question id.
if (isset($question->options->questionid) && $question->options->questionid) {
$fromform->deployedseeds = $this->get_question_deployed_seeds($question->options->questionid);
$fromform->testcases = $this->load_question_tests($question->options->questionid);
}
$new = parent::save_question($question, $fromform);
return $new;
}
/**
* Replace any $...$ and $$...$$ delimiters in the question text from the
* form with the recommended delimiters.
* @param object $fromform the data from the form.
*/
protected function fix_dollars_in_form_data($fromform) {
$questionfields = array('questiontext', 'generalfeedback', 'specificfeedback',
'prtcorrect', 'prtpartiallycorrect', 'prtincorrect', 'questiondescription');
foreach ($questionfields as $field) {
$fromform->{$field}['text'] = stack_maths::replace_dollars($fromform->{$field}['text']);
}
$fromform->questionnote = stack_maths::replace_dollars($fromform->questionnote);
Chris Sangwin
committed
$prtnames = array_keys($this->get_prt_names_from_question($fromform->questiontext['text'],
$fromform->specificfeedback['text']));
foreach ($prtnames as $prt) {
foreach ($fromform->{$prt . 'truefeedback'} as &$feedback) {
$feedback['text'] = stack_maths::replace_dollars($feedback['text']);
}
foreach ($fromform->{$prt . 'falsefeedback'} as &$feedback) {
$feedback['text'] = stack_maths::replace_dollars($feedback['text']);
}
}
foreach ($fromform->hint as &$hint) {
$hint['text'] = stack_maths::replace_dollars($hint['text']);
}
}
public function save_question_options($fromform) {
global $DB;
$context = $fromform->context;
parent::save_question_options($fromform);
$options = $DB->get_record('qtype_stack_options', array('questionid' => $fromform->id));
if (!$options) {
$options = new stdClass();
$options->questionid = $fromform->id;
$options->questionvariables = '';
Chris Sangwin
committed
$options->questionnote = '';
$options->specificfeedback = '';
$options->prtcorrect = '';
$options->prtpartiallycorrect = '';
$options->prtincorrect = '';
Chris Sangwin
committed
$options->stackversion = get_config('qtype_stack', 'version');
$options->id = $DB->insert_record('qtype_stack_options', $options);
$options->questionvariables = $fromform->questionvariables;
$options->specificfeedback = $this->import_or_save_files($fromform->specificfeedback,
$context, 'qtype_stack', 'specificfeedback', $fromform->id);
$options->specificfeedbackformat = $fromform->specificfeedback['format'];
$options->questionnote = $fromform->questionnote;
$options->questiondescription = $this->import_or_save_files($fromform->questiondescription,
$context, 'qtype_stack', 'questiondescription', $fromform->id);
$options->questiondescriptionformat = $fromform->questiondescription['format'];
$options->questionsimplify = $fromform->questionsimplify;
$options->assumepositive = $fromform->assumepositive;
$options->assumereal = $fromform->assumereal;
$options->prtcorrect = $this->import_or_save_files($fromform->prtcorrect,
$context, 'qtype_stack', 'prtcorrect', $fromform->id);
$options->prtcorrectformat = $fromform->prtcorrect['format'];
$options->prtpartiallycorrect = $this->import_or_save_files($fromform->prtpartiallycorrect,
$context, 'qtype_stack', 'prtpartiallycorrect', $fromform->id);
$options->prtpartiallycorrectformat = $fromform->prtpartiallycorrect['format'];
$options->prtincorrect = $this->import_or_save_files($fromform->prtincorrect,
$context, 'qtype_stack', 'prtincorrect', $fromform->id);
$options->prtincorrectformat = $fromform->prtincorrect['format'];
$options->multiplicationsign = $fromform->multiplicationsign;
$options->sqrtsign = $fromform->sqrtsign;
$options->complexno = $fromform->complexno;
$options->inversetrig = $fromform->inversetrig;
$options->matrixparens = $fromform->matrixparens;
$options->variantsselectionseed = $fromform->variantsselectionseed;
// We will not have the values for this.
$options->compiledcache = '{}';
$DB->update_record('qtype_stack_options', $options);
Chris Sangwin
committed
$inputnames = array_keys($this->get_input_names_from_question_text_lang($fromform->questiontext));
$inputs = $DB->get_records('qtype_stack_inputs',
array('questionid' => $fromform->id), '', 'name, id, questionid');
$questionhasinputs = false;
foreach ($inputnames as $inputname) {
if (array_key_exists($inputname, $inputs)) {
$input = $inputs[$inputname];
unset($inputs[$inputname]);
} else {
$input = new stdClass();
$input->questionid = $fromform->id;
$input->name = $inputname;
$input->id = $DB->insert_record('qtype_stack_inputs', $input);
}
$input->type = $fromform->{$inputname . 'type'};
$input->tans = $fromform->{$inputname . 'modelans'};
$input->boxsize = $fromform->{$inputname . 'boxsize'};
Chris Sangwin
committed
// TODO: remove this when we remove strictsyntax from the DB.
$input->strictsyntax = true;
$input->insertstars = $fromform->{$inputname . 'insertstars'};
$input->syntaxhint = $fromform->{$inputname . 'syntaxhint'};
$input->syntaxattribute = $fromform->{$inputname . 'syntaxattribute'};
$input->forbidwords = $fromform->{$inputname . 'forbidwords'};
Chris Sangwin
committed
$input->allowwords = $fromform->{$inputname . 'allowwords'};
$input->forbidfloat = $fromform->{$inputname . 'forbidfloat'};
$input->requirelowestterms = $fromform->{$inputname . 'requirelowestterms'};
$input->checkanswertype = $fromform->{$inputname . 'checkanswertype'};
$input->mustverify = $fromform->{$inputname . 'mustverify'};
$input->showvalidation = $fromform->{$inputname . 'showvalidation'};
$input->options = $fromform->{$inputname . 'options'};
$questionhasinputs = true;
$DB->update_record('qtype_stack_inputs', $input);
}
if ($inputs) {
list($test, $params) = $DB->get_in_or_equal(array_keys($inputs));
$params[] = $fromform->id;
$DB->delete_records_select('qtype_stack_inputs',
'name ' . $test . ' AND questionid = ?', $params);
}
if (!$questionhasinputs) {
// A question with no inputs is an information item.
$DB->set_field('question', 'length', 0, array('id' => $fromform->id));
}
Chris Sangwin
committed
$prtnames = array_keys($this->get_prt_names_from_question($fromform->questiontext, $options->specificfeedback));
$prts = $DB->get_records('qtype_stack_prts',
array('questionid' => $fromform->id), '', 'name, id, questionid');
foreach ($prtnames as $prtname) {
if (array_key_exists($prtname, $prts)) {
$prt = $prts[$prtname];
unset($prts[$prtname]);
} else {
$prt = new stdClass();
$prt->questionid = $fromform->id;
$prt->name = $prtname;
$prt->feedbackvariables = '';
$prt->firstnodename = 0;
$prt->id = $DB->insert_record('qtype_stack_prts', $prt);
}
// Find the root node of the PRT.
// Otherwise, if an existing question is being edited, and this is an
// existing PRT, base things on the existing question definition.
$graph = new stack_abstract_graph();
foreach ($fromform->{$prtname . 'answertest'} as $nodename => $notused) {
$description = $fromform->{$prtname . 'description'}[$nodename];
$truenextnode = $fromform->{$prtname . 'truenextnode'}[$nodename];
$falsenextnode = $fromform->{$prtname . 'falsenextnode'}[$nodename];
if ($truenextnode == -1) {
$left = null;
} else {
$left = $truenextnode + 1;
}
if ($falsenextnode == -1) {
$right = null;
} else {
$right = $falsenextnode + 1;
}
$graph->add_prt_node($nodename + 1, $description, $left, $right);
}
$graph->layout();
$roots = $graph->get_roots();
if (count($roots) != 1 || $graph->get_broken_cycles()) {
throw new coding_exception('The PRT ' . $prtname . ' is malformed.');
}
reset($roots);
$firstnode = key($roots) - 1;
$prt->value = $fromform->{$prtname . 'value'};
$prt->autosimplify = $fromform->{$prtname . 'autosimplify'};
$prt->feedbackstyle = $fromform->{$prtname . 'feedbackstyle'};
$prt->feedbackvariables = $fromform->{$prtname . 'feedbackvariables'};
$prt->firstnodename = $firstnode;
$DB->update_record('qtype_stack_prts', $prt);
$nodes = $DB->get_records('qtype_stack_prt_nodes',
array('questionid' => $fromform->id, 'prtname' => $prtname),
'', 'nodename, id, questionid, prtname');
foreach ($fromform->{$prtname . 'answertest'} as $nodename => $notused) {
if (array_key_exists($nodename, $nodes)) {
$node = $nodes[$nodename];
unset($nodes[$nodename]);
} else {
$node = new stdClass();
$node->questionid = $fromform->id;
$node->prtname = $prtname;
$node->nodename = $nodename;
$node->truefeedback = '';
$node->falsefeedback = '';
$node->id = $DB->insert_record('qtype_stack_prt_nodes', $node);
}
$node->description = $fromform->{$prtname . 'description'}[$nodename];
$node->answertest = $fromform->{$prtname . 'answertest'}[$nodename];
$node->sans = $fromform->{$prtname . 'sans'}[$nodename];
$node->tans = $fromform->{$prtname . 'tans'}[$nodename];
// For input types which do not have test options, the input field is hidden
// and therefore null is passed to $node->testoptions, which crashes the form.
// The empty string should be used instead. (Also see issue #974).
$node->testoptions = '';
if (property_exists($fromform, $prtname . 'testoptions')) {
if (array_key_exists($nodename, $fromform->{$prtname . 'testoptions'})) {
$node->testoptions = $fromform->{$prtname . 'testoptions'}[$nodename];
}
$node->quiet = $fromform->{$prtname . 'quiet'}[$nodename];
$node->truescoremode = $fromform->{$prtname . 'truescoremode'}[$nodename];
$node->truescore = $fromform->{$prtname . 'truescore'}[$nodename];
Chris Sangwin
committed
if (property_exists($fromform, $prtname . 'truepenalty')) {
$node->truepenalty = stack_utils::fix_approximate_thirds(
$fromform->{$prtname . 'truepenalty'}[$nodename]);
} else {
// Else we just deleted a PRT.
$node->truepenalty = '';
}
$node->truenextnode = $fromform->{$prtname . 'truenextnode'}[$nodename];
$node->trueanswernote = $fromform->{$prtname . 'trueanswernote'}[$nodename];
$node->truefeedback = $this->import_or_save_files(
$fromform->{$prtname . 'truefeedback'}[$nodename],
$context, 'qtype_stack', 'prtnodetruefeedback', $node->id);
$node->truefeedbackformat = $fromform->{$prtname . 'truefeedback'}[$nodename]['format'];
$node->falsescoremode = $fromform->{$prtname . 'falsescoremode'}[$nodename];
$node->falsescore = $fromform->{$prtname . 'falsescore'}[$nodename];
Chris Sangwin
committed
if (property_exists($fromform, $prtname . 'falsepenalty')) {
$node->falsepenalty = stack_utils::fix_approximate_thirds(
$fromform->{$prtname . 'falsepenalty'}[$nodename]);
} else {
// Else we just deleted a PRT.
$node->falsepenalty = '';
}
$node->falsenextnode = $fromform->{$prtname . 'falsenextnode'}[$nodename];
$node->falseanswernote = $fromform->{$prtname . 'falseanswernote'}[$nodename];
$node->falsefeedback = $this->import_or_save_files(
$fromform->{$prtname . 'falsefeedback'}[$nodename],
$context, 'qtype_stack', 'prtnodefalsefeedback', $node->id);
$node->falsefeedbackformat = $fromform->{$prtname . 'falsefeedback'}[$nodename]['format'];
if ('' === $node->truepenalty) {
$node->truepenalty = null;
}
if ('' === $node->falsepenalty) {
$node->falsepenalty = null;
}
$DB->update_record('qtype_stack_prt_nodes', $node);
}
if ($nodes) {
list($test, $params) = $DB->get_in_or_equal(array_keys($nodes));
$params[] = $fromform->id;
$params[] = $prt->name;
$DB->delete_records_select('qtype_stack_prt_nodes',
'nodename ' . $test . ' AND questionid = ? AND prtname = ?', $params);
}
}
if ($prts) {
list($test, $params) = $DB->get_in_or_equal(array_keys($prts));
$params[] = $fromform->id;
$DB->delete_records_select('qtype_stack_prt_nodes',
'prtname ' . $test . ' AND questionid = ?', $params);
$DB->delete_records_select('qtype_stack_prts',
'name ' . $test . ' AND questionid = ?', $params);
}
$this->save_hints($fromform);
if (isset($fromform->deployedseeds)) {
$DB->delete_records('qtype_stack_deployed_seeds', array('questionid' => $fromform->id));
foreach ($fromform->deployedseeds as $deployedseed) {
$record = new stdClass();
$record->questionid = $fromform->id;
$record->seed = $deployedseed;
$DB->insert_record('qtype_stack_deployed_seeds', $record, false);
}
}
if (isset($fromform->testcases)) {
// If the data includes the definition of the question tests that there
// should be (i.e. when doing import) then replace the existing set
// of tests with the new one.
$this->save_question_tests($fromform->id, $fromform->testcases);
}
// Irrespective of what else has happened, ensure there is no garbage
// in the database, for example if we delete a PRT, remove the expected
// values for that PRT while leaving the rest of the testcases alone.
list($nametest, $params) = $DB->get_in_or_equal($inputnames, SQL_PARAMS_NAMED, 'input', false, null);
$params['questionid'] = $fromform->id;
$DB->delete_records_select('qtype_stack_qtest_inputs',
'questionid = :questionid AND inputname ' . $nametest, $params);
list($nametest, $params) = $DB->get_in_or_equal($prtnames, SQL_PARAMS_NAMED, 'prt', false, null);
$params['questionid'] = $fromform->id;
$DB->delete_records_select('qtype_stack_qtest_expected',
'questionid = :questionid AND prtname ' . $nametest, $params);
public function get_question_options($question) {
global $DB;
parent::get_question_options($question);
$question->options = $DB->get_record('qtype_stack_options',
array('questionid' => $question->id), '*', MUST_EXIST);
$question->inputs = $DB->get_records('qtype_stack_inputs',
array('questionid' => $question->id), 'name',
'name, id, questionid, type, tans, boxsize, strictsyntax, insertstars, ' .
'syntaxhint, syntaxattribute, forbidwords, allowwords, forbidfloat, requirelowestterms, ' .
'checkanswertype, mustverify, showvalidation, options');
$question->prts = $DB->get_records('qtype_stack_prts',
array('questionid' => $question->id), 'name',
'name, id, questionid, value, autosimplify, feedbackstyle, feedbackvariables, firstnodename');
$noders = $DB->get_recordset('qtype_stack_prt_nodes',
foreach ($noders as $node) {
if (!property_exists($question->prts[$node->prtname], 'nodes')) {
$question->prts[$node->prtname]->nodes = [];
}
$question->prts[$node->prtname]->nodes[$node->nodename] = $node;
}
$noders->close();
$question->deployedseeds = $this->get_question_deployed_seeds($question->id);
return true;
}
protected function get_question_deployed_seeds($qid) {
global $DB;
return $DB->get_fieldset_sql('
SELECT seed
FROM {qtype_stack_deployed_seeds}
WHERE questionid = ?
}
protected function initialise_question_instance(question_definition $question, $questiondata) {
parent::initialise_question_instance($question, $questiondata);
Chris Sangwin
committed
$question->stackversion = $questiondata->options->stackversion;
$question->questionvariables = $questiondata->options->questionvariables;
$question->questionnote = $questiondata->options->questionnote;
$question->questiondescription = $questiondata->options->questiondescription;
$question->questiondescriptionformat = $questiondata->options->questiondescriptionformat;
$question->specificfeedback = $questiondata->options->specificfeedback;
$question->specificfeedbackformat = $questiondata->options->specificfeedbackformat;
$question->prtcorrect = $questiondata->options->prtcorrect;
$question->prtcorrectformat = $questiondata->options->prtcorrectformat;
$question->prtpartiallycorrect = $questiondata->options->prtpartiallycorrect;
$question->prtpartiallycorrectformat = $questiondata->options->prtpartiallycorrectformat;
$question->prtincorrect = $questiondata->options->prtincorrect;
$question->prtincorrectformat = $questiondata->options->prtincorrectformat;
$question->variantsselectionseed = $questiondata->options->variantsselectionseed;
$question->compiledcache = $questiondata->options->compiledcache;
// Parse the cache in advance.
if (is_string($question->compiledcache)) {
$question->compiledcache = json_decode($question->compiledcache, true);
} else if ($question->compiledcache === null) {
// If someone has done nulling through the database.
$question->compiledcache = [];
}
$question->options = new stack_options();
$question->options->set_option('decimals', $questiondata->options->decimals);
$question->options->set_option('multiplicationsign', $questiondata->options->multiplicationsign);
$question->options->set_option('complexno', $questiondata->options->complexno);
$question->options->set_option('inversetrig', $questiondata->options->inversetrig);
$question->options->set_option('logicsymbol', $questiondata->options->logicsymbol);
$question->options->set_option('matrixparens', $questiondata->options->matrixparens);
$question->options->set_option('sqrtsign', (bool) $questiondata->options->sqrtsign);
$question->options->set_option('simplify', (bool) $questiondata->options->questionsimplify);
$question->options->set_option('assumepos', (bool) $questiondata->options->assumepositive);
$question->options->set_option('assumereal', (bool) $questiondata->options->assumereal);
$requiredparams = stack_input_factory::get_parameters_used();
Huong Nguyen
committed
foreach (stack_utils::extract_placeholders($question->questiontext, 'input') as $name) {
$inputdata = $questiondata->inputs[$name];
Chris Sangwin
committed
'strictSyntax' => true,
'insertStars' => (int) $inputdata->insertstars,
'syntaxHint' => $inputdata->syntaxhint,
'syntaxAttribute' => $inputdata->syntaxattribute,
'forbidWords' => $inputdata->forbidwords,
'allowWords' => $inputdata->allowwords,
'forbidFloats' => (bool) $inputdata->forbidfloat,
'lowestTerms' => (bool) $inputdata->requirelowestterms,
'sameType' => (bool) $inputdata->checkanswertype,
'mustVerify' => (bool) $inputdata->mustverify,
'showValidation' => $inputdata->showvalidation,
'options' => $inputdata->options,
$parameters = array();
foreach ($requiredparams[$inputdata->type] as $paramname) {
if ($paramname == 'inputType') {
continue;
}
$parameters[$paramname] = $allparameters[$paramname];
}
$question->inputs[$name] = stack_input_factory::make(
$inputdata->type, $inputdata->name, $inputdata->tans, $question->options, $parameters);
$prtnames = array_keys($this->get_prt_names_from_question($question->questiontext, $question->specificfeedback));
// If not then we have just created the PRT.
if (array_key_exists($name, $questiondata->prts)) {
$prtdata = $questiondata->prts[$name];
// At this point we do not have the PRT method is_formative() available to us.
if ($prtdata->feedbackstyle > 0) {
$totalvalue += $prtdata->value;
$allformative = false;
}
}
if ($questiondata->prts && !$allformative && $totalvalue < 0.0000001) {
throw new coding_exception('There is an error authoring your question. ' .
'The $totalvalue, the marks available for the question, must be positive in question ' .
$question->name);
Chris Sangwin
committed
foreach ($prtnames as $name) {
if (array_key_exists($name, $questiondata->prts)) {
$prtvalue = 0;
if (!$allformative) {
$prtvalue = $questiondata->prts[$name]->value / $totalvalue;
}
$question->prts[$name] = new stack_potentialresponse_tree_lite($questiondata->prts[$name],
$prtvalue, $question);
} // If not we just added a PRT.
$question->deployedseeds = array_values($questiondata->deployedseeds);
}
/**
* Get the URL params required for linking to associated scripts like
* questiontestrun.php.
*
* @param stdClass|qtype_stack_question $question question data, as from question_bank::load_question
* or question_bank::load_question_data.
* @return array of URL params. Can be passed to moodle_url.
*/
protected function get_question_url_params($question) {
$urlparams = array('questionid' => $question->id);
if (property_exists($question, 'seed')) {
$urlparams['seed'] = $question->seed;
}
// This is a bit of a hack to find the right thing to put in the URL.
// If we are already on a URL that gives us a clue what to do, use that.
$context = context::instance_by_id($question->contextid);
if ($cmid = optional_param('cmid', null, PARAM_INT)) {
$urlparams['cmid'] = $cmid;
} else if ($courseid = optional_param('courseid', null, PARAM_INT)) {
$urlparams['courseid'] = $courseid;
} else if ($context->contextlevel == CONTEXT_MODULE) {
$urlparams['cmid'] = $context->instanceid;
} else if ($context->contextlevel == CONTEXT_COURSE) {
$urlparams['courseid'] = $context->instanceid;
} else {
$urlparams['courseid'] = get_site()->id;
}
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
return $urlparams;
}
/**
* Get the URL for questiontestrun.php for a question.
*
* @param stdClass|qtype_stack_question $question question data, as from question_bank::load_question
* or question_bank::load_question_data.
* @return moodle_url the URL.
*/
public function get_question_test_url($question) {
$linkparams = $this->get_question_url_params($question);
return new moodle_url('/question/type/stack/questiontestrun.php', $linkparams);
}
/**
* Get the URL for tidyquestion.php for a question.
*
* @param stdClass|qtype_stack_question $question question data, as from question_bank::load_question
* or question_bank::load_question_data.
* @return moodle_url the URL.
*/
public function get_tidy_question_url($question) {
$linkparams = $this->get_question_url_params($question);
return new moodle_url('/question/type/stack/tidyquestion.php', $linkparams);
}
public function get_extra_question_bank_actions(stdClass $question): array {
$actions = parent::get_extra_question_bank_actions($question);
$linkparams = $this->get_question_url_params($question);
// Directly link to question tests and deployed variants.
if (question_has_capability_on($question, 'view')) {
$actions[] = new \action_menu_link_secondary(
new moodle_url('/question/type/stack/questiontestrun.php', $linkparams),
new \pix_icon('t/approve', ''),
get_string('runquestiontests', 'qtype_stack'));
}
// Directly link to tidy question script.
if (question_has_capability_on($question, 'view')) {
$actions[] = new \action_menu_link_secondary(
new moodle_url('/question/type/stack/tidyquestion.php', $linkparams),
new \pix_icon('t/edit', ''),
get_string('tidyquestion', 'qtype_stack'));
}
return $actions;
}
public function delete_question($questionid, $contextid) {
global $DB;
$DB->delete_records('qtype_stack_deployed_seeds', array('questionid' => $questionid));
$DB->delete_records('qtype_stack_prt_nodes', array('questionid' => $questionid));
$DB->delete_records('qtype_stack_prts', array('questionid' => $questionid));
$DB->delete_records('qtype_stack_inputs', array('questionid' => $questionid));
$DB->delete_records('qtype_stack_options', array('questionid' => $questionid));
parent::delete_question($questionid, $contextid);
}
public function move_files($questionid, $oldcontextid, $newcontextid) {
global $DB;
$fs = get_file_storage();
parent::move_files($questionid, $oldcontextid, $newcontextid);
$this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
$fs->move_area_files_to_new_context($oldcontextid, $newcontextid,
'qtype_stack', 'specificfeedback', $questionid);
$fs->move_area_files_to_new_context($oldcontextid, $newcontextid,
'qtype_stack', 'questiondescription', $questionid);
$fs->move_area_files_to_new_context($oldcontextid, $newcontextid,
'qtype_stack', 'prtcorrect', $questionid);
$fs->move_area_files_to_new_context($oldcontextid, $newcontextid,
'qtype_stack', 'prtpartiallycorrect', $questionid);
$fs->move_area_files_to_new_context($oldcontextid, $newcontextid,
'qtype_stack', 'prtincorrect', $questionid);
$nodeids = $DB->get_records_menu('qtype_stack_prt_nodes', array('questionid' => $questionid), 'id', 'id,1');
foreach ($nodeids as $nodeid => $notused) {
$fs->move_area_files_to_new_context($oldcontextid, $newcontextid,
'qtype_stack', 'prtnodetruefeedback', $nodeid);
$fs->move_area_files_to_new_context($oldcontextid, $newcontextid,
'qtype_stack', 'prtnodefalsefeedback', $nodeid);
}
}
protected function delete_files($questionid, $contextid) {
global $DB;
$fs = get_file_storage();
parent::delete_files($questionid, $contextid);
$this->delete_files_in_hints($questionid, $contextid);
$fs->delete_area_files($contextid, 'qtype_stack', 'specificfeedback', $questionid);
$fs->delete_area_files($contextid, 'qtype_stack', 'questiondescription', $questionid);
$fs->delete_area_files($contextid, 'qtype_stack', 'prtcorrect', $questionid);
$fs->delete_area_files($contextid, 'qtype_stack', 'prtpartiallycorrect', $questionid);
$fs->delete_area_files($contextid, 'qtype_stack', 'prtincorrect', $questionid);
$nodeids = $DB->get_records_menu('qtype_stack_prt_nodes', array('questionid' => $questionid), 'id', 'id,1');
foreach ($nodeids as $nodeid => $notused) {
$fs->delete_area_files($oldcontextid, $newcontextid,
'qtype_stack', 'prtnodetruefeedback', $nodeid);
$fs->delete_area_files($oldcontextid, $newcontextid,
'qtype_stack', 'prtnodefalsefeedback', $nodeid);
}
}
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
/**
* Save a set of question tests for a question, replacing any existing tests.
* @param int $questionid the question id of the question we are manipulating the tests for.
* @param array $testcases testcase number => stack_question_test
*/
public function save_question_tests($questionid, $testcases) {
global $DB;
$transaction = $DB->start_delegated_transaction();
$this->delete_question_tests($questionid);
foreach ($testcases as $number => $testcase) {
$this->save_question_test($questionid, $testcase, $number);
}
$transaction->allow_commit();
}
/**
* Save a question tests for a question, either replacing the test at a given
* number, or adding a new test, either with a given number, or taking the
* first unused number.
* @param int $questionid the question id of the question we are manipulating the tests for.
* @param stack_question_test $qtest
* @param int $testcases testcase number to replace/add. If not given, the first unused number is found.
*/
public function save_question_test($questionid, stack_question_test $qtest, $testcase = null) {
global $DB;
$transaction = $DB->start_delegated_transaction();
// Limit the length of descriptions.
$description = substr($qtest->description, 0, 255);
if (!$testcase || !$DB->record_exists('qtype_stack_qtests',
array('questionid' => $questionid, 'testcase' => $testcase))) {
// Find the first unused testcase number.
$testcase = $DB->get_field_sql('
SELECT MIN(qt.testcase) + 1
FROM (
SELECT testcase FROM {qtype_stack_qtests} WHERE questionid = ?
UNION
SELECT 0
) qt
LEFT JOIN {qtype_stack_qtests} qt2 ON qt2.questionid = ? AND
qt2.testcase = qt.testcase + 1
WHERE qt2.id IS NULL
', array($questionid, $questionid));
$testcasedata = new stdClass();
$testcasedata->questionid = $questionid;
$testcasedata->testcase = $testcase;
$testcasedata->description = $description;
$testcasedata->timemodified = time();
$DB->insert_record('qtype_stack_qtests', $testcasedata);
} else {
$DB->set_field('qtype_stack_qtests', 'timemodified', time(),
array('questionid' => $questionid, 'testcase' => $testcase));
$DB->set_field('qtype_stack_qtests', 'description', $description,
array('questionid' => $questionid, 'testcase' => $testcase));
}
// Save the input data.
$DB->delete_records('qtype_stack_qtest_inputs', array('questionid' => $questionid, 'testcase' => $testcase));
foreach ($qtest->inputs as $name => $value) {
$testinput = new stdClass();
$testinput->questionid = $questionid;
$testinput->testcase = $testcase;
$testinput->inputname = $name;
$testinput->value = $value;
$DB->insert_record('qtype_stack_qtest_inputs', $testinput);
}
// Save the expected outcome data.
$DB->delete_records('qtype_stack_qtest_expected', array('questionid' => $questionid, 'testcase' => $testcase));
foreach ($qtest->expectedresults as $prtname => $expectedresults) {
$expected = new stdClass();
$expected->questionid = $questionid;
$expected->testcase = $testcase;
$expected->prtname = $prtname;
if ($expectedresults->score === '' || $expectedresults->score === null) {
Chris Sangwin
committed
$expected->expectedscore = null;
Chris Sangwin
committed
$expected->expectedscore = (float) $expectedresults->score;
}
if ($expectedresults->penalty === '' || $expectedresults->penalty === null) {
Chris Sangwin
committed
$expected->expectedpenalty = null;
$expected->expectedpenalty = stack_utils::fix_approximate_thirds(
(float) $expectedresults->penalty);
$expected->expectedanswernote = $expectedresults->answernotes[0];
$DB->insert_record('qtype_stack_qtest_expected', $expected);
}
$transaction->allow_commit();
}
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
/**
* Deploy a variant of a question.
* @param int $questionid the question id.
* @param int $seed the seed to deploy.
*/
public function deploy_variant($questionid, $seed) {
global $DB;
$record = new stdClass();
$record->questionid = $questionid;
$record->seed = $seed;
$DB->insert_record('qtype_stack_deployed_seeds', $record);
$this->notify_question_edited($questionid);
}
/**
* Un-deploy a variant of a question.
* @param int $questionid the question id.
* @param int $seed the seed to un-deploy.
*/
public function undeploy_variant($questionid, $seed) {
global $DB;
$DB->delete_records('qtype_stack_deployed_seeds',
array('questionid' => $questionid, 'seed' => $seed));
$this->notify_question_edited($questionid);
}
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
/**
* Rename an input in the question data. It is the caller's responsibility
* to ensure that the $to name will not violate any unique constraints.
* @param int $questionid the question id.
* @param string $from the input to rename.
* @param string $to the new name to give it.
*/
public function rename_input($questionid, $from, $to) {
global $DB;
$transaction = $DB->start_delegated_transaction();
// Place-holders in the question text.
$questiontext = $DB->get_field('question', 'questiontext', array('id' => $questionid));
$questiontext = str_replace(array("[[input:{$from}]]", "[[validation:{$from}]]"),
array("[[input:{$to}]]", "[[validation:{$to}]]"), $questiontext);
$DB->set_field('question', 'questiontext', $questiontext, array('id' => $questionid));
// Input names in question test data.
$DB->set_field('qtype_stack_qtest_inputs', 'inputname', $to,
array('questionid' => $questionid, 'inputname' => $from));
// The input itself.
$DB->set_field('qtype_stack_inputs', 'name', $to,
array('questionid' => $questionid, 'name' => $from));
$regex = '~\b' . preg_quote($from, '~') . '\b~';
// Where the input name appears in expressions in PRTs.
$prts = $DB->get_records('qtype_stack_prts', array('questionid' => $questionid),
'id, feedbackvariables');
foreach ($prts as $prt) {
$prt->feedbackvariables = preg_replace($regex, $to, $prt->feedbackvariables, -1, $changes);
if ($changes) {
$DB->update_record('qtype_stack_prts', $prt);
}
}
// Where the input name appears in expressions in PRT node.
$nodes = $DB->get_records('qtype_stack_prt_nodes', array('questionid' => $questionid),
'id, sans, tans, testoptions, truefeedback, falsefeedback');
foreach ($nodes as $node) {
$changes = false;
$node->sans = preg_replace($regex, $to, $node->sans, -1, $count);
$changes = $changes || $count;
$node->tans = preg_replace($regex, $to, $node->tans, -1, $count);
$changes = $changes || $count;
$node->testoptions = preg_replace($regex, $to, $node->testoptions, -1, $count);
$changes = $changes || $count;
$node->truefeedback = preg_replace($regex, $to, $node->truefeedback, -1, $count);
$changes = $changes || $count;
$node->falsefeedback = preg_replace($regex, $to, $node->falsefeedback, -1, $count);
$changes = $changes || $count;
if ($changes) {
$DB->update_record('qtype_stack_prt_nodes', $node);
}
}
// If someone plays with input names we need to clear compiledcache.
$sql = 'UPDATE {qtype_stack_options} SET compiledcache = ? WHERE questionid = ?';
$params[] = '{}';
$params[] = $questionid;
$DB->execute($sql, $params);
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
$transaction->allow_commit();
$this->notify_question_edited($questionid);
}
/**
* Rename a PRT in the question data. It is the caller's responsibility
* to ensure that the $to name will not violate any unique constraints.
* @param int $questionid the question id.
* @param string $from the PRT to rename.
* @param string $to the new name to give it.
*/
public function rename_prt($questionid, $from, $to) {
global $DB;
$transaction = $DB->start_delegated_transaction();
// Place-holders in the question text.
$questiontext = $DB->get_field('question', 'questiontext', array('id' => $questionid));
$questiontext = str_replace("[[feedback:{$from}]]", "[[feedback:{$to}]]", $questiontext);
$DB->set_field('question', 'questiontext', $questiontext, array('id' => $questionid));
// Place-holders in the specific feedback.
$specificfeedback = $DB->get_field('qtype_stack_options', 'specificfeedback',
array('questionid' => $questionid));
$specificfeedback = str_replace("[[feedback:{$from}]]", "[[feedback:{$to}]]", $specificfeedback);
$DB->set_field('qtype_stack_options', 'specificfeedback', $specificfeedback,
array('questionid' => $questionid));
// PRT names in question test data.
$DB->set_field('qtype_stack_qtest_expected', 'prtname', $to,
array('questionid' => $questionid, 'prtname' => $from));
// The PRT name in its nodes.
$DB->set_field('qtype_stack_prt_nodes', 'prtname', $to,
array('questionid' => $questionid, 'prtname' => $from));
// The PRT itself.
$DB->set_field('qtype_stack_prts', 'name', $to,
array('questionid' => $questionid, 'name' => $from));
// If someone plays with PRT names we need to clear compiledcache.
$sql = 'UPDATE {qtype_stack_options} SET compiledcache = ? WHERE questionid = ?';
$params[] = '{}';
$params[] = $questionid;
$DB->execute($sql, $params);
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
$transaction->allow_commit();
$this->notify_question_edited($questionid);
}
/**
* Rename a PRT node in the question data. It is the caller's responsibility
* to ensure that the $to name will not violate any unique constraints.
* @param int $questionid the question id.
* @param string $prtname the PRT that the node belongs to.
* @param string $from the input to rename.
* @param string $to the new name to give it.
*/
public function rename_prt_node($questionid, $prtname, $from, $to) {
global $DB;
$transaction = $DB->start_delegated_transaction();
// The PRT node itself.
$DB->set_field('qtype_stack_prt_nodes', 'nodename', $to,
array('questionid' => $questionid, 'prtname' => $prtname, 'nodename' => $from));
// True next node links.
$DB->set_field('qtype_stack_prt_nodes', 'truenextnode', $to,
array('questionid' => $questionid, 'prtname' => $prtname, 'truenextnode' => $from));
// False next node links.
$DB->set_field('qtype_stack_prt_nodes', 'falsenextnode', $to,
array('questionid' => $questionid, 'prtname' => $prtname, 'falsenextnode' => $from));
// PRT first node link.
$DB->set_field('qtype_stack_prts', 'firstnodename', $to,
array('questionid' => $questionid, 'name' => $prtname, 'firstnodename' => $from));
// If someone plays with PRT node names we need to clear compiledcache.
$sql = 'UPDATE {qtype_stack_options} SET compiledcache = ? WHERE questionid = ?';
$params[] = '{}';
$params[] = $questionid;
$DB->execute($sql, $params);
$transaction->allow_commit();
$this->notify_question_edited($questionid);
}
/**
* From Moodle 2.4 onwards, we need to clear the entry from the question
* cache if a question definition changes. This method deals with doing
* that without causing errors on earlier versions of Moodle.
* @param int $questionid the question id to clear from the cache.
*/
protected function notify_question_edited($questionid) {
if (method_exists('question_bank', 'notify_question_edited')) {
call_user_func(array('question_bank', 'notify_question_edited'), $questionid);
}
}
/**
* Load all the question tests for a question.
* @param int $questionid the id of the question to load the tests for.
* @return array testcase number => stack_question_test
*/
public function load_question_tests($questionid) {
global $DB;
$testinputdata = $DB->get_records('qtype_stack_qtest_inputs',
array('questionid' => $questionid), 'testcase, inputname');
$testinputs = array();
foreach ($testinputdata as $data) {
$testinputs[$data->testcase][$data->inputname] = $data->value;
}
$testcasenumbers = $DB->get_records_menu('qtype_stack_qtests',
array('questionid' => $questionid), 'testcase', 'testcase, description');
foreach ($testcasenumbers as $number => $description) {
if (!array_key_exists($number, $testinputs)) {
$testinputs[$number] = array();
Chris Sangwin
committed
}
$testcase = new stack_question_test($description, $testinputs[$number], $number);
$testcases[$number] = $testcase;
}
$expecteddata = $DB->get_records('qtype_stack_qtest_expected',
array('questionid' => $questionid), 'testcase, prtname');
foreach ($expecteddata as $data) {
$testcases[$data->testcase]->add_expected_result($data->prtname,
new stack_potentialresponse_tree_state(1, true,
$data->expectedscore, $data->expectedpenalty,
'', array($data->expectedanswernote)));
}
return $testcases;
}
/**
* Load one particular question tests for a question.
* @param int $questionid the id of the question to load the tests for.
* @param int $testcase the testcase nubmer to load.
* @return stack_question_test the test-case
*/
public function load_question_test($questionid, $testcase) {
global $DB;
// Verify that this testcase exists.
$test = $DB->get_record('qtype_stack_qtests',
array('questionid' => $questionid, 'testcase' => $testcase), '*', MUST_EXIST);
// Load the inputs.
$inputs = $DB->get_records_menu('qtype_stack_qtest_inputs',
array('questionid' => $questionid, 'testcase' => $testcase),
'inputname', 'inputname, value');
$qtest = new stack_question_test($test->description, $inputs, $testcase);
// Load the expectations.
$expectations = $DB->get_records('qtype_stack_qtest_expected',
array('questionid' => $questionid, 'testcase' => $testcase), 'prtname',
'prtname, expectedscore, expectedpenalty, expectedanswernote');
foreach ($expectations as $prtname => $expected) {
$qtest->add_expected_result($prtname, new stack_potentialresponse_tree_state(
1, true, $expected->expectedscore, $expected->expectedpenalty,
'', array($expected->expectedanswernote)));
}
return $qtest;
}
/**
* Delete all the question tests for a question.
* @param int $questionid the id of the question to load the tests for.
*/
protected function delete_question_tests($questionid) {
global $DB;
$transaction = $DB->start_delegated_transaction();
$DB->delete_records('qtype_stack_qtest_expected', array('questionid' => $questionid));
$DB->delete_records('qtype_stack_qtest_inputs', array('questionid' => $questionid));
$DB->delete_records('qtype_stack_qtests', array('questionid' => $questionid));
$DB->delete_records('qtype_stack_qtest_results', array('questionid' => $questionid));
$transaction->allow_commit();
}
/**
* Delete one particular question test for a question.
* @param int $questionid the id of the question to load the tests for.
* @param int $testcase the testcase nubmer to load.
*/
public function delete_question_test($questionid, $testcase) {
global $DB;
$transaction = $DB->start_delegated_transaction();
$DB->delete_records('qtype_stack_qtest_expected',
array('questionid' => $questionid, 'testcase' => $testcase));
$DB->delete_records('qtype_stack_qtest_inputs',
array('questionid' => $questionid, 'testcase' => $testcase));
$DB->delete_records('qtype_stack_qtests',
array('questionid' => $questionid, 'testcase' => $testcase));
$DB->delete_records('qtype_stack_qtest_results',
array('questionid' => $questionid, 'testcase' => $testcase));
public function get_possible_responses($questiondata) {
$parts = array();
$q = $this->make_question($questiondata);
foreach ($q->prts as $index => $prt) {
foreach ($prt->get_nodes_summary() as $nodeid => $choices) {
// STACK allows variables in scores, which may not be evaluated.
if (is_numeric($choices->falsescore) && is_numeric($choices->truescore)) {
$parts[$index . '-' . $nodeid] = array(
$choices->falseanswernote => new question_possible_response(
$choices->falseanswernote, $choices->falsescore * $prt->get_value()),
$choices->trueanswernote => new question_possible_response(
$choices->trueanswernote, $choices->truescore * $prt->get_value()),
null => question_possible_response::no_response(),
);
}
}
}
return $parts;
}
/**
* Helper method used by {@link export_to_xml()}.
* @param qformat_xml $format the importer/exporter object.
* @param string $tag the XML tag to use.
* @param string $text the text to output.
* @param int $textformat the text's format.
* @param int $itemid the itemid for any files.
* @param int $contextid the context id that the text belongs to.
* @param string $indent the amount of indent to add at the start of the line.
* @return string XML fragment.
*/
protected function export_xml_text(qformat_xml $format, $tag, $text, $textformat,
$contextid, $filearea, $itemid, $indent = ' ') {
$files = $fs->get_area_files($contextid, 'qtype_stack', $filearea, $itemid);
$output = '';
$output .= $indent . "<{$tag} {$format->format($textformat)}>\n";
$output .= $indent . ' ' . $format->writetext($text);
$output .= $format->write_files($files);
$output .= $indent . "</{$tag}>\n";
return $output;
}
public function export_to_xml($questiondata, qformat_xml $format, $notused = null) {
$contextid = $questiondata->contextid;
// The method get_question_options does not load the testcases, because
// they are not normally needed, so we have to load them manually here.
// However, we only do it conditionally, so that the unit tests can
// just pass the data in.
$questiondata->testcases = $this->load_question_tests($questiondata->id);
}
$output = '';
$options = $questiondata->options;
Chris Sangwin
committed
$output .= " <stackversion>\n";
$output .= " " . $format->writetext($options->stackversion, 0);
$output .= " </stackversion>\n";
$output .= " <questionvariables>\n";
$output .= " " . $format->writetext($options->questionvariables, 0);
$output .= " </questionvariables>\n";
$output .= $this->export_xml_text($format, 'specificfeedback', $options->specificfeedback,
$options->specificfeedbackformat, $contextid, 'specificfeedback', $questiondata->id);
$output .= " <questionnote>\n";
$output .= " " . $format->writetext($options->questionnote, 0);
$output .= " </questionnote>\n";
$output .= $this->export_xml_text($format, 'questiondescription', $options->questiondescription,
$options->questiondescriptionformat, $contextid, 'questiondescription', $questiondata->id);
$output .= " <questionsimplify>{$options->questionsimplify}</questionsimplify>\n";
$output .= " <assumepositive>{$options->assumepositive}</assumepositive>\n";
$output .= " <assumereal>{$options->assumereal}</assumereal>\n";
$output .= $this->export_xml_text($format, 'prtcorrect', $options->prtcorrect,
$options->prtcorrectformat, $contextid, 'prtcorrect', $questiondata->id);
$output .= $this->export_xml_text($format, 'prtpartiallycorrect', $options->prtpartiallycorrect,
$options->prtpartiallycorrectformat, $contextid, 'prtpartiallycorrect', $questiondata->id);
$output .= $this->export_xml_text($format, 'prtincorrect', $options->prtincorrect,
$options->prtincorrectformat, $contextid, 'prtincorrect', $questiondata->id);
$output .= " <decimals>{$options->decimals}</decimals>\n";
$output .= " <multiplicationsign>{$options->multiplicationsign}</multiplicationsign>\n";
$output .= " <sqrtsign>{$options->sqrtsign}</sqrtsign>\n";
$output .= " <complexno>{$options->complexno}</complexno>\n";
$output .= " <inversetrig>{$options->inversetrig}</inversetrig>\n";
$output .= " <logicsymbol>{$options->logicsymbol}</logicsymbol>\n";
$output .= " <matrixparens>{$options->matrixparens}</matrixparens>\n";
$output .= " <variantsselectionseed>{$format->xml_escape($options->variantsselectionseed)}</variantsselectionseed>\n";
foreach ($questiondata->inputs as $input) {
$output .= " <input>\n";
$output .= " <name>{$input->name}</name>\n";
$output .= " <type>{$input->type}</type>\n";
$output .= " <tans>{$format->xml_escape($input->tans)}</tans>\n";
$output .= " <boxsize>{$input->boxsize}</boxsize>\n";
$output .= " <strictsyntax>{$input->strictsyntax}</strictsyntax>\n";
$output .= " <insertstars>{$input->insertstars}</insertstars>\n";
$output .= " <syntaxhint>{$format->xml_escape($input->syntaxhint)}</syntaxhint>\n";
$output .= " <syntaxattribute>{$format->xml_escape($input->syntaxattribute)}</syntaxattribute>\n";
$output .= " <forbidwords>{$format->xml_escape($input->forbidwords)}</forbidwords>\n";
Chris Sangwin
committed
$output .= " <allowwords>{$format->xml_escape($input->allowwords)}</allowwords>\n";
$output .= " <forbidfloat>{$input->forbidfloat}</forbidfloat>\n";
$output .= " <requirelowestterms>{$input->requirelowestterms}</requirelowestterms>\n";
$output .= " <checkanswertype>{$input->checkanswertype}</checkanswertype>\n";
$output .= " <mustverify>{$input->mustverify}</mustverify>\n";
$output .= " <showvalidation>{$input->showvalidation}</showvalidation>\n";
$output .= " <options>{$input->options}</options>\n";
$output .= " </input>\n";
}
foreach ($questiondata->prts as $prt) {
$output .= " <prt>\n";
$output .= " <name>{$prt->name}</name>\n";
$output .= " <value>{$prt->value}</value>\n";
$output .= " <autosimplify>{$prt->autosimplify}</autosimplify>\n";
$output .= " <feedbackstyle>{$prt->feedbackstyle}</feedbackstyle>\n";
$output .= " <feedbackvariables>\n";
$output .= " " . $format->writetext($prt->feedbackvariables, 0);
$output .= " </feedbackvariables>\n";
foreach ($prt->nodes as $node) {
$output .= " <node>\n";
$output .= " <description>{$format->xml_escape($node->description)}</description>\n";
$output .= " <answertest>{$node->answertest}</answertest>\n";
$output .= " <sans>{$format->xml_escape($node->sans)}</sans>\n";
$output .= " <tans>{$format->xml_escape($node->tans)}</tans>\n";
$output .= " <testoptions>{$format->xml_escape($node->testoptions)}</testoptions>\n";
$output .= " <quiet>{$node->quiet}</quiet>\n";
$output .= " <truescoremode>{$node->truescoremode}</truescoremode>\n";
$output .= " <truescore>{$format->xml_escape($node->truescore)}</truescore>\n";
$output .= " <truepenalty>{$format->xml_escape($node->truepenalty)}</truepenalty>\n";
$output .= " <truenextnode>{$node->truenextnode}</truenextnode>\n";
$output .= " <trueanswernote>{$format->xml_escape($node->trueanswernote)}</trueanswernote>\n";
$output .= $this->export_xml_text($format, 'truefeedback', $node->truefeedback, $node->truefeedbackformat,
$contextid, 'prtnodetruefeedback', $node->id, ' ');
$output .= " <falsescoremode>{$node->falsescoremode}</falsescoremode>\n";
$output .= " <falsescore>{$format->xml_escape($node->falsescore)}</falsescore>\n";
$output .= " <falsepenalty>{$format->xml_escape($node->falsepenalty)}</falsepenalty>\n";
$output .= " <falsenextnode>{$node->falsenextnode}</falsenextnode>\n";
$output .= " <falseanswernote>{$format->xml_escape($node->falseanswernote)}</falseanswernote>\n";
$output .= $this->export_xml_text($format, 'falsefeedback', $node->falsefeedback, $node->falsefeedbackformat,
$contextid, 'prtnodefalsefeedback', $node->id, ' ');
$output .= " </node>\n";
}
$output .= " </prt>\n";
}
foreach ($questiondata->deployedseeds as $deployedseed) {
$output .= " <deployedseed>{$deployedseed}</deployedseed>\n";
}
foreach ($questiondata->testcases as $testcase => $qtest) {
$output .= " <qtest>\n";
$output .= " <testcase>{$testcase}</testcase>\n";
$description = $format->xml_escape($qtest->description);
$output .= " <description>{$description}</description>\n";
foreach ($qtest->inputs as $name => $value) {
$output .= " <testinput>\n";
$output .= " <name>{$name}</name>\n";
$output .= " <value>{$format->xml_escape($value)}</value>\n";
$output .= " </testinput>\n";
}
foreach ($qtest->expectedresults as $name => $expected) {
$output .= " <expected>\n";
$output .= " <name>{$name}</name>\n";
$output .= " <expectedscore>{$format->xml_escape($expected->score)}</expectedscore>\n";
$output .= " <expectedpenalty>{$format->xml_escape($expected->penalty)}</expectedpenalty>\n";
$output .= " <expectedanswernote>{$format->xml_escape($expected->answernotes[0])}</expectedanswernote>\n";
$output .= " </expected>\n";
}
$output .= " </qtest>\n";
}
return $output;
}
public function import_from_xml($xml, $fromform, qformat_xml $format, $notused = null) {
if (!isset($xml['@']['type']) || $xml['@']['type'] != $this->name()) {
return false;
}
$fromform = $format->import_headers($xml);
$fromform->qtype = $this->name();
$fromform->stackversion = $format->getpath($xml, array('#', 'stackversion', 0, '#', 'text', 0, '#'), '', true);
$fromform->questionvariables = $format->getpath($xml, array('#', 'questionvariables',
0, '#', 'text', 0, '#'), '', true);
$fformat = $fromform->questiontextformat;
if (isset($fromform->specificfeedbackformat)) {
$fformat = $fromform->specificfeedbackformat;
}
$fromform->specificfeedback = $this->import_xml_text($xml, 'specificfeedback', $format, $fformat);
$fromform->questionnote = $format->getpath($xml, array('#', 'questionnote', 0, '#', 'text', 0, '#'), '', true);
$fformat = $fromform->questiontextformat;
if (isset($fromform->questiondescriptionformat)) {
$fformat = $fromform->questiondescriptionformat;
}
$fromform->questiondescription = $this->import_xml_text($xml, 'questiondescription', $format, $fformat);
$fromform->questionsimplify = $format->getpath($xml, array('#', 'questionsimplify', 0, '#'), 1);
$fromform->assumepositive = $format->getpath($xml, array('#', 'assumepositive', 0, '#'), 0);
$fromform->assumereal = $format->getpath($xml, array('#', 'assumereal', 0, '#'), 0);
$fformat = $fromform->questiontextformat;
if (isset($fromform->prtcorrectformat)) {
$fformat = $fromform->prtcorrectformat;
}
$fromform->prtcorrect = $this->import_xml_text($xml, 'prtcorrect', $format, $fformat);
$fformat = $fromform->questiontextformat;
if (isset($fromform->prtpartiallycorrectformat)) {
$fformat = $fromform->prtpartiallycorrectformat;
}
$fromform->prtpartiallycorrect = $this->import_xml_text($xml, 'prtpartiallycorrect', $format, $fformat);
$fformat = $fromform->questiontextformat;
if (isset($fromform->prtincorrectformat)) {
$fformat = $fromform->prtincorrectformat;
}
$fromform->prtincorrect = $this->import_xml_text($xml, 'prtincorrect', $format, $fformat);
$fromform->penalty = $format->getpath($xml, array('#', 'penalty', 0, '#'), 0.1);
$fromform->decimals = $format->getpath($xml, array('#', 'decimals', 0, '#'), '.');
$fromform->multiplicationsign = $format->getpath($xml, array('#', 'multiplicationsign', 0, '#'), 'dot');
$fromform->sqrtsign = $format->getpath($xml, array('#', 'sqrtsign', 0, '#'), 1);
$fromform->complexno = $format->getpath($xml, array('#', 'complexno', 0, '#'), 'i');
$fromform->inversetrig = $format->getpath($xml, array('#', 'inversetrig', 0, '#'), 'cos-1');
$fromform->logicsymbol = $format->getpath($xml, array('#', 'logicsymbol', 0, '#'), 'lang');
$fromform->matrixparens = $format->getpath($xml, array('#', 'matrixparens', 0, '#'), '[');
$fromform->variantsselectionseed = $format->getpath($xml, array('#', 'variantsselectionseed', 0, '#'), 'i');
if (isset($xml['#']['input'])) {
foreach ($xml['#']['input'] as $inputxml) {
$this->import_xml_input($inputxml, $fromform, $format);
}
}
if (isset($xml['#']['input'])) {
foreach ($xml['#']['input'] as $inputxml) {
$this->import_xml_input($inputxml, $fromform, $format);
}
}
if (isset($xml['#']['prt'])) {
foreach ($xml['#']['prt'] as $prtxml) {
$this->import_xml_prt($prtxml, $fromform, $format);
}
}
$format->import_hints($fromform, $xml, false, false,
$format->get_format($fromform->questiontextformat));
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
if (isset($xml['#']['deployedseed'])) {
$fromform->deployedseeds = array();
foreach ($xml['#']['deployedseed'] as $seedxml) {
$fromform->deployedseeds[] = $format->getpath($seedxml, array('#'), null);
}
}
if (isset($xml['#']['qtest'])) {
$fromform->testcases = array();
foreach ($xml['#']['qtest'] as $qtestxml) {
list($no, $testcase) = $this->import_xml_qtest($qtestxml, $format);
$fromform->testcases[$no] = $testcase;
}
}
return $fromform;
}
/**
* Helper method used by {@link export_to_xml()}.
* @param array $xml the XML to extract the data from.
* @param string $field the name of the sub-tag in the XML to load the data from.
* @param qformat_xml $format the importer/exporter object.
* @param int $defaultformat Dfeault text format, if it is not given in the file.
* @return array with fields text, format and files.
*/
protected function import_xml_text($xml, $field, qformat_xml $format, $defaultformat) {
$text = array();
$text['text'] = $format->getpath($xml, array('#', $field, 0, '#', 'text', 0, '#'), '', true);
$text['format'] = $format->trans_format($format->getpath($xml, array('#', $field, 0, '@', 'format'),
$format->get_format($defaultformat)));
$text['files'] = $format->import_files($format->getpath($xml, array('#', $field, 0, '#', 'file'), array(), false));
return $text;
}
/**
* Helper method used by {@link export_to_xml()}. Handle the data for one input.
* @param array $xml the bit of the XML representing one input.
* @param object $fromform the data structure we are building from the XML.
* @param qformat_xml $format the importer/exporter object.
*/
protected function import_xml_input($xml, $fromform, qformat_xml $format) {
$name = $format->getpath($xml, array('#', 'name', 0, '#'), null, false, 'Missing input name in the XML.');
$fromform->{$name . 'type'} = $format->getpath($xml, array('#', 'type', 0, '#'), '');
$fromform->{$name . 'modelans'} = $format->getpath($xml, array('#', 'tans', 0, '#'), '');
$fromform->{$name . 'boxsize'} = $format->getpath($xml, array('#', 'boxsize', 0, '#'), 15);
$fromform->{$name . 'strictsyntax'} = $format->getpath($xml, array('#', 'strictsyntax', 0, '#'), 1);
$fromform->{$name . 'insertstars'} = $format->getpath($xml, array('#', 'insertstars', 0, '#'), 0);
$fromform->{$name . 'syntaxhint'} = $format->getpath($xml, array('#', 'syntaxhint', 0, '#'), '');
$fromform->{$name . 'syntaxattribute'} = $format->getpath($xml, array('#', 'syntaxattribute', 0, '#'), 0);
$fromform->{$name . 'forbidwords'} = $format->getpath($xml, array('#', 'forbidwords', 0, '#'), '');
Chris Sangwin
committed
$fromform->{$name . 'allowwords'} = $format->getpath($xml, array('#', 'allowwords', 0, '#'), '');
$fromform->{$name . 'forbidfloat'} = $format->getpath($xml, array('#', 'forbidfloat', 0, '#'), 1);
$fromform->{$name . 'requirelowestterms'} = $format->getpath($xml, array('#', 'requirelowestterms', 0, '#'), 0);
$fromform->{$name . 'checkanswertype'} = $format->getpath($xml, array('#', 'checkanswertype', 0, '#'), 0);
$fromform->{$name . 'mustverify'} = $format->getpath($xml, array('#', 'mustverify', 0, '#'), 1);
$fromform->{$name . 'showvalidation'} = $format->getpath($xml, array('#', 'showvalidation', 0, '#'), 1);
$fromform->{$name . 'options'} = $format->getpath($xml, array('#', 'options', 0, '#'), '');
}
/**
* Helper method used by {@link export_to_xml()}. Handle the data for one PRT.
* @param array $xml the bit of the XML representing one PRT.
* @param object $fromform the data structure we are building from the XML.
* @param qformat_xml $format the importer/exporter object.
*/
protected function import_xml_prt($xml, $fromform, qformat_xml $format) {
$name = $format->getpath($xml, array('#', 'name', 0, '#'), null, false, 'Missing PRT name in the XML.');
$fromform->{$name . 'value'} = $format->getpath($xml, array('#', 'value', 0, '#'), 1);
$fromform->{$name . 'autosimplify'} = $format->getpath($xml, array('#', 'autosimplify', 0, '#'), 1);
$fromform->{$name . 'feedbackstyle'} = $format->getpath($xml, array('#', 'feedbackstyle', 0, '#'), 1);
$fromform->{$name . 'feedbackvariables'} = $format->getpath($xml,
array('#', 'feedbackvariables', 0, '#', 'text', 0, '#'), '', true);
if (isset($xml['#']['node'])) {
foreach ($xml['#']['node'] as $nodexml) {
$this->import_xml_prt_node($nodexml, $name, $fromform, $format);
}
}
}
/**
* Helper method used by {@link import_xml_prt()}. Handle the data for one PRT node.
* @param array $xml the bit of the XML representing one PRT.
* @param string $prtname the name of the PRT this node belongs to.
* @param object $fromform the data structure we are building from the XML.
* @param qformat_xml $format the importer/exporter object.
*/
protected function import_xml_prt_node($xml, $prtname, $fromform, qformat_xml $format) {
$name = $format->getpath($xml, array('#', 'name', 0, '#'), null, false, 'Missing PRT name in the XML.');
$fromform->{$prtname . 'description'}[$name] = $format->getpath($xml, array('#', 'description', 0, '#'), '');
$fromform->{$prtname . 'answertest'}[$name] = $format->getpath($xml, array('#', 'answertest', 0, '#'), '');
$fromform->{$prtname . 'sans'}[$name] = $format->getpath($xml, array('#', 'sans', 0, '#'), '');
$fromform->{$prtname . 'tans'}[$name] = $format->getpath($xml, array('#', 'tans', 0, '#'), '');
$fromform->{$prtname . 'testoptions'}[$name] = $format->getpath($xml, array('#', 'testoptions', 0, '#'), '');
$fromform->{$prtname . 'quiet'}[$name] = $format->getpath($xml, array('#', 'quiet', 0, '#'), 0);
$fromform->{$prtname . 'truescoremode'}[$name] = $format->getpath($xml, array('#', 'truescoremode', 0, '#'), '=');
$fromform->{$prtname . 'truescore'}[$name] = $format->getpath($xml, array('#', 'truescore', 0, '#'), 1);
$fromform->{$prtname . 'truepenalty'}[$name] = $format->getpath($xml, array('#', 'truepenalty', 0, '#'), '');
$fromform->{$prtname . 'truenextnode'}[$name] = $format->getpath($xml, array('#', 'truenextnode', 0, '#'), -1);
$fromform->{$prtname . 'trueanswernote'}[$name] = $format->getpath($xml,
array('#', 'trueanswernote', 0, '#'), 1, '');
$fromform->{$prtname . 'truefeedback'}[$name] = $this->import_xml_text($xml,
'truefeedback', $format, $fromform->questiontextformat);
$fromform->{$prtname . 'falsescoremode'}[$name] = $format->getpath($xml, array('#', 'falsescoremode', 0, '#'), '=');
$fromform->{$prtname . 'falsescore'}[$name] = $format->getpath($xml, array('#', 'falsescore', 0, '#'), 1);
$fromform->{$prtname . 'falsepenalty'}[$name] = $format->getpath($xml, array('#', 'falsepenalty', 0, '#'), '');
$fromform->{$prtname . 'falsenextnode'}[$name] = $format->getpath($xml, array('#', 'falsenextnode', 0, '#'), -1);
$fromform->{$prtname . 'falseanswernote'}[$name] = $format->getpath($xml, array('#', 'falseanswernote', 0, '#'), '');
$fromform->{$prtname . 'falsefeedback'}[$name] = $this->import_xml_text($xml,
'falsefeedback', $format, $fromform->questiontextformat);
}
/**
* Helper method used by {@link export_to_xml()}. Handle the data for one question text.
* @param array $xml the bit of the XML representing one question text.
* @param qformat_xml $format the importer/exporter object.
* @return stack_question_test the question test.
*/
protected function import_xml_qtest($xml, qformat_xml $format) {
$number = $format->getpath($xml, array('#', 'testcase', 0, '#'), null, false, 'Missing testcase number in the XML.');
$inputs = array();
$description = '';
if (isset($xml['#']['description'])) {
$description = $format->getpath($xml, array('#', 'description', 0, '#'), '');
}
if (isset($xml['#']['testinput'])) {
foreach ($xml['#']['testinput'] as $inputxml) {
$name = $format->getpath($inputxml, array('#', 'name', 0, '#'), '');
$value = $format->getpath($inputxml, array('#', 'value', 0, '#'), '');
$inputs[$name] = $value;
}
}
$testcase = new stack_question_test($description, $inputs, $number);
if (isset($xml['#']['expected'])) {
foreach ($xml['#']['expected'] as $expectedxml) {
$name = $format->getpath($expectedxml, array('#', 'name', 0, '#'), '');
$expectedscore = $format->getpath($expectedxml, array('#', 'expectedscore', 0, '#'), '');
$expectedpenalty = $format->getpath($expectedxml, array('#', 'expectedpenalty', 0, '#'), '');
$expectedanswernote = $format->getpath($expectedxml, array('#', 'expectedanswernote', 0, '#'), '');
$testcase->add_expected_result($name, new stack_potentialresponse_tree_state(
1, true, $expectedscore, $expectedpenalty, '', array($expectedanswernote)));
}
}
return array($number, $testcase);
}
Chris Sangwin
committed
/*
* This method takes Moodle's "fromform" data type and validates the question. All question level validation and warnings
* should be in this method.
* Much of this code was in edit_stack_form.php (until Jan 2018).
* See https://docs.moodle.org/dev/Question_data_structures for why we chose the "fromform" data structure,
* not "question" objects.
*
* @param array $fromform Moodle's "fromform" data type.
* @param array $errors Existing partial error array.
* @return array($errors, $warnings).
*/
public function validate_fromform($fromform, $errors) {
$fixingdollars = array_key_exists('fixdollars', $fromform);
$this->options = new stack_options();
$this->options->set_option('decimals', $fromform['decimals']);
Chris Sangwin
committed
$this->options->set_option('multiplicationsign', $fromform['multiplicationsign']);
$this->options->set_option('complexno', $fromform['complexno']);
$this->options->set_option('inversetrig', $fromform['inversetrig']);
$this->options->set_option('logicsymbol', $fromform['logicsymbol']);
Chris Sangwin
committed
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
$this->options->set_option('matrixparens', $fromform['matrixparens']);
$this->options->set_option('sqrtsign', (bool) $fromform['sqrtsign']);
$this->options->set_option('simplify', (bool) $fromform['questionsimplify']);
$this->options->set_option('assumepos', (bool) $fromform['assumepositive']);
$this->options->set_option('assumereal', (bool) $fromform['assumereal']);
// We slightly break the usual conventions of validation, in that rather
// than building up $errors as an array of strings, we initially build it
// up as an array of arrays, then at the end remove any empty arrays,
// and implode (' ', ...) any arrays that are non-empty. This makes our
// rather complex validation easier to implement.
// Question text.
$errors['questiontext'] = array();
$errors = $this->validate_cas_text($errors, $fromform['questiontext']['text'], 'questiontext', $fixingdollars);
// Check multi-language versions all have the same feedback tags.
$ml = new stack_multilang();
$combinedtext = $fromform['questiontext']['text'] . $fromform['specificfeedback']['text'];
$langs = $ml->languages_used($combinedtext);
if ($langs == array()) {
$prts = $this->get_prt_names_from_question($fromform['questiontext']['text'], $fromform['specificfeedback']['text']);
} else {
$prtsbylang = array();
foreach ($langs as $lang) {
$prtsbylang[$lang] = $this->get_prt_names_from_question_lang($ml->filter($combinedtext, $lang));
}
// Check they are all equal, but don't fuss about exact differences as feedback.
$prts = reset($prtsbylang);
$failed = false;
foreach ($langs as $lang) {
if ($prtsbylang[$lang] != $prts) {
$failed = true;
}
}
if ($failed) {
$errors['questiontext'][] = stack_string('questiontextfeedbacklanguageproblems');
}
}
// Check for whitespace following placeholders.
$sloppytags = $this->validation_get_sloppy_tags($fromform['questiontext']['text']);
foreach ($sloppytags as $sloppytag) {
$errors['questiontext'][] = stack_string(
'questiontextplaceholderswhitespace', $sloppytag);
}
// Check multi-language versions all have the same inputs and validation tags.
$ml = new stack_multilang();
$langs = $ml->languages_used($fromform['questiontext']['text']);
if ($langs == array()) {
$inputs = $this->get_input_names_from_question_text_lang($fromform['questiontext']['text']);
} else {
$inputsbylang = array();
foreach ($langs as $lang) {
$inputsbylang[$lang] = $this->get_input_names_from_question_text_lang(
$ml->filter($fromform['questiontext']['text'], $lang));
}
// Check they are all equal, but don't fuss about exact differences as feedback.
$inputs = reset($inputsbylang);
$failed = false;
foreach ($langs as $lang) {
if ($inputsbylang[$lang] != $inputs) {
$failed = true;
}
}
if ($failed) {
$errors['questiontext'][] = stack_string('inputlanguageproblems');
}
}
// Check input placholders appear with the correct number of times in the question text.
foreach ($inputs as $inputname => $counts) {
list($numinputs, $numvalidations) = $counts;
if ($numinputs == 0 && $numvalidations == 0) {
if (!$fromform[$inputname . 'deleteconfirm']) {
$errors['questiontext'][] = stack_string('inputremovedconfirmbelow', $inputname);
}
continue;
}
if ($numinputs == 0) {
$errors['questiontext'][] = stack_string(
'questiontextmustcontain', '[[input:' . $inputname . ']]');
} else if ($numinputs > 1) {
$errors['questiontext'][] = stack_string(
'questiontextonlycontain', '[[input:' . $inputname . ']]');
}
if ($numvalidations == 0) {
$errors['questiontext'][] = stack_string(
'questiontextmustcontain', '[[validation:' . $inputname . ']]');
} else if ($numvalidations > 1) {
$errors['questiontext'][] = stack_string(
'questiontextonlycontain', '[[validation:' . $inputname . ']]');
}
}
if (empty($inputs) && !empty($prts)) {
$errors['questiontext'][] = stack_string('noprtsifnoinputs');
}
// Question variables.
$errors = $this->validate_cas_keyval($errors, $fromform['questionvariables'], 'questionvariables',
array_keys($inputs));
// Default mark.
if (empty($inputs) && $fromform['defaultmark'] != 0) {
$errors['defaultmark'][] = stack_string('defaultmarkzeroifnoprts');
}
// Penalty.
$penalty = $fromform['penalty'];
if (!is_numeric($penalty) || $penalty < 0 || $penalty > 1) {
$errors['penalty'][] = stack_string('penaltyerror');
}
// Specific feedback.
$errors['specificfeedback'] = array();
$errors = $this->validate_cas_text($errors, $fromform['specificfeedback']['text'], 'specificfeedback', $fixingdollars);
$errors['specificfeedback'] += $this->validation_check_no_placeholders(
stack_string('specificfeedback'), $fromform['specificfeedback']['text'],
array('input', 'validation'));
// General feedback.
$errors['generalfeedback'] = array();
$errors = $this->validate_cas_text($errors, $fromform['generalfeedback']['text'], 'generalfeedback', $fixingdollars);
$errors['generalfeedback'] += $this->validation_check_no_placeholders(
get_string('generalfeedback', 'question'), $fromform['generalfeedback']['text']);
// Question note.
$errors['questionnote'] = array();
if ('' == $fromform['questionnote']) {
$foundrandom = false;
foreach (stack_cas_security::get_all_with_feature('random') as $rndid) {
if (!(false === strpos($fromform['questionvariables'], $rndid))) {
$foundrandom = true;
break;
}
Chris Sangwin
committed
}
if ($foundrandom) {
$errors['questionnote'][] = stack_string('questionnotempty');
}
} else {
// Note, the 'questionnote' does not have an editor field and hence no 'text' sub-clause.
$errors = $this->validate_cas_text($errors, $fromform['questionnote'], 'questionnote', $fixingdollars);
}
$errors['questionnote'] += $this->validation_check_no_placeholders(
stack_string('questionnote'), $fromform['questionnote']);
// Question description.
$errors['questiondescription'] = array();
$errors = $this->validate_cas_text($errors, $fromform['questiondescription']['text'],
'questiondescription', $fixingdollars);
$errors['questiondescription'] += $this->validation_check_no_placeholders(
stack_string('questiondescription', 'question'), $fromform['questiondescription']['text']);
Chris Sangwin
committed
// 2) Validate all inputs.
Chris Sangwin
committed
foreach ($inputs as $inputname => $counts) {
list($numinputs, $numvalidations) = $counts;
if ($numinputs == 0 && $numvalidations == 0 && !$fromform[$inputname . 'deleteconfirm']) {
$errors[$inputname . 'deleteconfirm'][] = stack_string('youmustconfirm');
}
if ($numinputs == 0 && $numvalidations == 0) {
// Input is being deleted. Don't show validation errors.
continue;
}
if (strlen($inputname) > 18 && !isset($fromform[$inputname . 'deleteconfirm'])) {
$errors['questiontext'][] = stack_string('inputnamelength', $inputname);
}
Chris Sangwin
committed
if (!preg_match('/^([a-zA-Z]+|[a-zA-Z]+[0-9a-zA-Z_]*[0-9a-zA-Z]+)$/', $inputname) &&
!isset($fromform[$inputname . 'deleteconfirm'])) {
$errors['questiontext'][] = stack_string('inputnameform', $inputname);
}
if ($fromform[$inputname . 'mustverify'] && $fromform[$inputname . 'showvalidation'] == 0) {
Chris Sangwin
committed
$errors[$inputname . 'mustverify'][] = stack_string('mustverifyshowvalidation');
}
if (array_key_exists($inputname . 'modelans', $fromform)) {
$errors = $this->validate_cas_string($errors,
$fromform[$inputname . 'modelans'], $inputname . 'modelans', $inputname . 'modelans');
}
$modelans = '';
if (array_key_exists($inputname . 'modelans', $fromform)) {
$modelans = $fromform[$inputname . 'modelans'];
}
$stackinput = $stackinputfactory->make($inputtype, $inputname, $modelans, null, null, false);
$parameters = array();
foreach ($stackinputfactory->get_parameters_fromform_mapping($inputtype) as $key => $param) {
Chris Sangwin
committed
$paramvalue = '';
if (array_key_exists($inputname . $param, $fromform)) {
$paramvalue = $fromform[$inputname . $param];
}
$paramvalue = $stackinputfactory->convert_parameter_fromform($key, $paramvalue);
$parameters[$key] = $paramvalue;
if ('options' !== $key) {
$validityresult = $stackinput->validate_parameter($key, $paramvalue);
if (!($validityresult === true)) {
$errors[$inputname . $param][] = stack_string('inputinvalidparamater');
}
}
}
// Create an input with these parameters, in particular the 'options', and validate that.
Chris Sangwin
committed
$stackinput = $stackinputfactory->make($inputtype, $inputname, $modelans, null, $parameters, false);
$stackinput->validate_extra_options();
$errors[$inputname . 'options'] = $stackinput->get_errors();
Chris Sangwin
committed
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
}
// 3) Validate all prts.
foreach ($prts as $prtname => $count) {
if ($count == 0) {
if (!$fromform[$prtname . 'prtdeleteconfirm']) {
$errors['specificfeedback'][] = stack_string('prtremovedconfirmbelow', $prtname);
$errors[$prtname . 'prtdeleteconfirm'][] = stack_string('youmustconfirm');
}
// Don't show validation errors relating to a PRT that is to be deleted.
continue;
} else if ($count > 1) {
$errors['specificfeedback'][] = stack_string(
'questiontextfeedbackonlycontain', '[[feedback:' . $prtname . ']]');
}
$errors = $this->validate_prt($errors, $fromform, $prtname, $fixingdollars);
}
// 4) Validate all hints.
foreach ($fromform['hint'] as $index => $hint) {
$errors = $this->validate_cas_text($errors, $hint['text'], 'hint[' . $index . ']', $fixingdollars);
}
// Clear out any empty $errors elements, ready for the next check.
foreach ($errors as $field => $messages) {
if (empty($messages)) {
unset($errors[$field]);
}
}
// If everything else is OK, try executing the CAS code to check for errors.
if (empty($errors)) {
$errors = $this->validate_question_cas_code($errors, $fromform, $fixingdollars);
}
// Convert the $errors array from our array of arrays format to the
// standard array of strings format.
Chris Sangwin
committed
foreach ($errors as $field => $messages) {
if ($messages) {
foreach ($messages as $key => $val) {
if (is_array($val)) {
$messages[$key] = implode(' ', $val);
}
}
Chris Sangwin
committed
$errors[$field] = implode(' ', $messages);
Chris Sangwin
committed
} else {
unset($errors[$field]);
}
}
if ($errorsexit) {
// Add a message next to the question name to create a more prominent error alert.
$errors['name'] = stack_string_error('generalerrors');
}
Chris Sangwin
committed
Chris Sangwin
committed
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
}
/**
* Validate a CAS string field to make sure that: 1. it fits in the DB, and
* 2. that it is syntactically valid.
* @param array $errors the errors array that validation is assembling.
* @param string $value the submitted value validate.
* @param string $fieldname the name of the field add any errors to.
* @param string $savesession the array key to save the string to in $this->validationcasstrings.
* @param bool|string $notblank false means do nothing (default). A string
* will validate that the field is not blank, and if it is, display that error.
* @param int $maxlength the maximum allowable length. Defaults to 255.
* @return array updated $errors array.
*/
protected function validate_cas_string($errors, $value, $fieldname, $savesession, $notblank = true, $maxlength = 255) {
if ($notblank && '' === trim($value)) {
$errors[$fieldname][] = stack_string('nonempty');
} else if (strlen($value) > $maxlength) {
$errors[$fieldname][] = stack_string('strlengtherror');
} else {
$casstring = stack_ast_container::make_from_teacher_source($value, '', new stack_cas_security());
if (!$casstring->get_valid()) {
Chris Sangwin
committed
$errors[$fieldname][] = $casstring->get_errors();
}
}
// These particular castrings must not end in a semicolon, (unlike keyvals!).
$tvalue = trim($value);
if ($tvalue === ';') {
$errors[$fieldname][] = stack_string('nosemicolon');
}
Chris Sangwin
committed
return $errors;
}
/**
* Validate a CAS text field.
* @param array $errors the errors array that validation is assembling.
* @param string $value the submitted value validate.
* @param string $fieldname the name of the field add any errors to.
* @param string $savesession the array key to save the session to in $this->validationcasstrings.
* @return array updated $errors array.
*/
protected function validate_cas_text($errors, $value, $fieldname, $fixingdollars, $session = null) {
if (!$fixingdollars && strpos($value, '$$') !== false) {
$errors[$fieldname][] = stack_string('forbiddendoubledollars');
}
// The castext2_evaluatable-class is much simpler for validation use.
Matti Harjula
committed
// Could ask the utils class directly for the internal casstrings,
// but why when the evaluatable-class already does that.
$castext = castext2_evaluatable::make_from_source($value, 'validation-' . $fieldname);
Chris Sangwin
committed
if (!$castext->get_valid()) {
$errors[$fieldname][] = $castext->get_errors();
return $errors;
}
// Validate any [[facts:...]] tags.
$unrecognisedtags = stack_fact_sheets::get_unrecognised_tags($value);
if ($unrecognisedtags) {
$errors[$fieldname][] = stack_string('unrecognisedfactstags',
array('tags' => implode(', ', $unrecognisedtags)));
return $errors;
}
if ($castext->get_errors()) {
$errors[$fieldname][] = $castext->get_errors();
return $errors;
Chris Sangwin
committed
}
Chris Sangwin
committed
return $errors;
}
/**
* Validate a CAS string field to make sure that: 1. it fits in the DB, and
* 2. that it is syntactically valid.
* @param array $errors the errors array that validation is assembling.
* @param string $value the submitted value validate.
* @param string $fieldname the name of the field add any errors to.
* @return array updated $errors array.
*/
protected function validate_cas_keyval($errors, $value, $fieldname, $inputs = null) {
if ('' == trim($value)) {
return $errors;
}
$keyval = new stack_cas_keyval($value, $this->options, $this->seed);
Chris Sangwin
committed
if (!$keyval->get_valid($inputs)) {
$errors[$fieldname][] = $keyval->get_errors();
}
return $errors;
}
/**
* Validate all the maxima code in the question.
*
* This is done last, and separate from the other validation for two reasons:
* 1. The rest of the validation is organised to validate the form in order,
* to match the way the form is defined. Here we need to validate in the
Chris Sangwin
committed
* order that the CAS is evaluated at runtime.
Chris Sangwin
committed
* 2. This is the slowest part of validation, so we only do it at the end if
* everything else is OK.
*
* @param array $errors the errors array that validation is assembling.
* @param array $fromform the submitted data to validate.
* @return array updated $errors array.
*/
protected function validate_question_cas_code($errors, $fromform, $fixingdollars) {
$keyval = new stack_cas_keyval($fromform['questionvariables'], $this->options, $this->seed);
if ($keyval->get_valid()) {
$runtimeerrors = $keyval->instantiate();
}
if ($runtimeerrors) {
$errors['questionvariables'][] = $runtimeerrors;
Chris Sangwin
committed
$session = $keyval->get_session();
if ($session->get_errors()) {
$errors['questionvariables'][] = $session->get_errors(true);
$errors['questionvariables'] = array_unique($errors['questionvariables']);
Chris Sangwin
committed
return $errors;
}
// Instantiate all text fields and look for errors.
$castextfields = array('questiontext', 'specificfeedback', 'generalfeedback', 'questiondescription');
Chris Sangwin
committed
foreach ($castextfields as $field) {
$errors = $this->validate_cas_text($errors, $fromform[$field]['text'], $field, $fixingdollars, clone $session);
}
$errors = $this->validate_cas_text($errors, $fromform['questionnote'], 'questionnote', $fixingdollars, clone $session);
// Make a list of all inputs, instantiate it and then look for errors.
$inputs = array_keys($this->get_input_names_from_question_text($fromform['questiontext']['text']));
$inputvalues = array();
foreach ($inputs as $inputname) {
if (array_key_exists($inputname . 'modelans', $fromform)) {
$value = $inputname.':'.$fromform[$inputname . 'modelans'];
$cs = stack_ast_container::make_from_teacher_source($value, '', new stack_cas_security());
Chris Sangwin
committed
$inputvalues[] = $cs;
}
}
Matti Harjula
committed
// TODO: why clone when we never reuse the original...
Chris Sangwin
committed
$inputsession = clone $session;
$inputsession->add_statements($inputvalues);
if ($inputsession->get_valid()) {
$inputsession->instantiate();
}
Chris Sangwin
committed
$getdebuginfo = false;
foreach ($inputs as $inputname) {
if ($inputsession->get_by_key($inputname) !== null &&
$inputsession->get_by_key($inputname)->get_errors() !== '') {
Matti Harjula
committed
$errors[$inputname . 'modelans'][] = $inputsession->get_by_key($inputname)->get_errors();
$in = $inputsession->get_by_key($inputname);
if (!$in->is_correctly_evaluated()) {
Chris Sangwin
committed
$getdebuginfo = true;
}
Matti Harjula
committed
// TODO: Send the actual value to the input, and ask it to validate it.
Chris Sangwin
committed
// For example, the matrix input type could check that the model answer is a matrix.
}
if ($fromform[$inputname . 'options'] && $inputsession->get_by_key('optionsfor' . $inputname)
&& $inputsession->get_by_key('optionsfor' . $inputname)->get_errors() !== '') {
Matti Harjula
committed
$errors[$inputname . 'options'][] = $inputsession->get_by_key('optionsfor' . $inputname)->get_errors();
Chris Sangwin
committed
}
Matti Harjula
committed
// ... else TODO: Send the actual value to the input, and ask it to validate it.
Chris Sangwin
committed
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
}
if ($getdebuginfo) {
$errors['questionvariables'][] = $inputsession->get_debuginfo();
}
// At this point if we have errors, especially with inputs, there is no point in executing any of the PRTs.
if (!empty($errors)) {
return $errors;
}
// TODO: loop over all the PRTs in a similar manner....
// Remember, to clone the inputsession as the base session for each PRT.
// This will have all the teacher's answers instantiated.
// Otherwise we are likley to do illigitimate things to the various inputs.
return $errors;
}
/**
* Tags which have extra whitespace within them. E.g. [[input: ans1]] are forbidden.
* @return array of tags.
*/
public function validation_get_sloppy_tags($text) {
$sloppytags = stack_utils::extract_placeholders_sloppy($text, 'input');
$sloppytags = array_merge(stack_utils::extract_placeholders_sloppy($text, 'validation'), $sloppytags);
$sloppytags = array_merge(stack_utils::extract_placeholders_sloppy($text, 'prt'), $sloppytags);
return $sloppytags;
}
/**
* Check a form field to ensure it does not contain any placeholders of given types.
* @param string $fieldname the name of this field. Used in the error messages.
* @param value $value the value to check.
* @param array $placeholders types to check for. By default 'input', 'validation' and 'feedback'.
* @return array of problems (so an empty array means all is well).
*/
protected function validation_check_no_placeholders($fieldname, $value,
$placeholders = array('input', 'validation', 'feedback')) {
$problems = array();
foreach ($placeholders as $placeholder) {
if (stack_utils::extract_placeholders($value, 'input')) {
$problems[] = stack_string('fieldshouldnotcontainplaceholder',
array('field' => $fieldname, 'type' => $placeholder));
}
}
return $problems;
}
/**
* Validate the fields for a given PRT
* @param array $errors the error so far. This array is added to and returned.
* @param array $fromform the submitted data to validate.
* @param string $prtname the name of the PRT to validate.
* @return array the update $errors array.
*/
protected function validate_prt($errors, $fromform, $prtname, $fixingdollars) {
if (strlen($prtname) > 18 && !isset($fromform[$prtname . 'prtdeleteconfirm'])) {
$errors['specificfeedback'][] = stack_string('prtnamelength', $prtname);
}
if (!array_key_exists($prtname . 'feedbackvariables', $fromform)) {
// This happens when you edit the question text to add more PRTs.
// The user added a new PRT and did not click "Verify the question
// text and update the form". We need to fail validation, so the
// form is re-displayed so that this PRT can be configured.
$errors[$prtname . 'value'][] = stack_string('prtmustbesetup');
return $errors;
}
// Check the fields that belong to the PRT as a whole.
$inputs = array_keys($this->get_input_names_from_question_text($fromform['questiontext']['text']));
$errors = $this->validate_cas_keyval($errors, $fromform[$prtname . 'feedbackvariables'],
$prtname . 'feedbackvariables', $inputs);
if ($fromform[$prtname . 'value'] < 0) {
$errors[$prtname . 'value'][] = stack_string('questionvaluepostive');
}
// Check that answernotes are not duplicated.
$answernotes = array_merge($fromform[$prtname . 'trueanswernote'], $fromform[$prtname . 'falseanswernote']);
if (count(array_unique($answernotes)) < count($answernotes)) {
// Strictly speaking this should not be in the feedback variables. But there is no general place to put this error.
$errors[$prtname . 'feedbackvariables'][] = stack_string('answernoteunique');
}
// Check the nodes.
Chris Sangwin
committed
$question = null;
if (property_exists($this, 'question')) {
$question = $this->question;
}
$graph = $this->get_prt_graph($prtname, $question);
Chris Sangwin
committed
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
$textformat = null;
foreach ($graph->get_nodes() as $node) {
$nodekey = $node->name - 1;
// Check the fields the belong to this node individually.
$errors = $this->validate_prt_node($errors, $fromform, $prtname, $nodekey, $fixingdollars);
if (is_null($textformat)) {
$textformat = $fromform[$prtname . 'truefeedback'][$nodekey]['format'];
}
if ($textformat != $fromform[$prtname . 'truefeedback'][$nodekey]['format']) {
$errors[$prtname . 'truefeedback[' . $nodekey . ']'][]
= stack_string('allnodefeedbackmustusethesameformat');
}
}
// Check that the nodes form a directed acyclic graph.
$roots = $graph->get_roots();
// There should only be a single root. If there is more than one, then we
// assume that the first one is the intended root, and flat the others as unused.
array_shift($roots);
foreach ($roots as $node) {
$errors[$prtname . 'node[' . ($node->name - 1) . ']'][] = stack_string('nodenotused');
}
foreach ($graph->get_broken_cycles() as $backlink => $notused) {
list($nodename, $direction) = explode('|', $backlink);
if ($direction == stack_abstract_graph::LEFT) {
$field = 'nodewhentrue';
} else {
$field = 'nodewhenfalse';
}
$errors[$prtname.$field.'['.($nodename - 1).']'][] = stack_string('nodeloopdetected');
}
// TODO: check the descriptions of all nodes are unique.
Chris Sangwin
committed
return $errors;
}
/**
* Validate the fields for a given PRT node.
* @param array $errors the error so far. This array is added to and returned.
* @param array $fromform the submitted data to validate.
* @param string $prtname the name of the PRT to validate.
* @param string $nodekey the name of the node to validate.
* @return array the update $errors array.
*/
protected function validate_prt_node($errors, $fromform, $prtname, $nodekey, $fixingdollars) {
$nodegroup = $prtname . 'node[' . $nodekey . ']';
$errors = $this->validate_cas_string($errors, $fromform[$prtname . 'sans'][$nodekey],
$nodegroup, $prtname . 'sans' . $nodekey, 'sansrequired');
$errors = $this->validate_cas_string($errors, $fromform[$prtname . 'tans'][$nodekey],
$nodegroup, $prtname . 'tans' . $nodekey, 'tansrequired');
Chris Sangwin
committed
$atname = $fromform[$prtname . 'answertest'][$nodekey];
if (stack_ans_test_controller::required_atoptions($atname)) {
Chris Sangwin
committed
$opt = trim($fromform[$prtname . 'testoptions'][$nodekey]);
if ('' === trim($opt) && stack_ans_test_controller::required_atoptions($atname) === true) {
Chris Sangwin
committed
$errors[$nodegroup][] = stack_string('testoptionsrequired');
} else if (strlen($opt) > 255) {
Chris Sangwin
committed
$errors[$nodegroup][] = stack_string('testoptionsinvalid',
stack_string('strlengtherror'));
} else {
$cs = stack_ast_container::make_from_teacher_source('null', '', new stack_cas_security());
$answertest = new stack_ans_test_controller($atname, $cs, $cs);
Chris Sangwin
committed
list($valid, $message) = $answertest->validate_atoptions($opt);
if (!$valid) {
$errors[$nodegroup][] = stack_string('testoptionsinvalid', $message);
}
}
}
$description = $fromform[$prtname . 'description'][$nodekey];
if (mb_strlen($description) > 255) {
$errors[$nodegroup][] = stack_string('description_err');
}
Chris Sangwin
committed
foreach (array('true', 'false') as $branch) {
$branchgroup = $prtname . 'nodewhen' . $branch . '[' . $nodekey . ']';
$score = $fromform[$prtname . $branch . 'score'][$nodekey];
Chris Sangwin
committed
if (is_numeric($score)) {
if ($score < 0 || $score > 1) {
$errors[$branchgroup][] = stack_string('scoreerror');
}
Chris Sangwin
committed
}
Chris Sangwin
committed
if (array_key_exists($prtname . $branch . 'penalty', $fromform)) {
$penalty = $fromform[$prtname . $branch . 'penalty'][$nodekey];
if ('' != $penalty && is_numeric($penalty)) {
if ($penalty < 0 || $penalty > 1) {
$errors[$branchgroup][] = stack_string('penaltyerror2');
}
Chris Sangwin
committed
}
Chris Sangwin
committed
} // Else we have just deleted the PRT.
Chris Sangwin
committed
Chris Sangwin
committed
$answernote = $fromform[$prtname . $branch . 'answernote'][$nodekey];
if ('' == $answernote) {
$errors[$branchgroup][] = stack_string('answernoterequired');
} else if (strstr($answernote, '|') !== false) {
$errors[$branchgroup][] = stack_string('answernote_err');
foreach ($fromform[$prtname.$branch.'answernote'] as $key => $strin) {
if ('' == trim($strin)) {
$interror[$prtname.'nodewhen'.$branch.'['.$key.']'][] = stack_string('answernoterequired');
} else if (strstr($strin, '|') !== false) {
$nodename = $key + 1;
$interror[$prtname.'nodewhen'.$branch.'['.$key.']'][] = stack_string('answernote_err');
}
}
Chris Sangwin
committed
} else if (strstr($answernote, ';') !== false || strstr($answernote, ':') !== false) {
$errors[$branchgroup][] = stack_string('answernote_err2');
Chris Sangwin
committed
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
}
$errors = $this->validate_cas_text($errors, $fromform[$prtname . $branch . 'feedback'][$nodekey]['text'],
$prtname . $branch . 'feedback[' . $nodekey . ']', $fixingdollars);
}
return $errors;
}
/**
* This method is needed for validation, and to construct the editing form.
* @return array of the input names that currently appear in the question text.
*/
public function get_input_names_from_question_text($questiontext) {
$ml = new stack_multilang();
$langs = $ml->languages_used($questiontext);
if ($langs == array()) {
return $this->get_input_names_from_question_text_lang($questiontext);
}
// At this point, all languages are assumed to have the same inputs.
$lang = reset($langs);
return($this->get_input_names_from_question_text_lang($ml->filter($questiontext, $lang)));
}
private function get_input_names_from_question_text_lang($questiontext) {
$inputs = stack_utils::extract_placeholders($questiontext, 'input');
$validations = stack_utils::extract_placeholders($questiontext, 'validation');
Chris Sangwin
committed
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
$inputnames = array();
$data = data_submitted();
if ($data) {
foreach (get_object_vars($data) as $name => $value) {
if (preg_match('~(' . stack_utils::VALID_NAME_REGEX . ')modelans~', $name, $matches)) {
$inputnames[$matches[1]] = array(0, 0);
}
}
}
foreach ($inputs as $inputname) {
if (!array_key_exists($inputname, $inputnames)) {
$inputnames[$inputname] = array(0, 0);
}
$inputnames[$inputname][self::INPUTS] += 1;
}
foreach ($validations as $inputname) {
if (!array_key_exists($inputname, $inputnames)) {
$inputnames[$inputname] = array(0, 0);
}
$inputnames[$inputname][self::VALIDATIONS] += 1;
}
return $inputnames;
}
/**
* This method is needed for validation, and to construct the editing form.
* @return array of the PRT names that currently appear in the question
* text and specific feedback.
*/
public function get_prt_names_from_question($questiontext, $specificfeedback) {
$ml = new stack_multilang();
$langs = $ml->languages_used($questiontext.$specificfeedback);
if ($langs == array()) {
return $this->get_prt_names_from_question_lang($questiontext.$specificfeedback);
}
// At this point, all languages are assumed to have the same prts.
$lang = reset($langs);
return($this->get_prt_names_from_question_lang($ml->filter($questiontext.$specificfeedback, $lang)));
}
private function get_prt_names_from_question_lang($text) {
$prts = stack_utils::extract_placeholders($text, 'feedback');
$prtnames = array();
$data = data_submitted();
if ($data) {
foreach (get_object_vars($data) as $name => $value) {
if (preg_match('~(' . stack_utils::VALID_NAME_REGEX . ')feedbackvariables~', $name, $matches)) {
$prtnames[$matches[1]] = 0;
}
}
}
foreach ($prts as $name) {
if (!array_key_exists($name, $prtnames)) {
$prtnames[$name] = 0;
}
$prtnames[$name] += 1;
}
return $prtnames;
}
/**
* Get a list of the PRT notes that should be present for a given PRT.
* @param string $prtname the name of a PRT.
Chris Sangwin
committed
* @param $question the question itself.
Chris Sangwin
committed
* @return array list of nodes that should be present in the form definitino for this PRT.
*/
Chris Sangwin
committed
public function get_prt_graph($prtname, $question) {
Chris Sangwin
committed
if (array_key_exists($prtname, $this->prtgraph)) {
return $this->prtgraph[$prtname];
}
// If the form has been submitted and is being redisplayed, and this is
// an existing PRT, base things on the submitted data.
$submitted = optional_param_array($prtname . 'truenextnode', null, PARAM_RAW);
if ($submitted) {
$description = optional_param_array($prtname . 'description', null, PARAM_RAW);
Chris Sangwin
committed
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
$truescoremode = optional_param_array($prtname . 'truescoremode', null, PARAM_RAW);
$truescore = optional_param_array($prtname . 'truescore', null, PARAM_RAW);
$falsenextnode = optional_param_array($prtname . 'falsenextnode', null, PARAM_RAW);
$falsescoremode = optional_param_array($prtname . 'falsescoremode', null, PARAM_RAW);
$falsescore = optional_param_array($prtname . 'falsescore', null, PARAM_RAW);
$graph = new stack_abstract_graph();
$deletednode = null;
$lastkey = -1;
foreach ($submitted as $key => $truenextnode) {
if (optional_param($prtname . 'nodedelete' . $key, false, PARAM_BOOL)) {
// For deleted nodes, we add them to the tree anyway, and
// then remove them again below. We have to do it that way
// because we also need to delete links that point to the
// deleted node.
$deletednode = $key;
}
if ($truenextnode == -1 || !array_key_exists($truenextnode, $submitted)) {
$left = null;
} else {
$left = $truenextnode + 1;
}
if ($falsenextnode[$key] == -1 || !array_key_exists($falsenextnode[$key], $submitted)) {
$right = null;
} else {
$right = $falsenextnode[$key] + 1;
}
Chris Sangwin
committed
$ts = $truescore[$key];
if (is_numeric($ts)) {
$ts = round($ts, 2);
}
$fs = $falsescore[$key];
if (is_numeric($fs)) {
$fs = round($fs, 2);
}
$graph->add_prt_node($key + 1, $description[$key], $left, $right,
Chris Sangwin
committed
$truescoremode[$key] . $ts,
$falsescoremode[$key] . $fs,
Chris Sangwin
committed
'#fgroup_id_' . $prtname . 'node_' . $key);
$lastkey = max($lastkey, $key);
}
if (optional_param($prtname . 'nodeadd', false, PARAM_BOOL)) {
$graph->add_prt_node($lastkey + 2, '', null, null, '+0', '-0',
'#fgroup_id_' . $prtname . 'node_' . ($lastkey + 1));
Chris Sangwin
committed
}
if (!is_null($deletednode)) {
$graph->remove_node($deletednode + 1);
}
$graph->layout();
$this->prtgraph[$prtname] = $graph;
return $graph;
}
// Otherwise, if an existing question is being edited, and this is an
// existing PRT, base things on the existing question definition.
Chris Sangwin
committed
if (!empty($question->prts[$prtname]->nodes)) {
Chris Sangwin
committed
$graph = new stack_abstract_graph();
Chris Sangwin
committed
foreach ($question->prts[$prtname]->nodes as $node) {
Chris Sangwin
committed
if ($node->truenextnode == -1) {
$left = null;
} else {
$left = $node->truenextnode + 1;
}
if ($node->falsenextnode == -1) {
$right = null;
} else {
$right = $node->falsenextnode + 1;
}
$graph->add_prt_node($node->nodename + 1, $node->description, $left, $right,
$node->truescoremode . $node->truescore,
$node->falsescoremode . $node->falsescore,
Chris Sangwin
committed
'#fgroup_id_' . $prtname . 'node_' . $node->nodename);
// Generate a text-based representation of the cas command.
$at = stack_potentialresponse_tree_lite::compile_node_answertest($node);
$graph->add_prt_text($node->nodename + 1, $at, $node->quiet,
$node->trueanswernote, $node->falseanswernote);
Chris Sangwin
committed
}
$graph->layout();
$this->prtgraph[$prtname] = $graph;
return $graph;
}
// Otherwise, it is a new PRT. Just one node.
// And we don't add any text-based information for new PRTs.
Chris Sangwin
committed
$graph = new stack_abstract_graph();
$graph->add_prt_node('1', '', null, null, '=1', '=0', '#fgroup_id_' . $prtname . 'node_0');
Chris Sangwin
committed
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
$graph->layout();
$this->prtgraph[$prtname] = $graph;
return $graph;
}
/**
* Helper method to get the list of inputs required by a PRT, given the current
* state of the form.
* @param string $prtname the name of a PRT.
* @param qtype_stack $question
* @return array list of inputs used by this PRT.
*/
public function get_inputs_used_by_prt($prtname, $question) {
// Needed for questions with no inputs, (in particular blank starting questions).
if (!property_exists($question, 'inputs')) {
return array();
}
if (is_null($question->inputs)) {
return array();
}
$inputs = $question->inputs;
$inputkeys = array();
if (is_array($inputs)) {
foreach ($inputs as $input) {
$inputkeys[$input->name] = $input->name;
Chris Sangwin
committed
}
} else {
return array();
}
Matti Harjula
committed
// Note in real use we would simply read this from the compiled-cache
// and would let it find out if need be, but the assumption is that
Matti Harjula
committed
// at this moment it is not possible. Basically:
Matti Harjula
committed
Chris Sangwin
committed
// TODO fix this. At the moment it only considers the data from the unedited
// question. We should take into account any changes made since the
// form was first shown, for example adding or removing nodes, or changing
// the things they compare. However, it is not critical.
// If we are creating a new question, or if we add a new prt in the
// question stem, then the PRT will not yet exist, so return an empty array.
if (is_null($question->prts) || !array_key_exists($prtname, $question->prts)) {
return array();
}
$prt = $question->prts[$prtname];
Chris Sangwin
committed
// Safe to hard-wire the value as 1 here as this PRT is not used for scoreing.
$prt = new stack_potentialresponse_tree_lite($prt, 1, $question);
Matti Harjula
committed
// Just do the full compile it does all the checking including feedback.
$compile['required'] = array();
try {
$compile = $prt->compile($inputkeys, [], 0.0, new stack_cas_security(), '/p/0', null);
// Avoids dealing with an error in the PRT definition that a latter part handles.
return array();
Matti Harjula
committed
}
return array_keys($compile['required']);