Commit cdece95e authored by Tim Hunt's avatar Tim Hunt
Browse files

MDL-27412 Upgrade the calculatedmulti question type to the new question engine.

parent efe3e87b
......@@ -36,6 +36,7 @@
<FIELD NAME="incorrectfeedback" TYPE="text" LENGTH="small" NOTNULL="false" SEQUENCE="false" COMMENT="Feedback shown for any incorrect response." PREVIOUS="partiallycorrectfeedbackformat" NEXT="incorrectfeedbackformat"/>
<FIELD NAME="incorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="incorrectfeedback" NEXT="answernumbering"/>
<FIELD NAME="answernumbering" TYPE="char" LENGTH="10" NOTNULL="true" DEFAULT="abc" SEQUENCE="false" COMMENT="Indicates how and whether the choices should be numbered." PREVIOUS="incorrectfeedbackformat"/>
<FIELD NAME="shownumcorrect" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" COMMENT="If true, then when the user gets a multiple-response question partially correct, tell them how many choices they got correct alongside the feedback." PREVIOUS="answernumbering"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="question"/>
......
......@@ -208,6 +208,25 @@ function xmldb_qtype_calculated_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2010020801, 'qtype', 'calculated');
}
// Add new shownumcorrect field. If this is true, then when the user gets a
// multiple-response question partially correct, tell them how many choices
// they got correct alongside the feedback.
if ($oldversion < 2011051900) {
// Define field shownumcorrect to be added to question_multichoice
$table = new xmldb_table('question_calculated_options');
$field = new xmldb_field('shownumcorrect', XMLDB_TYPE_INTEGER, '2', null,
XMLDB_NOTNULL, null, '0', 'answernumbering');
// Launch add field shownumcorrect
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// multichoice savepoint reached
upgrade_plugin_savepoint(true, 2011051900, 'qtype', 'calculated');
}
return true;
}
......
......@@ -19,7 +19,7 @@
*
* @package qtype
* @subpackage calculated
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
......@@ -32,34 +32,94 @@ require_once($CFG->dirroot . '/question/type/numerical/question.php');
/**
* Represents a calculated question.
*
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_calculated_question extends qtype_numerical_question {
class qtype_calculated_question extends qtype_numerical_question
implements qtype_calculated_question_with_expressions {
/** @var qtype_calculated_dataset_loader helper for loading the dataset. */
public $datasetloader;
/** @var qtype_calculated_variable_substituter stores the dataset we are using. */
public $vs;
public function start_attempt(question_attempt_step $step) {
$maxnumber = $this->datasetloader->get_number_of_items();
qtype_calculated_question_helper::start_attempt($this, $step);
parent::start_attempt($step);
}
public function apply_attempt_state(question_attempt_step $step) {
qtype_calculated_question_helper::apply_attempt_state($this, $step);
parent::apply_attempt_state($step);
}
public function calculate_all_expressions() {
$this->questiontext = $this->vs->replace_expressions_in_text($this->questiontext);
$this->generalfeedback = $this->vs->replace_expressions_in_text($this->generalfeedback);
foreach ($this->answers as $ans) {
if ($ans->answer && $ans->answer !== '*') {
$ans->answer = $this->vs->calculate($ans->answer,
$ans->correctanswerlength, $ans->correctanswerformat);
}
$ans->feedback = $this->vs->replace_expressions_in_text($ans->feedback,
$ans->correctanswerlength, $ans->correctanswerformat);
}
}
}
/**
* This interface defines the method that a quetsion type must implement if it
* is to work with {@link qtype_calculated_question_helper}.
*
* As well as this method, the class that implements this interface must have
* fields
* public $datasetloader; // of type qtype_calculated_dataset_loader
* public $vs; // of type qtype_calculated_variable_substituter
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface qtype_calculated_question_with_expressions {
/**
* Replace all the expression in the question definition with the values
* computed from the selected dataset by calling $this->vs->calculate() and
* $this->vs->replace_expressions_in_text() on the parts of the question
* that require it.
*/
public function calculate_all_expressions();
}
/**
* Helper class for questions that use datasets. Works with the interface
* {@link qtype_calculated_question_with_expressions} and the class
* {@link qtype_calculated_dataset_loader} to set up the value of each variable
* in start_attempt, and restore that in apply_attempt_state.
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class qtype_calculated_question_helper {
public static function start_attempt(
qtype_calculated_question_with_expressions $question, question_attempt_step $step) {
$maxnumber = $question->datasetloader->get_number_of_items();
$setnumber = rand(1, $maxnumber);
// TODO implement the $synchronizecalculated bit from create_session_and_responses.
$this->vs = new qtype_calculated_variable_substituter(
$this->datasetloader->get_values($setnumber),
$question->vs = new qtype_calculated_variable_substituter(
$question->datasetloader->get_values($setnumber),
get_string('decsep', 'langconfig'));
$this->calculate_all_expressions();
$question->calculate_all_expressions();
$step->set_qt_var('_dataset', $setnumber);
foreach ($this->vs->get_values() as $name => $value) {
foreach ($question->vs->get_values() as $name => $value) {
$step->set_qt_var('_var_' . $name, $value);
}
parent::start_attempt($step);
}
public function apply_attempt_state(question_attempt_step $step) {
public static function apply_attempt_state(
qtype_calculated_question_with_expressions $question, question_attempt_step $step) {
$values = array();
foreach ($step->get_qt_data() as $name => $value) {
if (substr($name, 0, 5) === '_var_') {
......@@ -67,27 +127,9 @@ class qtype_calculated_question extends qtype_numerical_question {
}
}
$this->vs = new qtype_calculated_variable_substituter(
$question->vs = new qtype_calculated_variable_substituter(
$values, get_string('decsep', 'langconfig'));
$this->calculate_all_expressions();
parent::apply_attempt_state($step);
}
/**
* Replace all the expression in the question definition with the values
* computed from the selected dataset.
*/
protected function calculate_all_expressions() {
$this->questiontext = $this->vs->replace_expressions_in_text($this->questiontext);
$this->generalfeedback = $this->vs->replace_expressions_in_text($this->generalfeedback);
foreach ($this->answers as $ans) {
if ($ans->answer && $ans->answer !== '*') {
$ans->answer = $this->vs->calculate($ans->answer);
}
$ans->feedback = $this->vs->replace_expressions_in_text($ans->feedback);
}
$question->calculate_all_expressions();
}
}
......@@ -233,7 +275,17 @@ class qtype_calculated_variable_substituter {
* Display a float properly formatted with a certain number of decimal places.
* @param $x
*/
public function format_float($x) {
public function format_float($x, $length = null, $format = null) {
if (!is_null($format) && !is_null($format)) {
if ($format == 1) {
// Decimal places.
$x = sprintf('%.' . $length . 'F', $x);
} else if ($format == 1) {
// Significant figures.
$x = sprintf('%.' . $length . 'g', $x);
$x = str_replace(',', '.', $x);
}
}
return str_replace('.', $this->decimalpoint, $x);
}
......@@ -298,11 +350,12 @@ class qtype_calculated_variable_substituter {
* @param string $text the text to process.
* @return string the text with values substituted.
*/
public function replace_expressions_in_text($text) {
public function replace_expressions_in_text($text, $length = null, $format = null) {
$vs = $this; // Can't see to use $this in a PHP closure.
$text = preg_replace_callback('~\{=([^{}]*(?:\{[^{}]+}[^{}]*)*)}~', function ($matches) use ($vs) {
return $vs->format_float($vs->calculate($matches[1]));
}, $text);
$text = preg_replace_callback('~\{=([^{}]*(?:\{[^{}]+}[^{}]*)*)}~',
function ($matches) use ($vs, $format, $length) {
return $vs->format_float($vs->calculate($matches[1]), $length, $format);
}, $text);
return $this->substitute_values_pretty($text);
}
......
......@@ -343,6 +343,8 @@ class qtype_calculated extends question_type {
$question, $questiondata);
foreach ($questiondata->options->answers as $a) {
$question->answers[$a->id]->tolerancetype = $a->tolerancetype;
$question->answers[$a->id]->correctanswerlength = $a->correctanswerlength;
$question->answers[$a->id]->correctanswerformat = $a->correctanswerformat;
}
$question->unitdisplay = $questiondata->options->showunits;
......@@ -771,12 +773,6 @@ class qtype_calculated extends question_type {
}
public function create_virtual_qtype() {
global $CFG;
require_once("$CFG->dirroot/question/type/numerical/questiontype.php");
return new question_numerical_qtype();
}
public function supports_dataset_item_generation() {
// Calcualted support generation of randomly distributed number data
return true;
......
......@@ -19,7 +19,7 @@
*
* @package qtype
* @subpackage calculated
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
......
......@@ -41,8 +41,7 @@ class qtype_calculated_test_helper extends question_test_helper {
}
/**
* Makes a calculated question with correct ansewer 3.14, and various incorrect
* answers with different feedback.
* Makes a calculated question about summing two numbers.
* @return qtype_calculated_question
*/
public function make_calculated_question_sum() {
......@@ -52,11 +51,17 @@ class qtype_calculated_test_helper extends question_test_helper {
$q->name = 'Simple sum';
$q->questiontext = 'What is {a} + {b}?';
$q->generalfeedback = 'Generalfeedback: {={a} + {b}} is the right answer.';
$q->answers = array(
13 => new qtype_numerical_answer(13, '{a} + {b}', 1.0, 'Very good.', FORMAT_HTML, 0),
14 => new qtype_numerical_answer(14, '{a} - {b}', 0.0, 'Add. not subtract!.', FORMAT_HTML, 0),
17 => new qtype_numerical_answer(17, '*', 0.0, 'Completely wrong.', FORMAT_HTML, 0),
);
foreach ($q->answers as $answer) {
$answer->correctanswerlength = 2;
$answer->correctanswerformat = 1;
}
$q->qtype = question_bank::get_qtype('calculated');
$q->unitdisplay = qtype_numerical::UNITNONE;
$q->unitgradingtype = 0;
......
......@@ -4,5 +4,5 @@
display: inline;
}
.que.calculated .answer input {
width: 99%;
width: 30%;
}
......@@ -25,5 +25,5 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2010090501;
$plugin->requires = 2010090501;
$plugin->version = 2011051900;
$plugin->requires = 2011051212;
......@@ -43,11 +43,11 @@ class qtype_calculatedmulti_edit_form extends question_edit_form {
public $questiondisplay;
public $initialname = '';
public $reload = false;
public function __construct($submiturl, $question, $category,
$contexts, $formeditable = true) {
global $SESSION, $CFG, $DB;
$this->question = $question;
$this->qtypeobj = $QTYPES[$this->question->qtype];
$this->qtypeobj = question_bank::get_qtype('calculatedmulti');
if (1 == optional_param('reload', '', PARAM_INT)) {
$this->reload = true;
} else {
......@@ -105,13 +105,7 @@ class qtype_calculatedmulti_edit_form extends question_edit_form {
return $repeated;
}
/**
* Add question-type specific form fields.
*
* @param MoodleQuickForm $mform the form being built.
*/
protected function definition_inner($mform) {
$this->qtypeobj = $QTYPES[$this->qtype()];
$label = get_string('sharedwildcards', 'qtype_calculated');
$mform->addElement('hidden', 'initialcategory', 1);
......@@ -148,14 +142,9 @@ class qtype_calculatedmulti_edit_form extends question_edit_form {
$mform->addHelpButton('shuffleanswers', 'shuffleanswers', 'qtype_multichoice');
$mform->setDefault('shuffleanswers', 1);
$numberingoptions = $QTYPES['multichoice']->get_numbering_styles();
$menu = array();
foreach ($numberingoptions as $numberingoption) {
$menu[$numberingoption] = get_string(
'answernumbering' . $numberingoption, 'qtype_multichoice');
}
$numberingoptions = question_bank::get_qtype('multichoice')->get_numbering_styles();
$mform->addElement('select', 'answernumbering',
get_string('answernumbering', 'qtype_multichoice'), $menu);
get_string('answernumbering', 'qtype_multichoice'), $numberingoptions);
$mform->setDefault('answernumbering', 'abc');
$creategrades = get_grade_options();
......@@ -301,10 +290,6 @@ class qtype_calculatedmulti_edit_form extends question_edit_form {
return $question;
}
public function qtype() {
return 'calculatedmulti';
}
public function validation($data, $files) {
$errors = parent::validation($data, $files);
......@@ -431,4 +416,8 @@ class qtype_calculatedmulti_edit_form extends question_edit_form {
}
return $errors;
}
public function qtype() {
return 'calculatedmulti';
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Calculated multiple-choice question definition class.
*
* @package qtype
* @subpackage calculatedmulti
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/multichoice/question.php');
require_once($CFG->dirroot . '/question/type/calculated/question.php');
/**
* Represents a calculated multiple-choice multiple-response question.
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_calculatedmulti_single_question extends qtype_multichoice_single_question
implements qtype_calculated_question_with_expressions {
/** @var qtype_calculated_dataset_loader helper for loading the dataset. */
public $datasetloader;
/** @var qtype_calculated_variable_substituter stores the dataset we are using. */
public $vs;
public function start_attempt(question_attempt_step $step) {
qtype_calculated_question_helper::start_attempt($this, $step);
parent::start_attempt($step);
}
public function apply_attempt_state(question_attempt_step $step) {
qtype_calculated_question_helper::apply_attempt_state($this, $step);
parent::apply_attempt_state($step);
}
public function calculate_all_expressions() {
qtype_calculatedmulti_calculate_helper::calculate_all_expressions($this);
}
}
/**
* Represents a calculated multiple-choice multiple-response question.
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_calculatedmulti_multi_question extends qtype_multichoice_multi_question
implements qtype_calculated_question_with_expressions {
/** @var qtype_calculated_dataset_loader helper for loading the dataset. */
public $datasetloader;
/** @var qtype_calculated_variable_substituter stores the dataset we are using. */
public $vs;
public function start_attempt(question_attempt_step $step) {
qtype_calculated_question_helper::start_attempt($this, $step);
parent::start_attempt($step);
}
public function apply_attempt_state(question_attempt_step $step) {
qtype_calculated_question_helper::apply_attempt_state($this, $step);
parent::apply_attempt_state($step);
}
public function calculate_all_expressions() {
qtype_calculatedmulti_calculate_helper::calculate_all_expressions($this);
}
}
/**
* Helper to abstract common code between qtype_calculatedmulti_single_question
* and qtype_calculatedmulti_multi_question.
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class qtype_calculatedmulti_calculate_helper {
/**
* Calculate all the exressions in a qtype_calculatedmulti_single_question
* or qtype_calculatedmulti_multi_question.
* @param unknown_type $question
*/
public static function calculate_all_expressions(
qtype_calculated_question_with_expressions $question) {
$question->questiontext = $question->vs->replace_expressions_in_text(
$question->questiontext);
$question->generalfeedback = $question->vs->replace_expressions_in_text(
$question->generalfeedback);
foreach ($question->answers as $ans) {
if ($ans->answer && $ans->answer !== '*') {
$ans->answer = $question->vs->replace_expressions_in_text($ans->answer,
$ans->correctanswerlength, $ans->correctanswerformat);
}
$ans->feedback = $question->vs->replace_expressions_in_text($ans->feedback,
$ans->correctanswerlength, $ans->correctanswerformat);
}
}
}
\ No newline at end of file
......@@ -26,6 +26,9 @@
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/multichoice/questiontype.php');
require_once($CFG->dirroot . '/question/type/calculated/questiontype.php');
/**
* The calculated multiple-choice question type.
......@@ -37,7 +40,6 @@ class qtype_calculatedmulti extends qtype_calculated {
// Used by the function custom_generator_tools:
public $calcgenerateidhasbeenadded = false;
public $virtualqtype = false;
public function requires_qtypes() {
return array('calculated', 'multichoice');
......@@ -100,13 +102,6 @@ class qtype_calculatedmulti extends qtype_calculated {
$oldoptions = array();
}
// Save the units.
$virtualqtype = $this->get_virtual_qtype($question);
if (isset($result->error)) {
return $result;
} else {
$units = &$result->units;
}
// Insert all the new answers
if (isset($question->answer) && !isset($question->answers)) {
$question->answers = $question->answer;
......@@ -195,108 +190,36 @@ class qtype_calculatedmulti extends qtype_calculated {
return true;
}
public function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
// Find out how many datasets are available
global $CFG, $DB, $OUTPUT;
$maxnumber = (int)$DB->get_field_sql(
"SELECT MIN(a.itemcount)
FROM {question_dataset_definitions} a, {question_datasets} b
WHERE b.question = ? AND a.id = b.datasetdefinition", array($question->id));
if (!$maxnumber) {
print_error('cannotgetdsforquestion', 'question', '', $question->id);
}
$sql = "SELECT i.*
FROM {question_datasets} d, {question_dataset_definitions} i
WHERE d.question = ? AND d.datasetdefinition = i.id AND i.category != 0";
if (!$question->options->synchronize || !$records = $DB->get_records_sql($sql,
array($question->id))) {
$synchronize_calculated = false;
protected function make_question_instance($questiondata) {
question_bank::load_question_definition_classes($this->name());
if ($questiondata->options->single) {
$class = 'qtype_calculatedmulti_single_question';
} else {
// i.e records is true so test coherence
$coherence = true;
$a = new stdClass();
$a->qid = $question->id;
$a->qcat = $question->category;
foreach ($records as $def) {
if ($def->category != $question->category) {
$a->name = $def->name;
$a->sharedcat = $def->category;
$coherence = false;
break;
}
}
if (!$coherence) {
echo $OUTPUT->notification(
get_string('nocoherencequestionsdatyasetcategory', 'qtype_calculated', $a));
}
$synchronize_calculated = true;
$class = 'qtype_calculatedmulti_multi_question';
}
return new $class();
}
// Choose a random dataset
// maxnumber sould not be breater than 100
if ($maxnumber > qtype_calculated::MAX_DATASET_ITEMS) {
$maxnumber = qtype_calculated::MAX_DATASET_ITEMS;
}
if ($synchronize_calculated === false) {
$state->options->datasetitem = rand(1, $maxnumber);
} else {
$state->options->datasetitem =
intval($maxnumber * substr($attempt->timestart, -2) /100);
if ($state->options->datasetitem < 1) {
$state->options->datasetitem =1;
} else if ($state->options->datasetitem > $maxnumber) {
$state->options->datasetitem = $maxnumber;
}
protected function initialise_question_instance(question_definition $question, $questiondata) {
question_type::initialise_question_instance($question, $questiondata);
};
$state->options->dataset =
$this->pick_question_dataset($question, $state->options->datasetitem);
// create an array of answerids ??? why so complicated ???
$answerids = array_values(array_map(create_function('$val',
'return $val->id;'), $question->options->answers));
// Shuffle the answers if required
if (!empty($cmoptions->shuffleanswers) and !empty($question->options->shuffleanswers)) {
$answerids = swapshuffle($answerids);
}
$state->options->order = $answerids;
// Create empty responses
if ($question->options->single) {
$state->responses = array('' => '');
$question->shuffleanswers = $questiondata->options->shuffleanswers;
$question->answernumbering = $questiondata->options->answernumbering;
if (!empty($questiondata->options->layout)) {
$question->layout = $questiondata->options->layout;
} else {
$state->responses = array();
$question->layout = qtype_multichoice_single_question::LAYOUT_VERTICAL;
}
return true;
}
public function save_session_and_responses(&$question, &$state) {