From 667cdde36144f7ee4d01b0f45f20fdcf084c375a Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Fri, 20 May 2011 18:22:35 +0100 Subject: [PATCH] MDL-20636 Implement upgrade of calculated* question type attempts. --- .../engine/upgrade/behaviourconverters.php | 15 +- question/engine/upgrade/simpletest/helper.php | 14 + question/engine/upgrade/upgradelib.php | 33 +- .../db/simpletest/testupgradelibnewqe.php | 697 +++++++++++++++ question/type/calculated/db/upgradelib.php | 284 +++++++ .../db/simpletest/testupgradelibnewqe.php | 802 ++++++++++++++++++ .../type/calculatedmulti/db/upgradelib.php | 308 +++++++ .../db/simpletest/testupgradelibnewqe.php | 634 ++++++++++++++ .../type/calculatedsimple/db/upgradelib.php | 42 + 9 files changed, 2821 insertions(+), 8 deletions(-) create mode 100644 question/type/calculated/db/simpletest/testupgradelibnewqe.php create mode 100644 question/type/calculated/db/upgradelib.php create mode 100644 question/type/calculatedmulti/db/simpletest/testupgradelibnewqe.php create mode 100644 question/type/calculatedmulti/db/upgradelib.php create mode 100644 question/type/calculatedsimple/db/simpletest/testupgradelibnewqe.php create mode 100644 question/type/calculatedsimple/db/upgradelib.php diff --git a/question/engine/upgrade/behaviourconverters.php b/question/engine/upgrade/behaviourconverters.php index 5b3431d0e5f..2908ef3e4d2 100644 --- a/question/engine/upgrade/behaviourconverters.php +++ b/question/engine/upgrade/behaviourconverters.php @@ -47,12 +47,14 @@ abstract class question_behaviour_attempt_updater { protected $qtypeupdater; /** @var question_engine_assumption_logger */ protected $logger; + /** @var question_engine_attempt_upgrader */ + protected $qeupdater; /** * @var object this is the data for the upgraded questions attempt that * we are building. */ - protected $qa; + public $qa; /** @var object the quiz settings. */ protected $quiz; @@ -73,13 +75,14 @@ abstract class question_behaviour_attempt_updater { /** @var object pointer to the state that has already finished this attempt. */ protected $finishstate; - public function __construct($quiz, $attempt, $question, $qsession, $qstates, $logger) { + public function __construct($quiz, $attempt, $question, $qsession, $qstates, $logger, $qeupdater) { $this->quiz = $quiz; $this->attempt = $attempt; $this->question = $question; $this->qsession = $qsession; $this->qstates = $qstates; $this->logger = $logger; + $this->qeupdater = $qeupdater; } public function discard() { @@ -93,6 +96,7 @@ abstract class question_behaviour_attempt_updater { $this->qtypeupdater->discard(); $this->qtypeupdater = null; $this->logger = null; + $this->qeupdater = null; } protected abstract function behaviour_name(); @@ -128,11 +132,11 @@ abstract class question_behaviour_attempt_updater { $qa = new stdClass(); $qa->questionid = $this->question->id; $qa->behaviour = $this->behaviour_name(); + $qa->questionsummary = $this->qtypeupdater->question_summary($this->question); + $qa->rightanswer = $this->qtypeupdater->right_answer($this->question); $qa->maxmark = $this->question->maxmark; $qa->minfraction = 0; $qa->flagged = 0; - $qa->questionsummary = $this->qtypeupdater->question_summary($this->question); - $qa->rightanswer = $this->qtypeupdater->right_answer($this->question); $qa->responsesummary = ''; $qa->timemodified = 0; $qa->steps = array(); @@ -271,7 +275,7 @@ abstract class question_behaviour_attempt_updater { is missing important code (the class {$class}) required to run the upgrade to the new question engine."); } - return new $class($this, $this->question, $this->logger); + return new $class($this, $this->question, $this->logger, $this->qeupdater); } public function to_text($html) { @@ -438,6 +442,7 @@ class qbehaviour_adaptive_converter extends question_behaviour_attempt_updater { } protected function finish_up() { + parent::finish_up(); if ($this->finishstate || !$this->attempt->timefinish) { return; } diff --git a/question/engine/upgrade/simpletest/helper.php b/question/engine/upgrade/simpletest/helper.php index b8ae742584d..044268fb16b 100644 --- a/question/engine/upgrade/simpletest/helper.php +++ b/question/engine/upgrade/simpletest/helper.php @@ -88,6 +88,20 @@ class test_question_engine_upgrade_question_loader extends question_engine_upgra return null; } + + public function put_dataset_in_cache($questionid, $selecteditem, $dataset) { + $this->datasetcache[$questionid][$selecteditem] = $dataset; + } + + public function load_dataset($questionid, $selecteditem) { + global $DB; + + if (isset($this->datasetcache[$questionid][$selecteditem])) { + return $this->datasetcache[$questionid][$selecteditem]; + } + + throw new coding_exception('Test dataset not loaded.'); + } } diff --git a/question/engine/upgrade/upgradelib.php b/question/engine/upgrade/upgradelib.php index d4e9e7d9785..e60c7b8f760 100644 --- a/question/engine/upgrade/upgradelib.php +++ b/question/engine/upgrade/upgradelib.php @@ -274,6 +274,10 @@ class question_engine_attempt_upgrader { return $this->questionloader->get_question($questionid, $quizid); } + public function load_dataset($questionid, $selecteditem) { + return $this->questionloader->load_dataset($questionid, $selecteditem); + } + public function get_next_question_session($attempt, moodle_recordset $questionsessionsrs) { if (!$questionsessionsrs->valid()) { return false; @@ -335,7 +339,7 @@ class question_engine_attempt_upgrader { $converterclass = $this->get_converter_class_name($question, $quiz, 'missing'); $qbehaviourupdater = new $converterclass($quiz, $attempt, $question, - null, null, $this->logger); + null, null, $this->logger, $this); $qa = $qbehaviourupdater->supply_missing_qa(); $qbehaviourupdater->discard(); return $qa; @@ -352,7 +356,7 @@ class question_engine_attempt_upgrader { $converterclass = $this->get_converter_class_name($question, $quiz, $qsession->id); $qbehaviourupdater = new $converterclass($quiz, $attempt, $question, $qsession, - $qstates, $this->logger); + $qstates, $this->logger, $this); $qa = $qbehaviourupdater->get_converted_qa(); $qbehaviourupdater->discard(); return $qa; @@ -400,6 +404,7 @@ class question_engine_attempt_upgrader { */ class question_engine_upgrade_question_loader { private $cache = array(); + private $datasetcache = array(); public function __construct($logger) { $this->logger = $logger; @@ -464,6 +469,24 @@ class question_engine_upgrade_question_loader { $this->cache[$questionid] = $question; return $this->cache[$questionid]; } + + public function load_dataset($questionid, $selecteditem) { + global $DB; + + if (isset($this->datasetcache[$questionid][$selecteditem])) { + return $this->datasetcache[$questionid][$selecteditem]; + } + + $this->datasetcache[$questionid][$selecteditem] = $DB->get_records_sql_menu(' + SELECT qdd.name, qdi.value + FROM {question_dataset_items} qdi + JOIN {question_dataset_definitions} qdd ON qdd.id = qdi.definition + JOIN {question_datasets} qd ON qdd.id = qd.datasetdefinition + WHERE qd.question = ? + AND qdi.itemnumber = ? + ', array($questionid, $selecteditem)); + return $this->datasetcache[$questionid][$selecteditem]; + } } @@ -481,11 +504,14 @@ abstract class question_qtype_attempt_updater { protected $updater; /** @var question_engine_assumption_logger */ protected $logger; + /** @var question_engine_attempt_upgrader */ + protected $qeupdater; - public function __construct($updater, $question, $logger) { + public function __construct($updater, $question, $logger, $qeupdater) { $this->updater = $updater; $this->question = $question; $this->logger = $logger; + $this->qeupdater = $qeupdater; } public function discard() { @@ -493,6 +519,7 @@ abstract class question_qtype_attempt_updater { $this->updater = null; $this->question = null; $this->logger = null; + $this->qeupdater = null; } protected function to_text($html) { diff --git a/question/type/calculated/db/simpletest/testupgradelibnewqe.php b/question/type/calculated/db/simpletest/testupgradelibnewqe.php new file mode 100644 index 00000000000..ebaafc1b5b1 --- /dev/null +++ b/question/type/calculated/db/simpletest/testupgradelibnewqe.php @@ -0,0 +1,697 @@ +. + +/** + * Tests of the upgrade to the new Moodle question engine for attempts at + * calculated questions. + * + * @package qtype + * @subpackage calculated + * @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/upgrade/simpletest/helper.php'); + + +/** + * Testing the upgrade of calculated question attempts. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_calculated_attempt_upgrader_test extends question_attempt_upgrader_test_base { + public function test_calculated_adaptive_qsession97() { + $quiz = (object) array( + 'id' => '4', + 'course' => '2', + 'name' => 'Calculated quiz', + 'intro' => '', + 'introformat' => '1', + 'timeopen' => '0', + 'timeclose' => '0', + 'attempts' => '0', + 'attemptonlast' => '0', + 'grademethod' => '1', + 'decimalpoints' => '2', + 'questiondecimalpoints' => '-1', + 'questionsperpage' => '1', + 'shufflequestions' => '0', + 'shuffleanswers' => '1', + 'questions' => '16,0,17,0,18,0', + 'sumgrades' => '3.00000', + 'grade' => '10.00000', + 'timecreated' => '0', + 'timemodified' => '1305648351', + 'timelimit' => '0', + 'password' => '', + 'subnet' => '', + 'popup' => '0', + 'delay1' => '0', + 'delay2' => '0', + 'showuserpicture' => '0', + 'showblocks' => '0', + 'preferredbehaviour' => 'adaptive', + 'reviewattempt' => '69888', + 'reviewcorrectness' => '69888', + 'reviewmarks' => '69888', + 'reviewspecificfeedback' => '69888', + 'reviewgeneralfeedback' => '69888', + 'reviewrightanswer' => '69888', + 'reviewoverallfeedback' => '4352', + ); + $attempt = (object) array( + 'id' => '13', + 'uniqueid' => '13', + 'quiz' => '4', + 'userid' => '4', + 'attempt' => '1', + 'sumgrades' => '0.00000', + 'timestart' => '1305830650', + 'timefinish' => '1305830656', + 'timemodified' => '1305830656', + 'layout' => '16,0,17,0,18,0', + 'preview' => '0', + 'needsupgradetonewqe' => 1, + ); + $question = (object) array( + 'id' => '18', + 'category' => '2', + 'parent' => '0', + 'name' => 'Calculated', + 'questiontext' => '

What is {a} m + {b} m?

_______________

Remember to type a unit.

', + 'questiontextformat' => '1', + 'generalfeedback' => '', + 'generalfeedbackformat' => '1', + 'defaultmark' => '1.0000000', + 'penalty' => '0.1', + 'qtype' => 'calculated', + 'length' => '1', + 'stamp' => 'tjh238.vledev2.open.ac.uk+110519184316+ELvZeg', + 'version' => 'tjh238.vledev2.open.ac.uk+110519184317+exx1Bm', + 'hidden' => '0', + 'timecreated' => '1305830596', + 'timemodified' => '1305830596', + 'createdby' => '2', + 'modifiedby' => '2', + 'maxmark' => '1.0000000', + 'options' => (object) array( + 'id' => '2', + 'question' => '18', + 'synchronize' => '0', + 'single' => '0', + 'shuffleanswers' => '1', + 'correctfeedback' => '', + 'correctfeedbackformat' => '0', + 'partiallycorrectfeedback' => '', + 'partiallycorrectfeedbackformat' => '0', + 'incorrectfeedback' => '', + 'incorrectfeedbackformat' => '0', + 'answernumbering' => 'abc', + 'shownumcorrect' => '0', + 'answers' => array( + 28 => (object) array( + 'id' => '28', + 'question' => '18', + 'answer' => '{a} + {b}', + 'answerformat' => '0', + 'fraction' => '1.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + ), + 'units' => array( + 0 => (object) array( + 'id' => '9', + 'question' => '18', + 'multiplier' => 1, + 'unit' => 'm', + ), + ), + 'unitgradingtype' => '1', + 'unitpenalty' => '0.5000000', + 'showunits' => '0', + 'unitsleft' => '0', + ), + 'hints' => array( + ), + ); + $qsession = (object) array( + 'id' => '97', + 'attemptid' => '13', + 'questionid' => '18', + 'newest' => '258', + 'newgraded' => '258', + 'sumpenalty' => '0.1000000', + 'manualcomment' => '', + 'manualcommentformat' => '1', + 'flagged' => '0', + ); + $qstates = array( + 255 => (object) array( + 'id' => '255', + 'attempt' => '13', + 'question' => '18', + 'seq_number' => '0', + 'answer' => 'dataset10-|||||', + 'timestamp' => '1305830650', + 'event' => '0', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.0000000', + ), + 258 => (object) array( + 'id' => '258', + 'attempt' => '13', + 'question' => '18', + 'seq_number' => '1', + 'answer' => 'dataset10-|||||', + 'timestamp' => '1305830650', + 'event' => '6', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.1000000', + ), + ); + $this->loader->put_dataset_in_cache($question->id, 10, array('a' => '7.5', 'b' => '4.9')); + + $qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates); + + $expectedqa = (object) array( + 'behaviour' => 'adaptive', + 'questionid' => 18, + 'maxmark' => 1.0000000, + 'minfraction' => 0, + 'flagged' => 0, + 'questionsummary' => 'What is 7.5 m + 4.9 m? + +_______________ + +Remember to type a unit.', + 'rightanswer' => '12.4 m', + 'responsesummary' => '', + 'timemodified' => 1305830650, + 'steps' => array( + 0 => (object) array( + 'sequencenumber' => 0, + 'state' => 'todo', + 'fraction' => null, + 'timecreated' => 1305830650, + 'userid' => 4, + 'data' => array('-_try' => 1, '_dataset' => '10', '_separators' => '.$,', + '_var_a' => '7.5', '_var_b' => '4.9'), + ), + 1 => (object) array( + 'sequencenumber' => 1, + 'state' => 'gradedwrong', + 'fraction' => null, + 'timecreated' => 1305830650, + 'userid' => 4, + 'data' => array('answer' => '', '-finish' => 1, '-_try' => 1, '-_rawfraction' => 0), + ), + ), + ); + + $this->assertEqual($expectedqa, $qa); + } + + public function test_calculated_adaptive_qsession100() { + $quiz = (object) array( + 'id' => '4', + 'course' => '2', + 'name' => 'Calculated quiz', + 'intro' => '', + 'introformat' => '1', + 'timeopen' => '0', + 'timeclose' => '0', + 'attempts' => '0', + 'attemptonlast' => '0', + 'grademethod' => '1', + 'decimalpoints' => '2', + 'questiondecimalpoints' => '-1', + 'questionsperpage' => '1', + 'shufflequestions' => '0', + 'shuffleanswers' => '1', + 'questions' => '16,0,17,0,18,0', + 'sumgrades' => '3.00000', + 'grade' => '10.00000', + 'timecreated' => '0', + 'timemodified' => '1305648351', + 'timelimit' => '0', + 'password' => '', + 'subnet' => '', + 'popup' => '0', + 'delay1' => '0', + 'delay2' => '0', + 'showuserpicture' => '0', + 'showblocks' => '0', + 'preferredbehaviour' => 'adaptive', + 'reviewattempt' => '69888', + 'reviewcorrectness' => '69888', + 'reviewmarks' => '69888', + 'reviewspecificfeedback' => '69888', + 'reviewgeneralfeedback' => '69888', + 'reviewrightanswer' => '69888', + 'reviewoverallfeedback' => '4352', + ); + $attempt = (object) array( + 'id' => '14', + 'uniqueid' => '14', + 'quiz' => '4', + 'userid' => '4', + 'attempt' => '2', + 'sumgrades' => '2.80000', + 'timestart' => '1305830661', + 'timefinish' => '1305830729', + 'timemodified' => '1305830729', + 'layout' => '16,0,17,0,18,0', + 'preview' => '0', + 'needsupgradetonewqe' => 1, + ); + $question = (object) array( + 'id' => '18', + 'category' => '2', + 'parent' => '0', + 'name' => 'Calculated', + 'questiontext' => '

What is {a} m + {b} m?

_______________

Remember to type a unit.

', + 'questiontextformat' => '1', + 'generalfeedback' => '', + 'generalfeedbackformat' => '1', + 'defaultmark' => '1.0000000', + 'penalty' => '0.1', + 'qtype' => 'calculated', + 'length' => '1', + 'stamp' => 'tjh238.vledev2.open.ac.uk+110519184316+ELvZeg', + 'version' => 'tjh238.vledev2.open.ac.uk+110519184317+exx1Bm', + 'hidden' => '0', + 'timecreated' => '1305830596', + 'timemodified' => '1305830596', + 'createdby' => '2', + 'modifiedby' => '2', + 'maxmark' => '1.0000000', + 'options' => (object) array( + 'id' => '2', + 'question' => '18', + 'synchronize' => '0', + 'single' => '0', + 'shuffleanswers' => '1', + 'correctfeedback' => '', + 'correctfeedbackformat' => '0', + 'partiallycorrectfeedback' => '', + 'partiallycorrectfeedbackformat' => '0', + 'incorrectfeedback' => '', + 'incorrectfeedbackformat' => '0', + 'answernumbering' => 'abc', + 'shownumcorrect' => '0', + 'answers' => array( + 28 => (object) array( + 'id' => '28', + 'question' => '18', + 'answer' => '{a} + {b}', + 'answerformat' => '0', + 'fraction' => '1.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + ), + 'units' => array( + 0 => (object) array( + 'id' => '9', + 'question' => '18', + 'multiplier' => 1, + 'unit' => 'm', + ), + ), + 'unitgradingtype' => '1', + 'unitpenalty' => '0.5000000', + 'showunits' => '0', + 'unitsleft' => '0', + ), + 'hints' => array( + ), + ); + $qsession = (object) array( + 'id' => '100', + 'attemptid' => '14', + 'questionid' => '18', + 'newest' => '269', + 'newgraded' => '269', + 'sumpenalty' => '0.3000000', + 'manualcomment' => '', + 'manualcommentformat' => '1', + 'flagged' => '0', + ); + $qstates = array( + 261 => (object) array( + 'id' => '261', + 'attempt' => '14', + 'question' => '18', + 'seq_number' => '0', + 'answer' => 'dataset11-|||||', + 'timestamp' => '1305830661', + 'event' => '0', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.0000000', + ), + 265 => (object) array( + 'id' => '265', + 'attempt' => '14', + 'question' => '18', + 'seq_number' => '1', + 'answer' => 'dataset11-9.6|||||', + 'timestamp' => '1305830714', + 'event' => '3', + 'grade' => '0.5000000', + 'raw_grade' => '0.5000000', + 'penalty' => '0.1000000', + ), + 266 => (object) array( + 'id' => '266', + 'attempt' => '14', + 'question' => '18', + 'seq_number' => '2', + 'answer' => 'dataset11-9.6|||||m', + 'timestamp' => '1305830722', + 'event' => '3', + 'grade' => '0.9000000', + 'raw_grade' => '1.0000000', + 'penalty' => '0.1000000', + ), + 269 => (object) array( + 'id' => '269', + 'attempt' => '14', + 'question' => '18', + 'seq_number' => '3', + 'answer' => 'dataset11-9.6|||||m', + 'timestamp' => '1305830722', + 'event' => '6', + 'grade' => '0.9000000', + 'raw_grade' => '1.0000000', + 'penalty' => '0.1000000', + ), + ); + $this->loader->put_dataset_in_cache($question->id, 11, array('a' => '5.1', 'b' => '4.5')); + + $qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates); + + $expectedqa = (object) array( + 'behaviour' => 'adaptive', + 'questionid' => 18, + 'maxmark' => 1.0000000, + 'minfraction' => 0, + 'flagged' => 0, + 'questionsummary' => 'What is 5.1 m + 4.5 m? + +_______________ + +Remember to type a unit.', + 'rightanswer' => '9.6 m', + 'responsesummary' => '9.6 m', + 'timemodified' => 1305830722, + 'steps' => array( + 0 => (object) array( + 'sequencenumber' => 0, + 'state' => 'todo', + 'fraction' => null, + 'timecreated' => 1305830661, + 'userid' => 4, + 'data' => array('-_try' => 1, '_dataset' => '11', '_separators' => '.$,', + '_var_a' => '5.1', '_var_b' => '4.5'), + ), + 1 => (object) array( + 'sequencenumber' => 1, + 'state' => 'todo', + 'fraction' => 0.5, + 'timecreated' => 1305830714, + 'userid' => 4, + 'data' => array('answer' => 9.6, '-_try' => 2, + '-_rawfraction' => 0.5, '-submit' => 1), + ), + 2 => (object) array( + 'sequencenumber' => 2, + 'state' => 'todo', + 'fraction' => 0.9, + 'timecreated' => 1305830722, + 'userid' => 4, + 'data' => array('answer' => '9.6 m', '-_try' => 3, + '-_rawfraction' => 1, '-submit' => 1), + ), + 3 => (object) array( + 'sequencenumber' => 3, + 'state' => 'gradedright', + 'fraction' => 0.9, + 'timecreated' => 1305830722, + 'userid' => 4, + 'data' => array('answer' => '9.6 m', '-_try' => 3, + '-_rawfraction' => 1, '-finish' => 1), + ), + ), + ); + + $this->assertEqual($expectedqa, $qa); + } + + public function test_calculated_adaptive_qsession103() { + $quiz = (object) array( + 'id' => '4', + 'course' => '2', + 'name' => 'Calculated quiz', + 'intro' => '', + 'introformat' => '1', + 'timeopen' => '0', + 'timeclose' => '0', + 'attempts' => '0', + 'attemptonlast' => '0', + 'grademethod' => '1', + 'decimalpoints' => '2', + 'questiondecimalpoints' => '-1', + 'questionsperpage' => '1', + 'shufflequestions' => '0', + 'shuffleanswers' => '1', + 'questions' => '16,0,17,0,18,0', + 'sumgrades' => '3.00000', + 'grade' => '10.00000', + 'timecreated' => '0', + 'timemodified' => '1305648351', + 'timelimit' => '0', + 'password' => '', + 'subnet' => '', + 'popup' => '0', + 'delay1' => '0', + 'delay2' => '0', + 'showuserpicture' => '0', + 'showblocks' => '0', + 'preferredbehaviour' => 'adaptive', + 'reviewattempt' => '69888', + 'reviewcorrectness' => '69888', + 'reviewmarks' => '69888', + 'reviewspecificfeedback' => '69888', + 'reviewgeneralfeedback' => '69888', + 'reviewrightanswer' => '69888', + 'reviewoverallfeedback' => '4352', + ); + $attempt = (object) array( + 'id' => '15', + 'uniqueid' => '15', + 'quiz' => '4', + 'userid' => '3', + 'attempt' => '1', + 'sumgrades' => '0.70000', + 'timestart' => '1305830744', + 'timefinish' => '0', + 'timemodified' => '1305830792', + 'layout' => '16,0,17,0,18,0', + 'preview' => '0', + 'needsupgradetonewqe' => 1, + ); + $question = (object) array( + 'id' => '18', + 'category' => '2', + 'parent' => '0', + 'name' => 'Calculated', + 'questiontext' => '

What is {a} m + {b} m?

_______________

Remember to type a unit.

', + 'questiontextformat' => '1', + 'generalfeedback' => '', + 'generalfeedbackformat' => '1', + 'defaultmark' => '1.0000000', + 'penalty' => '0.1', + 'qtype' => 'calculated', + 'length' => '1', + 'stamp' => 'tjh238.vledev2.open.ac.uk+110519184316+ELvZeg', + 'version' => 'tjh238.vledev2.open.ac.uk+110519184317+exx1Bm', + 'hidden' => '0', + 'timecreated' => '1305830596', + 'timemodified' => '1305830596', + 'createdby' => '2', + 'modifiedby' => '2', + 'maxmark' => '1.0000000', + 'options' => (object) array( + 'id' => '2', + 'question' => '18', + 'synchronize' => '0', + 'single' => '0', + 'shuffleanswers' => '1', + 'correctfeedback' => '', + 'correctfeedbackformat' => '0', + 'partiallycorrectfeedback' => '', + 'partiallycorrectfeedbackformat' => '0', + 'incorrectfeedback' => '', + 'incorrectfeedbackformat' => '0', + 'answernumbering' => 'abc', + 'shownumcorrect' => '0', + 'answers' => array( + 28 => (object) array( + 'id' => '28', + 'question' => '18', + 'answer' => '{a} + {b}', + 'answerformat' => '0', + 'fraction' => '1.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + ), + 'units' => array( + 0 => (object) array( + 'id' => '9', + 'question' => '18', + 'multiplier' => 1, + 'unit' => 'm', + ), + ), + 'unitgradingtype' => '1', + 'unitpenalty' => '0.5000000', + 'showunits' => '0', + 'unitsleft' => '0', + ), + 'hints' => array( + ), + ); + $qsession = (object) array( + 'id' => '103', + 'attemptid' => '15', + 'questionid' => '18', + 'newest' => '280', + 'newgraded' => '279', + 'sumpenalty' => '0.1000000', + 'manualcomment' => '', + 'manualcommentformat' => '1', + 'flagged' => '0', + ); + $qstates = array( + 272 => (object) array( + 'id' => '272', + 'attempt' => '15', + 'question' => '18', + 'seq_number' => '0', + 'answer' => 'dataset1-|||||', + 'timestamp' => '1305830744', + 'event' => '0', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.0000000', + ), + 279 => (object) array( + 'id' => '279', + 'attempt' => '15', + 'question' => '18', + 'seq_number' => '1', + 'answer' => 'dataset1-123|||||cm', + 'timestamp' => '1305830775', + 'event' => '3', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.1000000', + ), + 280 => (object) array( + 'id' => '280', + 'attempt' => '15', + 'question' => '18', + 'seq_number' => '2', + 'answer' => 'dataset1-12.4|||||', + 'timestamp' => '1305830787', + 'event' => '2', + 'grade' => '0.0000000', + 'raw_grade' => '0.5000000', + 'penalty' => '0.1000000', + ), + ); + $this->loader->put_dataset_in_cache($question->id, 1, array('a' => '9.9', 'b' => '2.5')); + + $qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates); + + $expectedqa = (object) array( + 'behaviour' => 'adaptive', + 'questionid' => 18, + 'maxmark' => 1.0000000, + 'minfraction' => 0, + 'flagged' => 0, + 'questionsummary' => 'What is 9.9 m + 2.5 m? + +_______________ + +Remember to type a unit.', + 'rightanswer' => '12.4 m', + 'responsesummary' => '12.4', + 'timemodified' => 1305830787, + 'steps' => array( + 0 => (object) array( + 'sequencenumber' => 0, + 'state' => 'todo', + 'fraction' => null, + 'timecreated' => 1305830744, + 'userid' => 3, + 'data' => array('-_try' => 1, '_dataset' => '1', '_separators' => '.$,', + '_var_a' => '9.9', '_var_b' => '2.5'), + ), + 1 => (object) array( + 'sequencenumber' => 1, + 'state' => 'todo', + 'fraction' => 0, + 'timecreated' => 1305830775, + 'userid' => 3, + 'data' => array('answer' => '123 cm', '-_try' => 2, + '-_rawfraction' => 0, '-submit' => 1), + ), + 2 => (object) array( + 'sequencenumber' => 2, + 'state' => 'complete', + 'fraction' => 0, + 'timecreated' => 1305830787, + 'userid' => 3, + 'data' => array('answer' => '12.4'), + ), + ), + ); + + $this->assertEqual($expectedqa, $qa); + } +} diff --git a/question/type/calculated/db/upgradelib.php b/question/type/calculated/db/upgradelib.php new file mode 100644 index 00000000000..91f0fb9aa8b --- /dev/null +++ b/question/type/calculated/db/upgradelib.php @@ -0,0 +1,284 @@ +. + +/** + * Upgrade library code for the calculated question type. + * + * @package qtype + * @subpackage calculated + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Class for converting attempt data for calculated questions when upgrading + * attempts to the new question engine. + * + * This class is used by the code in question/engine/upgrade/upgradelib.php. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_calculated_qe2_attempt_updater extends question_qtype_attempt_updater { + protected $selecteditem = null; + /** @var array variable name => value */ + protected $values; + + /** @var array variable names wrapped in {...}. Used by {@link substitute_values()}. */ + protected $search; + + /** + * @var array variable values, with negative numbers wrapped in (...). + * Used by {@link substitute_values()}. + */ + protected $safevalue; + + /** + * @var array variable values, with negative numbers wrapped in (...). + * Used by {@link substitute_values()}. + */ + protected $prettyvalue; + + public function question_summary() { + return ''; // Done later, after we know which dataset is used. + } + + public function right_answer() { + foreach ($this->question->options->answers as $ans) { + if ($ans->fraction > 0.999) { + $right = $this->calculate($ans->answer); + + if (empty($this->question->options->units)) { + return $right; + } + + $unit = reset($this->question->options->units); + $unit = $unit->unit; + if (!empty($this->question->options->unitsleft)) { + return $unit . ' ' . $right; + } else { + return $right . ' ' . $unit; + } + } + } + } + + protected function parse_response($state) { + if (strpos($state->answer, '-') < 7) { + // Broken state, skip it. + throw new coding_exception("Brokes state {$state->id} for calcluated + question {$state->question}. (It did not specify a dataset."); + } + list($datasetbit, $realanswer) = explode('-', $state->answer, 2); + $selecteditem = substr($datasetbit, 7); + + if (is_null($this->selecteditem)) { + $this->load_dataset($selecteditem); + } else if ($this->selecteditem != $selecteditem) { + $this->logger->log_assumption("Different states for calcluated question + {$state->question} used different dataset items. Ignoring the change + in state {$state->id} and coninuting to use item {$this->selecteditem}."); + } + + if (!$realanswer) { + return ''; + } + + if (strpos($realanswer, '|||||') === false) { + $answer = $realanswer; + $unit = ''; + } else { + list($answer, $unit) = explode('|||||', $realanswer, 2); + } + + return array($answer, $unit); + } + + public function response_summary($state) { + list($answer, $unit) = $this->parse_response($state); + + if (empty($answer) && empty($unit)) { + $resp = null; + } else { + $resp = $answer; + } + + if (!empty($unit)) { + if (!empty($this->question->options->unitsleft)) { + $resp = trim($unit . ' ' . $resp); + } else { + $resp = trim($resp . ' ' . $unit); + } + } + + return $resp; + } + + public function was_answered($state) { + return !empty($state->answer); + } + + public function set_first_step_data_elements($state, &$data) { + $this->parse_response($state); + $this->updater->qa->questionsummary = $this->to_text( + $this->replace_expressions_in_text($this->question->questiontext)); + $this->updater->qa->rightanswer = $this->right_answer($this->question); + + $data['_dataset'] = $this->selecteditem; + foreach ($this->values as $name => $value) { + $data['_var_' . $name] = $value; + } + + $data['_separators'] = '.$,'; + } + + public function supply_missing_first_step_data(&$data) { + $data['_separators'] = '.$,'; + } + + public function set_data_elements_for_step($state, &$data) { + if (empty($state->answer)) { + return; + } + + list($answer, $unit) = $this->parse_response($state); + if (!empty($this->question->options->showunits) && + $this->question->options->showunits == 1) { + // Multichoice units. + $data['answer'] = $answer; + $data['unit'] = $unit; + } else if (!empty($this->question->options->unitsleft)) { + if (!empty($unit)) { + $data['answer'] = $unit . ' ' . $answer; + } else { + $data['answer'] = $answer; + } + } else { + if (!empty($unit)) { + $data['answer'] = $answer . ' ' . $unit; + } else { + $data['answer'] = $answer; + } + } + } + + public function load_dataset($selecteditem) { + $this->selecteditem = $selecteditem; + $this->values = $this->qeupdater->load_dataset( + $this->question->id, $selecteditem); + + // Prepare an array for {@link substitute_values()}. + $this->search = array(); + $this->safevalue = array(); + $this->prettyvalue = array(); + foreach ($this->values as $name => $value) { + if (!is_numeric($value)) { + $a = new stdClass(); + $a->name = '{' . $name . '}'; + $a->value = $value; + throw new moodle_exception('notvalidnumber', 'qtype_calculated', '', $a); + } + + $this->search[] = '{' . $name . '}'; + $this->safevalue[] = '(' . $value . ')'; + $this->prettyvalue[] = $this->format_float($value); + } + } + + /** + * Display a float properly formatted with a certain number of decimal places. + * @param $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 $x; + } + + /** + * Evaluate an expression using the variable values. + * @param string $expression the expression. A PHP expression with placeholders + * like {a} for where the variables need to go. + * @return float the computed result. + */ + public function calculate($expression) { + return $this->calculate_raw($this->substitute_values_for_eval($expression)); + } + + /** + * Evaluate an expression after the variable values have been substituted. + * @param string $expression the expression. A PHP expression with placeholders + * like {a} for where the variables need to go. + * @return float the computed result. + */ + protected function calculate_raw($expression) { + // This validation trick from http://php.net/manual/en/function.eval.php + if (!@eval('return true; $result = ' . $expression . ';')) { + return '[Invalid expression ' . $expression . ']'; + } + return eval('return ' . $expression . ';'); + } + + /** + * Substitute variable placehodlers like {a} with their value wrapped in (). + * @param string $expression the expression. A PHP expression with placeholders + * like {a} for where the variables need to go. + * @return string the expression with each placeholder replaced by the + * corresponding value. + */ + protected function substitute_values_for_eval($expression) { + return str_replace($this->search, $this->safevalue, $expression); + } + + /** + * Substitute variable placehodlers like {a} with their value without wrapping + * the value in anything. + * @param string $text some content with placeholders + * like {a} for where the variables need to go. + * @return string the expression with each placeholder replaced by the + * corresponding value. + */ + protected function substitute_values_pretty($text) { + return str_replace($this->search, $this->prettyvalue, $text); + } + + /** + * Replace any embedded variables (like {a}) or formulae (like {={a} + {b}}) + * in some text with the corresponding values. + * @param string $text the text to process. + * @return string the text with values substituted. + */ + 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, $format, $length) { + return $vs->format_float($vs->calculate($matches[1]), $length, $format); + }, $text); + return $this->substitute_values_pretty($text); + } +} diff --git a/question/type/calculatedmulti/db/simpletest/testupgradelibnewqe.php b/question/type/calculatedmulti/db/simpletest/testupgradelibnewqe.php new file mode 100644 index 00000000000..46ba068a1bc --- /dev/null +++ b/question/type/calculatedmulti/db/simpletest/testupgradelibnewqe.php @@ -0,0 +1,802 @@ +. + +/** + * Tests of the upgrade to the new Moodle question engine for attempts at + * 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/upgrade/simpletest/helper.php'); + + +/** + * Testing the upgrade of calculated multiple-choice question attempts. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_calculatedmulti_attempt_upgrader_test extends question_attempt_upgrader_test_base { + public function test_calculatedmulti_adaptive_qsession96() { + $quiz = (object) array( + 'id' => '4', + 'course' => '2', + 'name' => 'Calculated quiz', + 'intro' => '', + 'introformat' => '1', + 'timeopen' => '0', + 'timeclose' => '0', + 'attempts' => '0', + 'attemptonlast' => '0', + 'grademethod' => '1', + 'decimalpoints' => '2', + 'questiondecimalpoints' => '-1', + 'questionsperpage' => '1', + 'shufflequestions' => '0', + 'shuffleanswers' => '1', + 'questions' => '16,0,17,0,18,0', + 'sumgrades' => '3.00000', + 'grade' => '10.00000', + 'timecreated' => '0', + 'timemodified' => '1305648351', + 'timelimit' => '0', + 'password' => '', + 'subnet' => '', + 'popup' => '0', + 'delay1' => '0', + 'delay2' => '0', + 'showuserpicture' => '0', + 'showblocks' => '0', + 'preferredbehaviour' => 'adaptive', + 'reviewattempt' => '69888', + 'reviewcorrectness' => '69888', + 'reviewmarks' => '69888', + 'reviewspecificfeedback' => '69888', + 'reviewgeneralfeedback' => '69888', + 'reviewrightanswer' => '69888', + 'reviewoverallfeedback' => '4352', + ); + $attempt = (object) array( + 'id' => '13', + 'uniqueid' => '13', + 'quiz' => '4', + 'userid' => '4', + 'attempt' => '1', + 'sumgrades' => '0.00000', + 'timestart' => '1305830650', + 'timefinish' => '1305830656', + 'timemodified' => '1305830656', + 'layout' => '16,0,17,0,18,0', + 'preview' => '0', + 'needsupgradetonewqe' => 1, + ); + $question = (object) array( + 'id' => '17', + 'category' => '2', + 'parent' => '0', + 'name' => 'Calculated multiple-choice', + 'questiontext' => '

What is {a} + {b}?

', + 'questiontextformat' => '1', + 'generalfeedback' => '', + 'generalfeedbackformat' => '1', + 'defaultmark' => '1.0000000', + 'penalty' => '0.1', + 'qtype' => 'calculatedmulti', + 'length' => '1', + 'stamp' => 'tjh238.vledev2.open.ac.uk+110519184120+gpbLCv', + 'version' => 'tjh238.vledev2.open.ac.uk+110519184120+P5jpWJ', + 'hidden' => '0', + 'timecreated' => '1305830480', + 'timemodified' => '1305830480', + 'createdby' => '2', + 'modifiedby' => '2', + 'maxmark' => '1.0000000', + 'options' => (object) array( + 'id' => '1', + 'question' => '17', + 'synchronize' => '0', + 'single' => '1', + 'shuffleanswers' => '1', + 'correctfeedback' => '', + 'correctfeedbackformat' => '1', + 'partiallycorrectfeedback' => '', + 'partiallycorrectfeedbackformat' => '1', + 'incorrectfeedback' => '', + 'incorrectfeedbackformat' => '1', + 'answernumbering' => 'abc', + 'shownumcorrect' => '0', + 'answers' => array( + 24 => (object) array( + 'id' => '24', + 'question' => '17', + 'answer' => '{={a} - {b}}', + 'answerformat' => '0', + 'fraction' => '0.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + 25 => (object) array( + 'id' => '25', + 'question' => '17', + 'answer' => '{={a} + {b}}', + 'answerformat' => '0', + 'fraction' => '1.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + 26 => (object) array( + 'id' => '26', + 'question' => '17', + 'answer' => '{={a} / {b}}', + 'answerformat' => '0', + 'fraction' => '0.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + 27 => (object) array( + 'id' => '27', + 'question' => '17', + 'answer' => '{={a} * {b}}', + 'answerformat' => '0', + 'fraction' => '0.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + ), + ), + 'hints' => array( + ), + ); + $qsession = (object) array( + 'id' => '96', + 'attemptid' => '13', + 'questionid' => '17', + 'newest' => '257', + 'newgraded' => '257', + 'sumpenalty' => '0.1000000', + 'manualcomment' => '', + 'manualcommentformat' => '1', + 'flagged' => '0', + ); + $qstates = array( + 254 => (object) array( + 'id' => '254', + 'attempt' => '13', + 'question' => '17', + 'seq_number' => '0', + 'answer' => 'dataset3-24,26,27,25:', + 'timestamp' => '1305830650', + 'event' => '0', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.0000000', + ), + 257 => (object) array( + 'id' => '257', + 'attempt' => '13', + 'question' => '17', + 'seq_number' => '1', + 'answer' => 'dataset3-24,26,27,25:', + 'timestamp' => '1305830650', + 'event' => '6', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.1000000', + ), + ); + $this->loader->put_dataset_in_cache($question->id, 3, array('a' => '4.3', 'b' => '5.4')); + + $qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates); + + $expectedqa = (object) array( + 'behaviour' => 'adaptive', + 'questionid' => 17, + 'maxmark' => 1.0000000, + 'minfraction' => 0, + 'flagged' => 0, + 'questionsummary' => 'What is 4.3 + 5.4?', + 'rightanswer' => '9.7', + 'responsesummary' => '', + 'timemodified' => 1305830650, + 'steps' => array( + 0 => (object) array( + 'sequencenumber' => 0, + 'state' => 'todo', + 'fraction' => null, + 'timecreated' => 1305830650, + 'userid' => 4, + 'data' => array('-_try' => 1, '_dataset' => '3', + '_order' => '24,26,27,25', '_var_a' => '4.3', '_var_b' => '5.4'), + ), + 1 => (object) array( + 'sequencenumber' => 1, + 'state' => 'gradedwrong', + 'fraction' => null, + 'timecreated' => 1305830650, + 'userid' => 4, + 'data' => array('-finish' => 1, '-_try' => 1, '-_rawfraction' => 0), + ), + ), + ); + + $this->assertEqual($expectedqa, $qa); + } + + public function test_calculatedmulti_adaptive_qsession99() { + $quiz = (object) array( + 'id' => '4', + 'course' => '2', + 'name' => 'Calculated quiz', + 'intro' => '', + 'introformat' => '1', + 'timeopen' => '0', + 'timeclose' => '0', + 'attempts' => '0', + 'attemptonlast' => '0', + 'grademethod' => '1', + 'decimalpoints' => '2', + 'questiondecimalpoints' => '-1', + 'questionsperpage' => '1', + 'shufflequestions' => '0', + 'shuffleanswers' => '1', + 'questions' => '16,0,17,0,18,0', + 'sumgrades' => '3.00000', + 'grade' => '10.00000', + 'timecreated' => '0', + 'timemodified' => '1305648351', + 'timelimit' => '0', + 'password' => '', + 'subnet' => '', + 'popup' => '0', + 'delay1' => '0', + 'delay2' => '0', + 'showuserpicture' => '0', + 'showblocks' => '0', + 'preferredbehaviour' => 'adaptive', + 'reviewattempt' => '69888', + 'reviewcorrectness' => '69888', + 'reviewmarks' => '69888', + 'reviewspecificfeedback' => '69888', + 'reviewgeneralfeedback' => '69888', + 'reviewrightanswer' => '69888', + 'reviewoverallfeedback' => '4352', + ); + $attempt = (object) array( + 'id' => '14', + 'uniqueid' => '14', + 'quiz' => '4', + 'userid' => '4', + 'attempt' => '2', + 'sumgrades' => '2.80000', + 'timestart' => '1305830661', + 'timefinish' => '1305830729', + 'timemodified' => '1305830729', + 'layout' => '16,0,17,0,18,0', + 'preview' => '0', + 'needsupgradetonewqe' => 1, + ); + $question = (object) array( + 'id' => '17', + 'category' => '2', + 'parent' => '0', + 'name' => 'Calculated multiple-choice', + 'questiontext' => '

What is {a} + {b}?

', + 'questiontextformat' => '1', + 'generalfeedback' => '', + 'generalfeedbackformat' => '1', + 'defaultmark' => '1.0000000', + 'penalty' => '0.1', + 'qtype' => 'calculatedmulti', + 'length' => '1', + 'stamp' => 'tjh238.vledev2.open.ac.uk+110519184120+gpbLCv', + 'version' => 'tjh238.vledev2.open.ac.uk+110519184120+P5jpWJ', + 'hidden' => '0', + 'timecreated' => '1305830480', + 'timemodified' => '1305830480', + 'createdby' => '2', + 'modifiedby' => '2', + 'maxmark' => '1.0000000', + 'options' => (object) array( + 'id' => '1', + 'question' => '17', + 'synchronize' => '0', + 'single' => '1', + 'shuffleanswers' => '1', + 'correctfeedback' => '', + 'correctfeedbackformat' => '1', + 'partiallycorrectfeedback' => '', + 'partiallycorrectfeedbackformat' => '1', + 'incorrectfeedback' => '', + 'incorrectfeedbackformat' => '1', + 'answernumbering' => 'abc', + 'shownumcorrect' => '0', + 'answers' => array( + 24 => (object) array( + 'id' => '24', + 'question' => '17', + 'answer' => '{={a} - {b}}', + 'answerformat' => '0', + 'fraction' => '0.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + 25 => (object) array( + 'id' => '25', + 'question' => '17', + 'answer' => '{={a} + {b}}', + 'answerformat' => '0', + 'fraction' => '1.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + 26 => (object) array( + 'id' => '26', + 'question' => '17', + 'answer' => '{={a} / {b}}', + 'answerformat' => '0', + 'fraction' => '0.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + 27 => (object) array( + 'id' => '27', + 'question' => '17', + 'answer' => '{={a} * {b}}', + 'answerformat' => '0', + 'fraction' => '0.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + ), + ), + 'hints' => array( + ), + ); + $qsession = (object) array( + 'id' => '99', + 'attemptid' => '14', + 'questionid' => '17', + 'newest' => '268', + 'newgraded' => '268', + 'sumpenalty' => '0.2000000', + 'manualcomment' => '', + 'manualcommentformat' => '1', + 'flagged' => '0', + ); + $qstates = array( + 260 => (object) array( + 'id' => '260', + 'attempt' => '14', + 'question' => '17', + 'seq_number' => '0', + 'answer' => 'dataset8-25,24,27,26:', + 'timestamp' => '1305830661', + 'event' => '0', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.0000000', + ), + 264 => (object) array( + 'id' => '264', + 'attempt' => '14', + 'question' => '17', + 'seq_number' => '1', + 'answer' => 'dataset8-25,24,27,26:25', + 'timestamp' => '1305830699', + 'event' => '3', + 'grade' => '1.0000000', + 'raw_grade' => '1.0000000', + 'penalty' => '0.1000000', + ), + 268 => (object) array( + 'id' => '268', + 'attempt' => '14', + 'question' => '17', + 'seq_number' => '2', + 'answer' => 'dataset8-25,24,27,26:25', + 'timestamp' => '1305830699', + 'event' => '6', + 'grade' => '1.0000000', + 'raw_grade' => '1.0000000', + 'penalty' => '0.1000000', + ), + ); + $this->loader->put_dataset_in_cache($question->id, 8, array('a' => '3.7', 'b' => '6.0')); + + $qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates); + + $expectedqa = (object) array( + 'behaviour' => 'adaptive', + 'questionid' => 17, + 'maxmark' => 1.0000000, + 'minfraction' => 0, + 'flagged' => 0, + 'questionsummary' => 'What is 3.7 + 6.0?', + 'rightanswer' => '9.7', + 'responsesummary' => '9.7', + 'timemodified' => 1305830699, + 'steps' => array( + 0 => (object) array( + 'sequencenumber' => 0, + 'state' => 'todo', + 'fraction' => null, + 'timecreated' => 1305830661, + 'userid' => 4, + 'data' => array('-_try' => 1, '_dataset' => '8', + '_order' => '25,24,27,26', '_var_a' => '3.7', '_var_b' => '6.0'), + ), + 1 => (object) array( + 'sequencenumber' => 1, + 'state' => 'complete', + 'fraction' => 1, + 'timecreated' => 1305830699, + 'userid' => 4, + 'data' => array('answer' => '0', '-submit' => 1, '-_try' => 2, '-_rawfraction' => 1), + ), + 2 => (object) array( + 'sequencenumber' => 2, + 'state' => 'gradedright', + 'fraction' => 1, + 'timecreated' => 1305830699, + 'userid' => 4, + 'data' => array('answer' => '0', '-finish' => 1, '-_try' => 2, '-_rawfraction' => 1), + ), + ), + ); + + $this->assertEqual($expectedqa, $qa); + } + + public function test_calculatedmulti_adaptive_qsession102() { + $quiz = (object) array( + 'id' => '4', + 'course' => '2', + 'name' => 'Calculated quiz', + 'intro' => '', + 'introformat' => '1', + 'timeopen' => '0', + 'timeclose' => '0', + 'attempts' => '0', + 'attemptonlast' => '0', + 'grademethod' => '1', + 'decimalpoints' => '2', + 'questiondecimalpoints' => '-1', + 'questionsperpage' => '1', + 'shufflequestions' => '0', + 'shuffleanswers' => '1', + 'questions' => '16,0,17,0,18,0', + 'sumgrades' => '3.00000', + 'grade' => '10.00000', + 'timecreated' => '0', + 'timemodified' => '1305648351', + 'timelimit' => '0', + 'password' => '', + 'subnet' => '', + 'popup' => '0', + 'delay1' => '0', + 'delay2' => '0', + 'showuserpicture' => '0', + 'showblocks' => '0', + 'preferredbehaviour' => 'adaptive', + 'reviewattempt' => '69888', + 'reviewcorrectness' => '69888', + 'reviewmarks' => '69888', + 'reviewspecificfeedback' => '69888', + 'reviewgeneralfeedback' => '69888', + 'reviewrightanswer' => '69888', + 'reviewoverallfeedback' => '4352', + ); + $attempt = (object) array( + 'id' => '15', + 'uniqueid' => '15', + 'quiz' => '4', + 'userid' => '3', + 'attempt' => '1', + 'sumgrades' => '0.70000', + 'timestart' => '1305830744', + 'timefinish' => '0', + 'timemodified' => '1305830792', + 'layout' => '16,0,17,0,18,0', + 'preview' => '0', + 'needsupgradetonewqe' => 1, + ); + $question = (object) array( + 'id' => '17', + 'category' => '2', + 'parent' => '0', + 'name' => 'Calculated multiple-choice', + 'questiontext' => '

What is {a} + {b}?

', + 'questiontextformat' => '1', + 'generalfeedback' => '', + 'generalfeedbackformat' => '1', + 'defaultmark' => '1.0000000', + 'penalty' => '0.1', + 'qtype' => 'calculatedmulti', + 'length' => '1', + 'stamp' => 'tjh238.vledev2.open.ac.uk+110519184120+gpbLCv', + 'version' => 'tjh238.vledev2.open.ac.uk+110519184120+P5jpWJ', + 'hidden' => '0', + 'timecreated' => '1305830480', + 'timemodified' => '1305830480', + 'createdby' => '2', + 'modifiedby' => '2', + 'maxmark' => '1.0000000', + 'options' => (object) array( + 'id' => '1', + 'question' => '17', + 'synchronize' => '0', + 'single' => '1', + 'shuffleanswers' => '1', + 'correctfeedback' => '', + 'correctfeedbackformat' => '1', + 'partiallycorrectfeedback' => '', + 'partiallycorrectfeedbackformat' => '1', + 'incorrectfeedback' => '', + 'incorrectfeedbackformat' => '1', + 'answernumbering' => 'abc', + 'shownumcorrect' => '0', + 'answers' => array( + 24 => (object) array( + 'id' => '24', + 'question' => '17', + 'answer' => '{={a} - {b}}', + 'answerformat' => '0', + 'fraction' => '0.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + 25 => (object) array( + 'id' => '25', + 'question' => '17', + 'answer' => '{={a} + {b}}', + 'answerformat' => '0', + 'fraction' => '1.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + 26 => (object) array( + 'id' => '26', + 'question' => '17', + 'answer' => '{={a} / {b}}', + 'answerformat' => '0', + 'fraction' => '0.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + 27 => (object) array( + 'id' => '27', + 'question' => '17', + 'answer' => '{={a} * {b}}', + 'answerformat' => '0', + 'fraction' => '0.0000000', + 'feedback' => '', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + ), + ), + 'hints' => array( + ), + ); + $qsession = (object) array( + 'id' => '102', + 'attemptid' => '15', + 'questionid' => '17', + 'newest' => '278', + 'newgraded' => '278', + 'sumpenalty' => '0.5000000', + 'manualcomment' => '', + 'manualcommentformat' => '1', + 'flagged' => '0', + ); + $qstates = array( + 271 => (object) array( + 'id' => '271', + 'attempt' => '15', + 'question' => '17', + 'seq_number' => '0', + 'answer' => 'dataset7-26,24,25,27:', + 'timestamp' => '1305830744', + 'event' => '0', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.0000000', + ), + 274 => (object) array( + 'id' => '274', + 'attempt' => '15', + 'question' => '17', + 'seq_number' => '1', + 'answer' => 'dataset7-26,24,25,27:27', + 'timestamp' => '1305830759', + 'event' => '3', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.1000000', + ), + 275 => (object) array( + 'id' => '275', + 'attempt' => '15', + 'question' => '17', + 'seq_number' => '2', + 'answer' => 'dataset7-26,24,25,27:24', + 'timestamp' => '1305830761', + 'event' => '3', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.1000000', + ), + 276 => (object) array( + 'id' => '276', + 'attempt' => '15', + 'question' => '17', + 'seq_number' => '3', + 'answer' => 'dataset7-26,24,25,27:26', + 'timestamp' => '1305830764', + 'event' => '3', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.1000000', + ), + 277 => (object) array( + 'id' => '277', + 'attempt' => '15', + 'question' => '17', + 'seq_number' => '4', + 'answer' => 'dataset7-26,24,25,27:25', + 'timestamp' => '1305830766', + 'event' => '3', + 'grade' => '0.7000000', + 'raw_grade' => '1.0000000', + 'penalty' => '0.1000000', + ), + 278 => (object) array( + 'id' => '278', + 'attempt' => '15', + 'question' => '17', + 'seq_number' => '5', + 'answer' => 'dataset7-26,24,25,27:24', + 'timestamp' => '1305830768', + 'event' => '3', + 'grade' => '0.7000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.1000000', + ), + ); + $this->loader->put_dataset_in_cache($question->id, 7, array('a' => '4.4', 'b' => '8.2')); + + $qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates); + + $expectedqa = (object) array( + 'behaviour' => 'adaptive', + 'questionid' => 17, + 'maxmark' => 1.0000000, + 'minfraction' => 0, + 'flagged' => 0, + 'questionsummary' => 'What is 4.4 + 8.2?', + 'rightanswer' => '12.6', + 'responsesummary' => '-3.8', + 'timemodified' => 1305830768, + 'steps' => array( + 0 => (object) array( + 'sequencenumber' => 0, + 'state' => 'todo', + 'fraction' => null, + 'timecreated' => 1305830744, + 'userid' => 3, + 'data' => array('-_try' => 1, '_dataset' => '7', + '_order' => '26,24,25,27', '_var_a' => '4.4', '_var_b' => '8.2'), + ), + 1 => (object) array( + 'sequencenumber' => 1, + 'state' => 'todo', + 'fraction' => 0, + 'timecreated' => 1305830759, + 'userid' => 3, + 'data' => array('answer' => '3', '-submit' => 1, '-_try' => 2, '-_rawfraction' => 0), + ), + 2 => (object) array( + 'sequencenumber' => 2, + 'state' => 'todo', + 'fraction' => 0, + 'timecreated' => 1305830761, + 'userid' => 3, + 'data' => array('answer' => '1', '-submit' => 1, '-_try' => 3, '-_rawfraction' => 0), + ), + 3 => (object) array( + 'sequencenumber' => 3, + 'state' => 'todo', + 'fraction' => 0, + 'timecreated' => 1305830764, + 'userid' => 3, + 'data' => array('answer' => '0', '-submit' => 1, '-_try' => 4, '-_rawfraction' => 0), + ), + 4 => (object) array( + 'sequencenumber' => 4, + 'state' => 'todo', + 'fraction' => 0.7, + 'timecreated' => 1305830766, + 'userid' => 3, + 'data' => array('answer' => '2', '-submit' => 1, '-_try' => 5, '-_rawfraction' => 1), + ), + 5 => (object) array( + 'sequencenumber' => 5, + 'state' => 'todo', + 'fraction' => 0.7, + 'timecreated' => 1305830768, + 'userid' => 3, + 'data' => array('answer' => '1', '-submit' => 1, '-_try' => 6, '-_rawfraction' => 0), + ), + ), + ); + + $this->assertEqual($expectedqa, $qa); + } +} diff --git a/question/type/calculatedmulti/db/upgradelib.php b/question/type/calculatedmulti/db/upgradelib.php new file mode 100644 index 00000000000..31db43dbad4 --- /dev/null +++ b/question/type/calculatedmulti/db/upgradelib.php @@ -0,0 +1,308 @@ +. + +/** + * Upgrade library code for the calculated multiple-choice 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(); + + +/** + * Class for converting attempt data for calculated multiple-choice questions + * when upgrading attempts to the new question engine. + * + * This class is used by the code in question/engine/upgrade/upgradelib.php. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_calculatedmulti_qe2_attempt_updater extends question_qtype_attempt_updater { + protected $selecteditem = null; + /** @var array variable name => value */ + protected $values; + + /** @var array variable names wrapped in {...}. Used by {@link substitute_values()}. */ + protected $search; + + /** + * @var array variable values, with negative numbers wrapped in (...). + * Used by {@link substitute_values()}. + */ + protected $safevalue; + + /** + * @var array variable values, with negative numbers wrapped in (...). + * Used by {@link substitute_values()}. + */ + protected $prettyvalue; + + protected $order; + + public function question_summary() { + return ''; // Done later, after we know which dataset is used. + } + + public function right_answer() { + if ($this->question->options->single) { + foreach ($this->question->options->answers as $ans) { + if ($ans->fraction > 0.999) { + return $this->to_text($this->replace_expressions_in_text($ans->answer)); + } + } + + } else { + $rightbits = array(); + foreach ($this->question->options->answers as $ans) { + if ($ans->fraction >= 0.000001) { + $rightbits[] = $this->to_text($this->replace_expressions_in_text($ans->answer)); + } + } + return implode('; ', $rightbits); + } + } + + protected function explode_answer($state) { + if (strpos($state->answer, '-') < 7) { + // Broken state, skip it. + throw new coding_exception("Brokes state {$state->id} for calcluatedmulti + question {$state->question}. (It did not specify a dataset."); + } + list($datasetbit, $answer) = explode('-', $state->answer, 2); + $selecteditem = substr($datasetbit, 7); + + if (is_null($this->selecteditem)) { + $this->load_dataset($selecteditem); + } else if ($this->selecteditem != $selecteditem) { + $this->logger->log_assumption("Different states for calcluatedmulti question + {$state->question} used different dataset items. Ignoring the change + in state {$state->id} and coninuting to use item {$this->selecteditem}."); + } + + if (strpos($answer, ':') !== false) { + list($order, $responses) = explode(':', $answer); + return $responses; + } else { + // Sometimes, a bug means that a state is missing the : bit, + // We need to deal with that. + $this->logger->log_assumption("Dealing with missing order information + in attempt at multiple choice question {$this->question->id}"); + return $answer; + } + } + + public function response_summary($state) { + $responses = $this->explode_answer($state); + if ($this->question->options->single) { + if (is_numeric($responses)) { + if (array_key_exists($responses, $this->question->options->answers)) { + return $this->to_text($this->replace_expressions_in_text( + $this->question->options->answers[$responses]->answer)); + } else { + $this->logger->log_assumption("Dealing with a place where the + student selected a choice that was later deleted for + multiple choice question {$this->question->id}"); + return '[CHOICE THAT WAS LATER DELETED]'; + } + } else { + return null; + } + + } else { + if (!empty($responses)) { + $responses = explode(',', $responses); + $bits = array(); + foreach ($responses as $response) { + if (array_key_exists($response, $this->question->options->answers)) { + $bits[] = $this->to_text($this->replace_expressions_in_text( + $this->question->options->answers[$response]->answer)); + } else { + $this->logger->log_assumption("Dealing with a place where the + student selected a choice that was later deleted for + multiple choice question {$this->question->id}"); + $bits[] = '[CHOICE THAT WAS LATER DELETED]'; + } + } + return implode('; ', $bits); + } else { + return null; + } + } + } + + public function was_answered($state) { + $responses = $this->explode_answer($state); + if ($this->question->options->single) { + return is_numeric($responses); + } else { + return !empty($responses); + } + } + + public function set_first_step_data_elements($state, &$data) { + $this->explode_answer($state); + $this->updater->qa->questionsummary = $this->to_text( + $this->replace_expressions_in_text($this->question->questiontext)); + $this->updater->qa->rightanswer = $this->right_answer($this->question); + + $data['_dataset'] = $this->selecteditem; + foreach ($this->values as $name => $value) { + $data['_var_' . $name] = $value; + } + + list($datasetbit, $answer) = explode('-', $state->answer, 2); + list($order, $responses) = explode(':', $answer); + $data['_order'] = $order; + $this->order = explode(',', $order); + } + + public function supply_missing_first_step_data(&$data) { + $data['_order'] = implode(',', array_keys($this->question->options->answers)); + } + + public function set_data_elements_for_step($state, &$data) { + $responses = $this->explode_answer($state); + if ($this->question->options->single) { + if (is_numeric($responses)) { + $flippedorder = array_combine(array_values($this->order), array_keys($this->order)); + if (array_key_exists($responses, $flippedorder)) { + $data['answer'] = $flippedorder[$responses]; + } else { + $data['answer'] = '-1'; + } + } + + } else { + $responses = explode(',', $responses); + foreach ($this->order as $key => $ansid) { + if (in_array($ansid, $responses)) { + $data['choice' . $key] = 1; + } else { + $data['choice' . $key] = 0; + } + } + } + } + + public function load_dataset($selecteditem) { + $this->selecteditem = $selecteditem; + $this->values = $this->qeupdater->load_dataset( + $this->question->id, $selecteditem); + + // Prepare an array for {@link substitute_values()}. + $this->search = array(); + $this->safevalue = array(); + $this->prettyvalue = array(); + foreach ($this->values as $name => $value) { + if (!is_numeric($value)) { + $a = new stdClass(); + $a->name = '{' . $name . '}'; + $a->value = $value; + throw new moodle_exception('notvalidnumber', 'qtype_calculated', '', $a); + } + + $this->search[] = '{' . $name . '}'; + $this->safevalue[] = '(' . $value . ')'; + $this->prettyvalue[] = $this->format_float($value); + } + } + + /** + * Display a float properly formatted with a certain number of decimal places. + * @param $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 $x; + } + + /** + * Evaluate an expression using the variable values. + * @param string $expression the expression. A PHP expression with placeholders + * like {a} for where the variables need to go. + * @return float the computed result. + */ + public function calculate($expression) { + return $this->calculate_raw($this->substitute_values_for_eval($expression)); + } + + /** + * Evaluate an expression after the variable values have been substituted. + * @param string $expression the expression. A PHP expression with placeholders + * like {a} for where the variables need to go. + * @return float the computed result. + */ + protected function calculate_raw($expression) { + // This validation trick from http://php.net/manual/en/function.eval.php + if (!@eval('return true; $result = ' . $expression . ';')) { + return '[Invalid expression ' . $expression . ']'; + } + return eval('return ' . $expression . ';'); + } + + /** + * Substitute variable placehodlers like {a} with their value wrapped in (). + * @param string $expression the expression. A PHP expression with placeholders + * like {a} for where the variables need to go. + * @return string the expression with each placeholder replaced by the + * corresponding value. + */ + protected function substitute_values_for_eval($expression) { + return str_replace($this->search, $this->safevalue, $expression); + } + + /** + * Substitute variable placehodlers like {a} with their value without wrapping + * the value in anything. + * @param string $text some content with placeholders + * like {a} for where the variables need to go. + * @return string the expression with each placeholder replaced by the + * corresponding value. + */ + protected function substitute_values_pretty($text) { + return str_replace($this->search, $this->prettyvalue, $text); + } + + /** + * Replace any embedded variables (like {a}) or formulae (like {={a} + {b}}) + * in some text with the corresponding values. + * @param string $text the text to process. + * @return string the text with values substituted. + */ + 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, $format, $length) { + return $vs->format_float($vs->calculate($matches[1]), $length, $format); + }, $text); + return $this->substitute_values_pretty($text); + } +} diff --git a/question/type/calculatedsimple/db/simpletest/testupgradelibnewqe.php b/question/type/calculatedsimple/db/simpletest/testupgradelibnewqe.php new file mode 100644 index 00000000000..3b60b54b1ec --- /dev/null +++ b/question/type/calculatedsimple/db/simpletest/testupgradelibnewqe.php @@ -0,0 +1,634 @@ +. + +/** + * Tests of the upgrade to the new Moodle question engine for attempts at + * simple calculated questions. + * + * @package qtype + * @subpackage calculatedsimple + * @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/upgrade/simpletest/helper.php'); + + +/** + * Testing the upgrade of simple calculated question attempts. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_calculatedsimple_attempt_upgrader_test extends question_attempt_upgrader_test_base { + public function test_calculatedsimple_adaptive_qsession95() { + $quiz = (object) array( + 'id' => '4', + 'course' => '2', + 'name' => 'Calculated quiz', + 'intro' => '', + 'introformat' => '1', + 'timeopen' => '0', + 'timeclose' => '0', + 'attempts' => '0', + 'attemptonlast' => '0', + 'grademethod' => '1', + 'decimalpoints' => '2', + 'questiondecimalpoints' => '-1', + 'questionsperpage' => '1', + 'shufflequestions' => '0', + 'shuffleanswers' => '1', + 'questions' => '16,0,17,0,18,0', + 'sumgrades' => '3.00000', + 'grade' => '10.00000', + 'timecreated' => '0', + 'timemodified' => '1305648351', + 'timelimit' => '0', + 'password' => '', + 'subnet' => '', + 'popup' => '0', + 'delay1' => '0', + 'delay2' => '0', + 'showuserpicture' => '0', + 'showblocks' => '0', + 'preferredbehaviour' => 'adaptive', + 'reviewattempt' => '69888', + 'reviewcorrectness' => '69888', + 'reviewmarks' => '69888', + 'reviewspecificfeedback' => '69888', + 'reviewgeneralfeedback' => '69888', + 'reviewrightanswer' => '69888', + 'reviewoverallfeedback' => '4352', + ); + $attempt = (object) array( + 'id' => '13', + 'uniqueid' => '13', + 'quiz' => '4', + 'userid' => '4', + 'attempt' => '1', + 'sumgrades' => '0.00000', + 'timestart' => '1305830650', + 'timefinish' => '1305830656', + 'timemodified' => '1305830656', + 'layout' => '16,0,17,0,18,0', + 'preview' => '0', + 'needsupgradetonewqe' => 1, + ); + $question = (object) array( + 'id' => '16', + 'category' => '2', + 'parent' => '0', + 'name' => 'Calculated simple', + 'questiontext' => '

What is {={a}} + {={b}} ?

', + 'questiontextformat' => '1', + 'generalfeedback' => '', + 'generalfeedbackformat' => '1', + 'defaultmark' => '1.0000000', + 'penalty' => '0.1', + 'qtype' => 'calculatedsimple', + 'length' => '1', + 'stamp' => 'tjh238.vledev2.open.ac.uk+110517161007+2Barhu', + 'version' => 'tjh238.vledev2.open.ac.uk+110517161008+Mu6OQu', + 'hidden' => '0', + 'timecreated' => '1305648607', + 'timemodified' => '1305648607', + 'createdby' => '2', + 'modifiedby' => '2', + 'maxmark' => '1.0000000', + 'options' => (object) array( + 'synchronize' => 0, + 'single' => 0, + 'answernumbering' => 'abc', + 'shuffleanswers' => 0, + 'correctfeedback' => '', + 'partiallycorrectfeedback' => '', + 'incorrectfeedback' => '', + 'correctfeedbackformat' => 0, + 'partiallycorrectfeedbackformat' => 0, + 'incorrectfeedbackformat' => 0, + 'answers' => array( + 23 => (object) array( + 'id' => '23', + 'question' => '16', + 'answer' => '{a} + {b}', + 'answerformat' => '0', + 'fraction' => '1.0000000', + 'feedback' => '

Well done!

', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + ), + 'units' => array( + ), + 'unitgradingtype' => '0', + 'unitpenalty' => '0.1000000', + 'showunits' => '3', + 'unitsleft' => '0', + ), + 'hints' => array( + ), + ); + $qsession = (object) array( + 'id' => '95', + 'attemptid' => '13', + 'questionid' => '16', + 'newest' => '256', + 'newgraded' => '256', + 'sumpenalty' => '0.1000000', + 'manualcomment' => '', + 'manualcommentformat' => '1', + 'flagged' => '0', + ); + $qstates = array( + 253 => (object) array( + 'id' => '253', + 'attempt' => '13', + 'question' => '16', + 'seq_number' => '0', + 'answer' => 'dataset7-|||||', + 'timestamp' => '1305830650', + 'event' => '0', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.0000000', + ), + 256 => (object) array( + 'id' => '256', + 'attempt' => '13', + 'question' => '16', + 'seq_number' => '1', + 'answer' => 'dataset7-|||||', + 'timestamp' => '1305830650', + 'event' => '6', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.1000000', + ), + ); + $this->loader->put_dataset_in_cache($question->id, 7, array('a' => '3', 'b' => '6')); + + $qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates); + + $expectedqa = (object) array( + 'behaviour' => 'adaptive', + 'questionid' => 16, + 'maxmark' => 1.0000000, + 'minfraction' => 0, + 'flagged' => 0, + 'questionsummary' => 'What is 3 + 6 ?', + 'rightanswer' => '9', + 'responsesummary' => '', + 'timemodified' => 1305830650, + 'steps' => array( + 0 => (object) array( + 'sequencenumber' => 0, + 'state' => 'todo', + 'fraction' => null, + 'timecreated' => 1305830650, + 'userid' => 4, + 'data' => array('-_try' => 1, '_dataset' => '7', '_separators' => '.$,', + '_var_a' => '3', '_var_b' => '6'), + ), + 1 => (object) array( + 'sequencenumber' => 1, + 'state' => 'gradedwrong', + 'fraction' => 0, + 'timecreated' => 1305830650, + 'userid' => 4, + 'data' => array('answer' => '', '-finish' => 1, '-_try' => 1, '-_rawfraction' => 0), + ), + ), + ); + + $this->assertEqual($expectedqa, $qa); + } + + public function test_calculatedsimple_adaptive_qsession98() { + $quiz = (object) array( + 'id' => '4', + 'course' => '2', + 'name' => 'Calculated quiz', + 'intro' => '', + 'introformat' => '1', + 'timeopen' => '0', + 'timeclose' => '0', + 'attempts' => '0', + 'attemptonlast' => '0', + 'grademethod' => '1', + 'decimalpoints' => '2', + 'questiondecimalpoints' => '-1', + 'questionsperpage' => '1', + 'shufflequestions' => '0', + 'shuffleanswers' => '1', + 'questions' => '16,0,17,0,18,0', + 'sumgrades' => '3.00000', + 'grade' => '10.00000', + 'timecreated' => '0', + 'timemodified' => '1305648351', + 'timelimit' => '0', + 'password' => '', + 'subnet' => '', + 'popup' => '0', + 'delay1' => '0', + 'delay2' => '0', + 'showuserpicture' => '0', + 'showblocks' => '0', + 'preferredbehaviour' => 'adaptive', + 'reviewattempt' => '69888', + 'reviewcorrectness' => '69888', + 'reviewmarks' => '69888', + 'reviewspecificfeedback' => '69888', + 'reviewgeneralfeedback' => '69888', + 'reviewrightanswer' => '69888', + 'reviewoverallfeedback' => '4352', + ); + $attempt = (object) array( + 'id' => '14', + 'uniqueid' => '14', + 'quiz' => '4', + 'userid' => '4', + 'attempt' => '2', + 'sumgrades' => '2.80000', + 'timestart' => '1305830661', + 'timefinish' => '1305830729', + 'timemodified' => '1305830729', + 'layout' => '16,0,17,0,18,0', + 'preview' => '0', + 'needsupgradetonewqe' => 1, + ); + $question = (object) array( + 'id' => '16', + 'category' => '2', + 'parent' => '0', + 'name' => 'Calculated simple', + 'questiontext' => '

What is {={a}} + {={b}} ?

', + 'questiontextformat' => '1', + 'generalfeedback' => '', + 'generalfeedbackformat' => '1', + 'defaultmark' => '1.0000000', + 'penalty' => '0.1', + 'qtype' => 'calculatedsimple', + 'length' => '1', + 'stamp' => 'tjh238.vledev2.open.ac.uk+110517161007+2Barhu', + 'version' => 'tjh238.vledev2.open.ac.uk+110517161008+Mu6OQu', + 'hidden' => '0', + 'timecreated' => '1305648607', + 'timemodified' => '1305648607', + 'createdby' => '2', + 'modifiedby' => '2', + 'maxmark' => '1.0000000', + 'options' => (object) array( + 'synchronize' => 0, + 'single' => 0, + 'answernumbering' => 'abc', + 'shuffleanswers' => 0, + 'correctfeedback' => '', + 'partiallycorrectfeedback' => '', + 'incorrectfeedback' => '', + 'correctfeedbackformat' => 0, + 'partiallycorrectfeedbackformat' => 0, + 'incorrectfeedbackformat' => 0, + 'answers' => array( + 23 => (object) array( + 'id' => '23', + 'question' => '16', + 'answer' => '{a} + {b}', + 'answerformat' => '0', + 'fraction' => '1.0000000', + 'feedback' => '

Well done!

', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + ), + 'units' => array( + ), + 'unitgradingtype' => '0', + 'unitpenalty' => '0.1000000', + 'showunits' => '3', + 'unitsleft' => '0', + ), + 'hints' => array( + ), + ); + $qsession = (object) array( + 'id' => '98', + 'attemptid' => '14', + 'questionid' => '16', + 'newest' => '267', + 'newgraded' => '267', + 'sumpenalty' => '0.3000000', + 'manualcomment' => '', + 'manualcommentformat' => '1', + 'flagged' => '0', + ); + $qstates = array( + 259 => (object) array( + 'id' => '259', + 'attempt' => '14', + 'question' => '16', + 'seq_number' => '0', + 'answer' => 'dataset4-|||||', + 'timestamp' => '1305830661', + 'event' => '0', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.0000000', + ), + 262 => (object) array( + 'id' => '262', + 'attempt' => '14', + 'question' => '16', + 'seq_number' => '1', + 'answer' => 'dataset4-9.00|||||', + 'timestamp' => '1305830668', + 'event' => '3', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.1000000', + ), + 263 => (object) array( + 'id' => '263', + 'attempt' => '14', + 'question' => '16', + 'seq_number' => '2', + 'answer' => 'dataset4-15.40|||||', + 'timestamp' => '1305830679', + 'event' => '3', + 'grade' => '0.9000000', + 'raw_grade' => '1.0000000', + 'penalty' => '0.1000000', + ), + 267 => (object) array( + 'id' => '267', + 'attempt' => '14', + 'question' => '16', + 'seq_number' => '3', + 'answer' => 'dataset4-15.40|||||', + 'timestamp' => '1305830679', + 'event' => '6', + 'grade' => '0.9000000', + 'raw_grade' => '1.0000000', + 'penalty' => '0.1000000', + ), + ); + $this->loader->put_dataset_in_cache($question->id, 4, array('a' => '6.4', 'b' => '9')); + + $qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates); + + $expectedqa = (object) array( + 'behaviour' => 'adaptive', + 'questionid' => 16, + 'maxmark' => 1.0000000, + 'minfraction' => 0, + 'flagged' => 0, + 'questionsummary' => 'What is 6.4 + 9 ?', + 'rightanswer' => '15.4', + 'responsesummary' => '15.40', + 'timemodified' => 1305830679, + 'steps' => array( + 0 => (object) array( + 'sequencenumber' => 0, + 'state' => 'todo', + 'fraction' => null, + 'timecreated' => 1305830661, + 'userid' => 4, + 'data' => array('-_try' => 1, '_dataset' => '4', '_separators' => '.$,', + '_var_a' => '6.4', '_var_b' => '9'), + ), + 1 => (object) array( + 'sequencenumber' => 1, + 'state' => 'todo', + 'fraction' => 0, + 'timecreated' => 1305830668, + 'userid' => 4, + 'data' => array('answer' => '9.00', '-submit' => 1, '-_try' => 2, '-_rawfraction' => 0), + ), + 2 => (object) array( + 'sequencenumber' => 2, + 'state' => 'todo', + 'fraction' => 0.9, + 'timecreated' => 1305830679, + 'userid' => 4, + 'data' => array('answer' => '15.40', '-submit' => 1, '-_try' => 3, '-_rawfraction' => 1), + ), + 3 => (object) array( + 'sequencenumber' => 3, + 'state' => 'gradedright', + 'fraction' => 0.9, + 'timecreated' => 1305830679, + 'userid' => 4, + 'data' => array('answer' => '15.40', '-finish' => 1, '-_try' => 3, '-_rawfraction' => 1), + ), + ), + ); + + $this->assertEqual($expectedqa, $qa); + } + + public function test_calculatedsimple_adaptive_qsession101() { + $quiz = (object) array( + 'id' => '4', + 'course' => '2', + 'name' => 'Calculated quiz', + 'intro' => '', + 'introformat' => '1', + 'timeopen' => '0', + 'timeclose' => '0', + 'attempts' => '0', + 'attemptonlast' => '0', + 'grademethod' => '1', + 'decimalpoints' => '2', + 'questiondecimalpoints' => '-1', + 'questionsperpage' => '1', + 'shufflequestions' => '0', + 'shuffleanswers' => '1', + 'questions' => '16,0,17,0,18,0', + 'sumgrades' => '3.00000', + 'grade' => '10.00000', + 'timecreated' => '0', + 'timemodified' => '1305648351', + 'timelimit' => '0', + 'password' => '', + 'subnet' => '', + 'popup' => '0', + 'delay1' => '0', + 'delay2' => '0', + 'showuserpicture' => '0', + 'showblocks' => '0', + 'preferredbehaviour' => 'adaptive', + 'reviewattempt' => '69888', + 'reviewcorrectness' => '69888', + 'reviewmarks' => '69888', + 'reviewspecificfeedback' => '69888', + 'reviewgeneralfeedback' => '69888', + 'reviewrightanswer' => '69888', + 'reviewoverallfeedback' => '4352', + ); + $attempt = (object) array( + 'id' => '15', + 'uniqueid' => '15', + 'quiz' => '4', + 'userid' => '3', + 'attempt' => '1', + 'sumgrades' => '0.70000', + 'timestart' => '1305830744', + 'timefinish' => '0', + 'timemodified' => '1305830792', + 'layout' => '16,0,17,0,18,0', + 'preview' => '0', + 'needsupgradetonewqe' => 1, + ); + $question = (object) array( + 'id' => '16', + 'category' => '2', + 'parent' => '0', + 'name' => 'Calculated simple', + 'questiontext' => '

What is {={a}} + {={b}} ?

', + 'questiontextformat' => '1', + 'generalfeedback' => '', + 'generalfeedbackformat' => '1', + 'defaultmark' => '1.0000000', + 'penalty' => '0.1', + 'qtype' => 'calculatedsimple', + 'length' => '1', + 'stamp' => 'tjh238.vledev2.open.ac.uk+110517161007+2Barhu', + 'version' => 'tjh238.vledev2.open.ac.uk+110517161008+Mu6OQu', + 'hidden' => '0', + 'timecreated' => '1305648607', + 'timemodified' => '1305648607', + 'createdby' => '2', + 'modifiedby' => '2', + 'maxmark' => '1.0000000', + 'options' => (object) array( + 'synchronize' => 0, + 'single' => 0, + 'answernumbering' => 'abc', + 'shuffleanswers' => 0, + 'correctfeedback' => '', + 'partiallycorrectfeedback' => '', + 'incorrectfeedback' => '', + 'correctfeedbackformat' => 0, + 'partiallycorrectfeedbackformat' => 0, + 'incorrectfeedbackformat' => 0, + 'answers' => array( + 23 => (object) array( + 'id' => '23', + 'question' => '16', + 'answer' => '{a} + {b}', + 'answerformat' => '0', + 'fraction' => '1.0000000', + 'feedback' => '

Well done!

', + 'feedbackformat' => '1', + 'tolerance' => '0.01', + 'tolerancetype' => '1', + 'correctanswerlength' => '2', + 'correctanswerformat' => '1', + ), + ), + 'units' => array( + ), + 'unitgradingtype' => '0', + 'unitpenalty' => '0.1000000', + 'showunits' => '3', + 'unitsleft' => '0', + ), + 'hints' => array( + ), + ); + $qsession = (object) array( + 'id' => '101', + 'attemptid' => '15', + 'questionid' => '16', + 'newest' => '273', + 'newgraded' => '270', + 'sumpenalty' => '0.0000000', + 'manualcomment' => '', + 'manualcommentformat' => '1', + 'flagged' => '0', + ); + $qstates = array( + 270 => (object) array( + 'id' => '270', + 'attempt' => '15', + 'question' => '16', + 'seq_number' => '0', + 'answer' => 'dataset6-|||||', + 'timestamp' => '1305830744', + 'event' => '0', + 'grade' => '0.0000000', + 'raw_grade' => '0.0000000', + 'penalty' => '0.0000000', + ), + 273 => (object) array( + 'id' => '273', + 'attempt' => '15', + 'question' => '16', + 'seq_number' => '1', + 'answer' => 'dataset6-13.1|||||', + 'timestamp' => '1305830755', + 'event' => '2', + 'grade' => '0.0000000', + 'raw_grade' => '1.0000000', + 'penalty' => '0.1000000', + ), + ); + $this->loader->put_dataset_in_cache($question->id, 6, array('a' => '6.1', 'b' => '7')); + + $qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates); + + $expectedqa = (object) array( + 'behaviour' => 'adaptive', + 'questionid' => 16, + 'maxmark' => 1.0000000, + 'minfraction' => 0, + 'flagged' => 0, + 'questionsummary' => 'What is 6.1 + 7 ?', + 'rightanswer' => '13.1', + 'responsesummary' => '13.1', + 'timemodified' => 1305830755, + 'steps' => array( + 0 => (object) array( + 'sequencenumber' => 0, + 'state' => 'todo', + 'fraction' => null, + 'timecreated' => 1305830744, + 'userid' => 3, + 'data' => array('-_try' => 1, '_dataset' => '6', '_separators' => '.$,', + '_var_a' => '6.1', '_var_b' => '7'), + ), + 1 => (object) array( + 'sequencenumber' => 1, + 'state' => 'complete', + 'fraction' => null, + 'timecreated' => 1305830755, + 'userid' => 3, + 'data' => array('answer' => 13.1), + ), + ), + ); + + $this->assertEqual($expectedqa, $qa); + } +} diff --git a/question/type/calculatedsimple/db/upgradelib.php b/question/type/calculatedsimple/db/upgradelib.php new file mode 100644 index 00000000000..be6ddf0a773 --- /dev/null +++ b/question/type/calculatedsimple/db/upgradelib.php @@ -0,0 +1,42 @@ +. + +/** + * Upgrade library code for the simple calculated question type. + * + * @package qtype + * @subpackage calculatedsimple + * @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/db/upgradelib.php'); + + +/** + * Class for converting attempt data for simple calculated questions when upgrading + * attempts to the new question engine. + * + * This class is used by the code in question/engine/upgrade/upgradelib.php. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_calculatedsimple_qe2_attempt_updater extends qtype_calculated_qe2_attempt_updater { +}