/**
* Start a normal, new, quiz attempt.
*
- * @param quiz $quizobj the quiz object to start an attempt for.
- * @param question_usage_by_activity $quba
- * @param object $attempt
- * @param integer $attemptnumber starting from 1
- * @param integer $timenow the attempt start time
- * @param array $questionids slot number => question id. Used for random questions, to force the choice of a particular actual
- * question. Intended for testing purposes only.
+ * @param quiz $quizobj the quiz object to start an attempt for.
+ * @param question_usage_by_activity $quba
+ * @param object $attempt
+ * @param integer $attemptnumber starting from 1
+ * @param integer $timenow the attempt start time
+ * @param array $questionids slot number => question id. Used for random questions, to force the choice
+ * of a particular actual question. Intended for testing purposes only.
+ * @param array $forcedvariantsbyslot slot number => variant. Used for questions with variants,
+ * to force the choice of a particular variant. Intended for testing
+ * purposes only.
* @throws moodle_exception
- * @return object modified attempt object
+ * @return object modified attempt object
*/
-function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, $questionids = array()) {
+function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow,
+ $questionids = array(), $forcedvariantsbyslot = array()) {
// Fully load all the questions in this quiz.
$quizobj->preload_questions();
$quizobj->load_questions();
} else {
$variantoffset = $attemptnumber;
}
- $quba->start_all_questions(
- new question_variant_pseudorandom_no_repeats_strategy($variantoffset), $timenow);
+ $variantstrategy = new question_variant_pseudorandom_no_repeats_strategy($variantoffset);
+
+ if (!empty($forcedvariantsbyslot)) {
+ $forcedvariantsbyseed = question_variant_forced_choices_selection_strategy::prepare_forced_choices_array(
+ $forcedvariantsbyslot, $quba);
+ $variantstrategy = new question_variant_forced_choices_selection_strategy(
+ $forcedvariantsbyseed, $variantstrategy);
+ }
+
+ $quba->start_all_questions($variantstrategy, $timenow);
// Update attempt layout.
$newlayout = array();
$this->assertEquals(100, $gradebookgrade->grade);
}
}
+
+
+ public function get_correct_response_for_variants() {
+ return array(array(1, 9.9), array(2, 8.5), array(5, 14.2), array(10, 6.8, true));
+ }
+
+ protected $quizwithvariants = null;
+
+ /**
+ * Create a quiz with a single question with variants and walk through quiz attempts.
+ *
+ * @dataProvider get_correct_response_for_variants
+ */
+ public function test_quiz_with_question_with_variants_attempt_walkthrough($variantno, $correctresponse, $done = false) {
+ global $SITE;
+
+ $this->resetAfterTest($done);
+
+ $this->setAdminUser();
+
+ if ($this->quizwithvariants === null) {
+ // Make a quiz.
+ $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+
+ $this->quizwithvariants = $quizgenerator->create_instance(array('course'=>$SITE->id,
+ 'questionsperpage' => 0,
+ 'grade' => 100.0,
+ 'sumgrades' => 1));
+
+ $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+
+ $cat = $questiongenerator->create_question_category();
+ $calc = $questiongenerator->create_question('calculatedsimple', 'sumwithvariants', array('category' => $cat->id));
+ quiz_add_quiz_question($calc->id, $this->quizwithvariants, 0);
+ }
+
+
+ // Make a new user to do the quiz.
+ $user1 = $this->getDataGenerator()->create_user();
+ $this->setUser($user1);
+ $quizobj = quiz::create($this->quizwithvariants->id, $user1->id);
+
+ // Start the attempt.
+ $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+ $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
+
+ $timenow = time();
+ $attempt = quiz_create_attempt($quizobj, 1, false, $timenow);
+ // Select variant.
+ quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, array(), array(1 => $variantno));
+ quiz_attempt_save_started($quizobj, $quba, $attempt);
+
+ // Process some responses from the student.
+ $attemptobj = quiz_attempt::create($attempt->id);
+ $tosubmit = array(1 => array('answer' => $correctresponse));
+ $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
+
+ // Finish the attempt.
+ $attemptobj = quiz_attempt::create($attempt->id);
+ $attemptobj->process_finish($timenow, false);
+
+ // Re-load quiz attempt data.
+ $attemptobj = quiz_attempt::create($attempt->id);
+
+ // Check that results are stored as expected.
+ $this->assertEquals(1, $attemptobj->get_attempt_number());
+ $this->assertEquals(1, $attemptobj->get_sum_marks());
+ $this->assertEquals(true, $attemptobj->is_finished());
+ $this->assertEquals($timenow, $attemptobj->get_submitted_date());
+ $this->assertEquals($user1->id, $attemptobj->get_userid());
+
+ // Check quiz grades.
+ $grades = quiz_get_user_grades($this->quizwithvariants, $user1->id);
+ $grade = array_shift($grades);
+ $this->assertEquals(100.0, $grade->rawgrade);
+
+ // Check grade book.
+ $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $this->quizwithvariants->id, $user1->id);
+ $gradebookitem = array_shift($gradebookgrades->items);
+ $gradebookgrade = array_shift($gradebookitem->grades);
+ $this->assertEquals(100, $gradebookgrade->grade);
+ }
+
}
$this->records = null;
}
}
+
+/**
+ * A {@link question_variant_selection_strategy} designed for testing.
+ * For selected. questions it wil return a specific variants. In for the other
+ * slots it will use a fallback strategy.
+ *
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_variant_forced_choices_selection_strategy
+ implements question_variant_selection_strategy {
+
+ /** @var array seed => variant to select. */
+ protected $forcedchoices;
+
+ /** @var question_variant_selection_strategy strategy used to make the non-forced choices. */
+ protected $basestrategy;
+
+ /**
+ * Constructor.
+ * @param array $forcedchoice array seed => variant to select.
+ * @param question_variant_selection_strategy $basestrategy strategy used
+ * to make the non-forced choices.
+ */
+ public function __construct(array $forcedchoices, question_variant_selection_strategy $basestrategy) {
+ $this->forcedchoices = $forcedchoices;
+ $this->basestrategy = $basestrategy;
+ }
+
+ public function choose_variant($maxvariants, $seed) {
+ if (array_key_exists($seed, $this->forcedchoices)) {
+ if ($this->forcedchoices[$seed] > $maxvariants) {
+ throw new coding_exception('Forced variant out of range.');
+ }
+ return $this->forcedchoices[$seed];
+ } else {
+ return $this->basestrategy->choose_variant($maxvariants, $seed);
+ }
+ }
+
+ /**
+ * Helper method for preparing the $forcedchoices array.
+ * @param array $variantsbyslot slot number => variant to select.
+ * @param question_usage_by_activity $quba the question usage we need a strategy for.
+ * @return array that can be passed to the constructor as $forcedchoices.
+ */
+ public static function prepare_forced_choices_array(array $variantsbyslot,
+ question_usage_by_activity $quba) {
+
+ $forcedchoices = array();
+
+ foreach ($variantsbyslot as $slot => $varianttochoose) {
+ $question = $quba->get_question($slot);
+ $seed = $question->get_variants_selection_seed();
+ if (array_key_exists($seed, $forcedchoices) && $forcedchoices[$seed] != $varianttochoose) {
+ throw new coding_exception('Inconsistent forced variant detected at slot ' . $slot);
+ }
+ if ($varianttochoose > $question->get_num_variants()) {
+ throw new coding_exception('Forced variant out of range at slot ' . $slot);
+ }
+ $forcedchoices[$seed] = $varianttochoose;
+ }
+ return $forcedchoices;
+ }
+}