From cdece95ea45e4a060a2859e3c0d9390f2eb824ab Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 19 May 2011 17:14:09 +0100 Subject: [PATCH] MDL-27412 Upgrade the calculatedmulti question type to the new question engine. --- question/type/calculated/db/install.xml | 1 + question/type/calculated/db/upgrade.php | 19 ++ question/type/calculated/question.php | 137 +++++++---- question/type/calculated/questiontype.php | 8 +- question/type/calculated/renderer.php | 2 +- .../type/calculated/simpletest/helper.php | 9 +- question/type/calculated/styles.css | 2 +- question/type/calculated/version.php | 4 +- .../edit_calculatedmulti_form.php | 27 +-- question/type/calculatedmulti/question.php | 122 ++++++++++ .../type/calculatedmulti/questiontype.php | 221 +++--------------- question/type/calculatedmulti/renderer.php | 39 ++++ .../calculatedmulti/simpletest/helper.php | 98 ++++++++ .../simpletest/testquestion.php | 111 +++++++++ .../simpletest/testwalkthrough.php | 104 +++++++++ question/type/calculatedmulti/styles.css | 13 ++ question/type/calculatedmulti/version.php | 29 +++ .../edit_calculatedsimple_form.php | 1 - .../calculatedsimple/simpletest/helper.php | 11 +- question/type/calculatedsimple/styles.css | 8 + question/type/calculatedsimple/version.php | 29 +++ question/type/multichoice/questiontype.php | 1 + 22 files changed, 726 insertions(+), 270 deletions(-) create mode 100644 question/type/calculatedmulti/question.php create mode 100644 question/type/calculatedmulti/renderer.php create mode 100644 question/type/calculatedmulti/simpletest/helper.php create mode 100644 question/type/calculatedmulti/simpletest/testquestion.php create mode 100644 question/type/calculatedmulti/simpletest/testwalkthrough.php create mode 100644 question/type/calculatedmulti/styles.css create mode 100644 question/type/calculatedmulti/version.php create mode 100644 question/type/calculatedsimple/styles.css create mode 100644 question/type/calculatedsimple/version.php diff --git a/question/type/calculated/db/install.xml b/question/type/calculated/db/install.xml index 3bd701147e5..21290f7bf13 100644 --- a/question/type/calculated/db/install.xml +++ b/question/type/calculated/db/install.xml @@ -36,6 +36,7 @@ + diff --git a/question/type/calculated/db/upgrade.php b/question/type/calculated/db/upgrade.php index a9c2846c7d2..40b228fbd09 100644 --- a/question/type/calculated/db/upgrade.php +++ b/question/type/calculated/db/upgrade.php @@ -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; } diff --git a/question/type/calculated/question.php b/question/type/calculated/question.php index 8f1b11bf83e..5a29da63b24 100644 --- a/question/type/calculated/question.php +++ b/question/type/calculated/question.php @@ -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(); - $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), - get_string('decsep', 'langconfig')); - $this->calculate_all_expressions(); - - $step->set_qt_var('_dataset', $setnumber); - foreach ($this->vs->get_values() as $name => $value) { - $step->set_qt_var('_var_' . $name, $value); - } - + 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. + + $question->vs = new qtype_calculated_variable_substituter( + $question->datasetloader->get_values($setnumber), + get_string('decsep', 'langconfig')); + $question->calculate_all_expressions(); + + $step->set_qt_var('_dataset', $setnumber); + foreach ($question->vs->get_values() as $name => $value) { + $step->set_qt_var('_var_' . $name, $value); + } + } + + 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); } diff --git a/question/type/calculated/questiontype.php b/question/type/calculated/questiontype.php index 90db114e938..fdbd0c84d39 100644 --- a/question/type/calculated/questiontype.php +++ b/question/type/calculated/questiontype.php @@ -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; diff --git a/question/type/calculated/renderer.php b/question/type/calculated/renderer.php index b3aca3074af..28a23135742 100644 --- a/question/type/calculated/renderer.php +++ b/question/type/calculated/renderer.php @@ -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 */ diff --git a/question/type/calculated/simpletest/helper.php b/question/type/calculated/simpletest/helper.php index 0c059cb81f8..4fdc0fc20ca 100644 --- a/question/type/calculated/simpletest/helper.php +++ b/question/type/calculated/simpletest/helper.php @@ -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; diff --git a/question/type/calculated/styles.css b/question/type/calculated/styles.css index ae7a4867c7d..a242e84d9ba 100644 --- a/question/type/calculated/styles.css +++ b/question/type/calculated/styles.css @@ -4,5 +4,5 @@ display: inline; } .que.calculated .answer input { - width: 99%; + width: 30%; } diff --git a/question/type/calculated/version.php b/question/type/calculated/version.php index 1022ad5a862..47305329975 100644 --- a/question/type/calculated/version.php +++ b/question/type/calculated/version.php @@ -25,5 +25,5 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2010090501; -$plugin->requires = 2010090501; +$plugin->version = 2011051900; +$plugin->requires = 2011051212; diff --git a/question/type/calculatedmulti/edit_calculatedmulti_form.php b/question/type/calculatedmulti/edit_calculatedmulti_form.php index 725ea185c5a..e44c492f300 100644 --- a/question/type/calculatedmulti/edit_calculatedmulti_form.php +++ b/question/type/calculatedmulti/edit_calculatedmulti_form.php @@ -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'; + } } diff --git a/question/type/calculatedmulti/question.php b/question/type/calculatedmulti/question.php new file mode 100644 index 00000000000..d3cd12bdf3e --- /dev/null +++ b/question/type/calculatedmulti/question.php @@ -0,0 +1,122 @@ +. + +/** + * 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 diff --git a/question/type/calculatedmulti/questiontype.php b/question/type/calculatedmulti/questiontype.php index 677598d7483..8861f1d25bb 100644 --- a/question/type/calculatedmulti/questiontype.php +++ b/question/type/calculatedmulti/questiontype.php @@ -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); + protected function initialise_question_instance(question_definition $question, $questiondata) { + question_type::initialise_question_instance($question, $questiondata); + + $question->shuffleanswers = $questiondata->options->shuffleanswers; + $question->answernumbering = $questiondata->options->answernumbering; + if (!empty($questiondata->options->layout)) { + $question->layout = $questiondata->options->layout; } 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; - } - - }; - $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); + $question->layout = qtype_multichoice_single_question::LAYOUT_VERTICAL; } - $state->options->order = $answerids; - // Create empty responses - if ($question->options->single) { - $state->responses = array('' => ''); - } else { - $state->responses = array(); + + $this->initialise_combined_feedback($question, $questiondata, true); + $this->initialise_question_answers($question, $questiondata); + + foreach ($questiondata->options->answers as $a) { + $question->answers[$a->id]->correctanswerlength = $a->correctanswerlength; + $question->answers[$a->id]->correctanswerformat = $a->correctanswerformat; } - return true; - } - public function save_session_and_responses(&$question, &$state) { - global $DB; - $responses = 'dataset'.$state->options->datasetitem.'-'; - $responses .= implode(',', $state->options->order) . ':'; - $responses .= implode(',', $state->responses); - - // Set the legacy answer field - $DB->set_field('question_states', 'answer', $responses, array('id'=> $state->id)); - return true; - } - - public function get_default_numerical_unit($question, $virtualqtype) { - $unit = ''; - return $unit; - } - - public function grade_responses(&$question, &$state, $cmoptions) { - // Forward the grading to the virtual qtype - // We modify the question to look like a multichoice question - // for grading nothing to do - $virtualqtype = $this->get_virtual_qtype($question); - return $virtualqtype->grade_responses($question, $state, $cmoptions); - } - - public function create_virtual_qtype() { - global $CFG; - require_once("$CFG->dirroot/question/type/multichoice/questiontype.php"); - return new question_multichoice_qtype(); + $question->datasetloader = new qtype_calculated_dataset_loader($questiondata->id); } public function comment_header($question) { @@ -325,8 +248,6 @@ class qtype_calculatedmulti extends qtype_calculated { $comment->answers = array(); $answers = fullclone($answers); - $strmin = get_string('min'); - $strmax = get_string('max'); $errors = ''; $delimiter = ': '; foreach ($answers as $key => $answer) { @@ -355,62 +276,8 @@ class qtype_calculatedmulti extends qtype_calculated { return fullclone($comment); } - public function get_correct_responses1(&$question, &$state) { - $virtualqtype = $this->get_vir(al_qtype($question)); - - return $virtualqtype->get_correct_responses($question, $state); - } - public function get_virtual_qtype() { - $this->virtualqtype = $QTYPES['multichoice']; - return $this->virtualqtype; - } - - /** - * Runs all the code required to set up and save an essay question for testing purposes. - * Alternate DB table prefix may be used to facilitate data deletion. - */ - public function generate_test($name, $courseid = null) { - global $DB; - list($form, $question) = parent::generate_test($name, $courseid); - $form->feedback = 1; - $form->multiplier = array(1, 1); - $form->shuffleanswers = 1; - $form->noanswers = 1; - $form->qtype ='calculatedmulti'; - $question->qtype ='calculatedmulti'; - $form->answers = array('{a} + {b}'); - $form->fraction = array(1); - $form->tolerance = array(0.01); - $form->tolerancetype = array(1); - $form->correctanswerlength = array(2); - $form->correctanswerformat = array(1); - $form->questiontext = "What is {a} + {b}?"; - - if ($courseid) { - $course = $DB->get_record('course', array('id'=> $courseid)); - } - - $new_question = $this->save_question($question, $form); - - $dataset_form = new stdClass(); - $dataset_form->nextpageparam['forceregeneration']= 1; - $dataset_form->calcmin = array(1 => 1.0, 2 => 1.0); - $dataset_form->calcmax = array(1 => 10.0, 2 => 10.0); - $dataset_form->calclength = array(1 => 1, 2 => 1); - $dataset_form->number = array(1 => 5.4, 2 => 4.9); - $dataset_form->itemid = array(1 => '', 2 => ''); - $dataset_form->calcdistribution = array(1 => 'uniform', 2 => 'uniform'); - $dataset_form->definition = array(1 => "1-0-a", 2 => "1-0-b"); - $dataset_form->nextpageparam = array('forceregeneration' => false); - $dataset_form->addbutton = 1; - $dataset_form->selectadd = 1; - $dataset_form->courseid = $courseid; - $dataset_form->cmid = 0; - $dataset_form->id = $new_question->id; - $this->save_dataset_items($new_question, $dataset_form); - - return $new_question; + return question_bank::get_qtype('multichoice'); } public function move_files($questionid, $oldcontextid, $newcontextid) { @@ -432,6 +299,7 @@ class qtype_calculatedmulti extends qtype_calculated { parent::delete_files($questionid, $contextid); $this->delete_files_in_answers($questionid, $contextid, true); + $fs->delete_area_files($contextid, 'qtype_calculatedmulti', 'correctfeedback', $questionid); $fs->delete_area_files($contextid, 'qtype_calculatedmulti', @@ -439,37 +307,4 @@ class qtype_calculatedmulti extends qtype_calculated { $fs->delete_area_files($contextid, 'qtype_calculatedmulti', 'incorrectfeedback', $questionid); } - - public function check_file_access($question, $state, $options, $contextid, $component, - $filearea, $args) { - $itemid = reset($args); - - if (empty($question->maxgrade)) { - $question->maxgrade = $question->defaultgrade; - } - - if (in_array($filearea, array('correctfeedback', 'partiallycorrectfeedback', - 'incorrectfeedback'))) { - $result = $options->feedback && ($itemid == $question->id); - if (!$result) { - return false; - } - if ($state->raw_grade >= $question->maxgrade/1.01) { - $feedbacktype = 'correctfeedback'; - } else if ($state->raw_grade > 0) { - $feedbacktype = 'partiallycorrectfeedback'; - } else { - $feedbacktype = 'incorrectfeedback'; - } - if ($feedbacktype != $filearea) { - return false; - } - return true; - } else if ($component == 'question' && $filearea == 'answerfeedback') { - return $options->feedback && array_key_exists($itemid, $question->options->answers); - } else { - return parent::check_file_access($question, $state, $options, $contextid, $component, - $filearea, $args); - } - } } diff --git a/question/type/calculatedmulti/renderer.php b/question/type/calculatedmulti/renderer.php new file mode 100644 index 00000000000..afb577f782e --- /dev/null +++ b/question/type/calculatedmulti/renderer.php @@ -0,0 +1,39 @@ +. + +/** + * Calculated multiple-choice question renderer 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/renderer.php'); + + +/** + * Generates the output for calculated multiple-choice questions. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_calculatedmulti_renderer extends qtype_numerical_renderer { +} diff --git a/question/type/calculatedmulti/simpletest/helper.php b/question/type/calculatedmulti/simpletest/helper.php new file mode 100644 index 00000000000..ff81c3d17f9 --- /dev/null +++ b/question/type/calculatedmulti/simpletest/helper.php @@ -0,0 +1,98 @@ +. + +/** + * Test helpers for the calculated question type. + * + * @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/calculated/question.php'); + + +/** + * Test helper class for the calculated multiple-choice question type. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_calculatedmulti_test_helper extends question_test_helper { + public function get_test_questions() { + return array('sum'); + } + + /** + * Makes a calculated multiple-choice question about summing two numbers. + * @return qtype_calculatedmulti_question + */ + public function make_calculatedmulti_question_sum() { + // TODO + question_bank::load_question_definition_classes('calculated'); + $q = new qtype_calculatedmulti_question(); + test_question_maker::initialise_a_question($q); + $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), + ); + $q->qtype = question_bank::get_qtype('calculated'); + $q->unitdisplay = qtype_numerical::UNITNONE; + $q->unitgradingtype = 0; + $q->unitpenalty = 0; + $q->ap = new qtype_numerical_answer_processor(array()); + + $q->datasetloader = new qtype_calculated_test_dataset_loader(0, array( + array('a' => 1, 'b' => 5), + array('a' => 3, 'b' => 4), + )); + + return $q; + } +} + + +/** + * Test implementation of {@link qtype_calculated_dataset_loader}. Gets the values + * from an array passed to the constructor, rather than querying the database. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_calculated_test_dataset_loader extends qtype_calculated_dataset_loader{ + protected $valuesets; + + public function __construct($questionid, array $valuesets) { + parent::__construct($questionid); + $this->valuesets = $valuesets; + } + + public function get_number_of_items() { + return count($this->valuesets); + } + + public function load_values($itemnumber) { + return $this->valuesets[$itemnumber - 1]; + } +} diff --git a/question/type/calculatedmulti/simpletest/testquestion.php b/question/type/calculatedmulti/simpletest/testquestion.php new file mode 100644 index 00000000000..f661fd57cb9 --- /dev/null +++ b/question/type/calculatedmulti/simpletest/testquestion.php @@ -0,0 +1,111 @@ +. + + +/** + * Unit tests for the 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 + */ + + +require_once($CFG->dirroot . '/question/engine/simpletest/helpers.php'); + + +/** + * Unit tests for qtype_calculatedmulti_definition. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_calculatedmulti_question_test extends UnitTestCase { + public function test_is_complete_response() { + $question = test_question_maker::make_question('calculated'); + + $this->assertFalse($question->is_complete_response(array())); + $this->assertTrue($question->is_complete_response(array('answer' => '0'))); + $this->assertTrue($question->is_complete_response(array('answer' => 0))); + $this->assertFalse($question->is_complete_response(array('answer' => 'test'))); + } + + public function test_is_gradable_response() { + $question = test_question_maker::make_question('calculated'); + + $this->assertFalse($question->is_gradable_response(array())); + $this->assertTrue($question->is_gradable_response(array('answer' => '0'))); + $this->assertTrue($question->is_gradable_response(array('answer' => 0))); + $this->assertTrue($question->is_gradable_response(array('answer' => 'test'))); + } + + public function test_grading() { + $question = test_question_maker::make_question('calculated'); + $question->start_attempt(new question_attempt_step()); + $values = $question->vs->get_values(); + + $this->assertEqual(array(0, question_state::$gradedwrong), + $question->grade_response(array('answer' => $values['a'] - $values['b']))); + $this->assertEqual(array(1, question_state::$gradedright), + $question->grade_response(array('answer' => $values['a'] + $values['b']))); + } + + public function test_get_correct_response() { + $question = test_question_maker::make_question('calculated'); + $question->start_attempt(new question_attempt_step()); + $values = $question->vs->get_values(); + + $this->assertEqual(array('answer' => $values['a'] + $values['b']), + $question->get_correct_response()); + } + + public function test_get_question_summary() { + $question = test_question_maker::make_question('calculated'); + $question->start_attempt(new question_attempt_step()); + $values = $question->vs->get_values(); + + $qsummary = $question->get_question_summary(); + $this->assertEqual('What is ' . $values['a'] . ' + ' . $values['b'] . '?', $qsummary); + } + + public function test_summarise_response() { + $question = test_question_maker::make_question('calculated'); + $question->start_attempt(new question_attempt_step()); + $values = $question->vs->get_values(); + + $this->assertEqual('3.1', $question->summarise_response(array('answer' => '3.1'))); + } + + public function test_classify_response() { + $question = test_question_maker::make_question('calculated'); + $question->start_attempt(new question_attempt_step()); + $values = $question->vs->get_values(); + + $this->assertEqual(array( + new question_classified_response(13, $values['a'] + $values['b'], 1.0)), + $question->classify_response(array('answer' => $values['a'] + $values['b']))); + $this->assertEqual(array( + new question_classified_response(14, $values['a'] - $values['b'], 0.0)), + $question->classify_response(array('answer' => $values['a'] - $values['b']))); + $this->assertEqual(array( + new question_classified_response(17, 7 * $values['a'], 0.0)), + $question->classify_response(array('answer' => 7 * $values['a']))); + $this->assertEqual(array( + question_classified_response::no_response()), + $question->classify_response(array('answer' => ''))); + } +} \ No newline at end of file diff --git a/question/type/calculatedmulti/simpletest/testwalkthrough.php b/question/type/calculatedmulti/simpletest/testwalkthrough.php new file mode 100644 index 00000000000..847021358c6 --- /dev/null +++ b/question/type/calculatedmulti/simpletest/testwalkthrough.php @@ -0,0 +1,104 @@ +. + +/** + * This file contains overall tests of calculated multiple-choice questions. + * + * @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/engine/simpletest/helpers.php'); + + +/** + * Unit tests for the calculated multiple-choice question type. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_calculatedmulti_walkthrough_test extends qbehaviour_walkthrough_test_base { + public function test_interactive() { + + // Create a gapselect question. + $q = test_question_maker::make_question('calculated'); + $q->hints = array( + new question_hint(1, 'This is the first hint.', FORMAT_HTML), + new question_hint(2, 'This is the second hint.', FORMAT_HTML), + ); + $this->start_attempt_at_question($q, 'interactive', 3); + $values = $q->vs->get_values(); + + // Check the initial state. + $this->check_current_state(question_state::$todo); + $this->check_current_mark(null); + $this->check_current_output( + $this->get_contains_marked_out_of_summary(), + $this->get_contains_submit_button_expectation(true), + $this->get_does_not_contain_feedback_expectation(), + $this->get_does_not_contain_validation_error_expectation(), + $this->get_does_not_contain_try_again_button_expectation(), + $this->get_no_hint_visible_expectation()); + + // Submit blank. + $this->process_submission(array('-submit' => 1, 'answer' => '')); + + // Verify. + $this->check_current_state(question_state::$invalid); + $this->check_current_mark(null); + $this->check_current_output( + $this->get_contains_marked_out_of_summary(), + $this->get_contains_submit_button_expectation(true), + $this->get_does_not_contain_feedback_expectation(), + $this->get_contains_validation_error_expectation(), + $this->get_does_not_contain_try_again_button_expectation(), + $this->get_no_hint_visible_expectation()); + + // Sumit something that does not look like a number. + $this->process_submission(array('-submit' => 1, 'answer' => 'newt')); + + // Verify. + $this->check_current_state(question_state::$invalid); + $this->check_current_mark(null); + $this->check_current_output( + $this->get_contains_marked_out_of_summary(), + $this->get_contains_submit_button_expectation(true), + $this->get_does_not_contain_feedback_expectation(), + $this->get_contains_validation_error_expectation(), + new PatternExpectation('/' . + preg_quote(get_string('invalidnumber', 'qtype_numerical') . '/')), + $this->get_does_not_contain_try_again_button_expectation(), + $this->get_no_hint_visible_expectation()); + + // Now get it right. + $this->process_submission(array('-submit' => 1, 'answer' => $values['a'] + $values['b'])); + + // Verify. + $this->check_current_state(question_state::$gradedright); + $this->check_current_mark(3); + $this->check_current_output( + $this->get_contains_mark_summary(3), + $this->get_contains_submit_button_expectation(false), + $this->get_contains_correct_expectation(), + $this->get_does_not_contain_validation_error_expectation(), + $this->get_no_hint_visible_expectation()); + } +} diff --git a/question/type/calculatedmulti/styles.css b/question/type/calculatedmulti/styles.css new file mode 100644 index 00000000000..75ce4f98a49 --- /dev/null +++ b/question/type/calculatedmulti/styles.css @@ -0,0 +1,13 @@ +.que.calculatedmulti .answer .specificfeedback { + display: inline; + padding: 0 0.7em; + background: #FFF3BF; +} +.que.calculatedmulti .answer .specificfeedback * { + display: inline; + background: #FFF3BF; +} +.que.calculatedmulti .answer div.r0, +.que.calculatedmulti .answer div.r1 { + padding: 0.3em; +} diff --git a/question/type/calculatedmulti/version.php b/question/type/calculatedmulti/version.php new file mode 100644 index 00000000000..27772b0fc86 --- /dev/null +++ b/question/type/calculatedmulti/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version information for the calculated multiple-choice question type. + * + * @package qtype + * @subpackage calculatedmulti + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2011051900; +$plugin->requires = 2011051212; diff --git a/question/type/calculatedsimple/edit_calculatedsimple_form.php b/question/type/calculatedsimple/edit_calculatedsimple_form.php index 8a79143851a..bdde0469ccc 100644 --- a/question/type/calculatedsimple/edit_calculatedsimple_form.php +++ b/question/type/calculatedsimple/edit_calculatedsimple_form.php @@ -70,7 +70,6 @@ class qtype_calculatedsimple_edit_form extends qtype_calculated_edit_form { public $formdata = array(); public function __construct($submiturl, $question, $category, $contexts, $formeditable = true) { - global $SESSION, $CFG, $DB; $this->regenerate = true; $this->question = $question; diff --git a/question/type/calculatedsimple/simpletest/helper.php b/question/type/calculatedsimple/simpletest/helper.php index 8ae41adef98..704d97964a2 100644 --- a/question/type/calculatedsimple/simpletest/helper.php +++ b/question/type/calculatedsimple/simpletest/helper.php @@ -41,9 +41,8 @@ class qtype_calculatedsimple_test_helper extends question_test_helper { } /** - * Makes a calculated question with correct ansewer 3.14, and various incorrect - * answers with different feedback. - * @return qtype_calculated_question + * Makes a simple calculated question about summing two numbers. + * @return qtype_calculatedsimple_question */ public function make_calculatedsimple_question_sum() { question_bank::load_question_definition_classes('calculatedsimple'); @@ -52,11 +51,17 @@ class qtype_calculatedsimple_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; diff --git a/question/type/calculatedsimple/styles.css b/question/type/calculatedsimple/styles.css new file mode 100644 index 00000000000..965f2edd6c9 --- /dev/null +++ b/question/type/calculatedsimple/styles.css @@ -0,0 +1,8 @@ +.que.calculatedsimple .answer { + padding: 0.3em; + width: auto; + display: inline; +} +.que.calculatedsimple .answer input { + width: 30%; +} diff --git a/question/type/calculatedsimple/version.php b/question/type/calculatedsimple/version.php new file mode 100644 index 00000000000..8e15542ca7f --- /dev/null +++ b/question/type/calculatedsimple/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version information for the simple calculated question type. + * + * @package qtype + * @subpackage calculatedsimple + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2011051900; +$plugin->requires = 2011051212; diff --git a/question/type/multichoice/questiontype.php b/question/type/multichoice/questiontype.php index ebef5c9d666..ae77b50a726 100644 --- a/question/type/multichoice/questiontype.php +++ b/question/type/multichoice/questiontype.php @@ -252,6 +252,7 @@ class qtype_multichoice extends question_type { parent::delete_files($questionid, $contextid); $this->delete_files_in_answers($questionid, $contextid, true); + $fs->delete_area_files($contextid, 'qtype_multichoice', 'correctfeedback', $questionid); $fs->delete_area_files($contextid,