/** * Test update question flag */ public function test_core_question_update_flag() { $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); // Create a question category. $cat = $questiongenerator->create_question_category(); $quba = question_engine::make_questions_usage_by_activity('core_question_update_flag', context_system::instance()); $quba->set_preferred_behaviour('deferredfeedback'); $questiondata = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); $question = question_bank::load_question($questiondata->id); $slot = $quba->add_question($question); $qa = $quba->get_question_attempt($slot); self::setUser($this->student); $quba->start_all_questions(); question_engine::save_questions_usage_by_activity($quba); $qubaid = $quba->get_id(); $questionid = $question->id; $qaid = $qa->get_database_id(); $checksum = md5($qubaid . "_" . $this->student->secret . "_" . $questionid . "_" . $qaid . "_" . $slot); $flag = core_question_external::update_flag($qubaid, $questionid, $qaid, $slot, $checksum, true); $this->assertTrue($flag['status']); // Test invalid checksum. try { // Using random_string to force failing. $checksum = md5($qubaid . "_" . random_string(11) . "_" . $questionid . "_" . $qaid . "_" . $slot); core_question_external::update_flag($qubaid, $questionid, $qaid, $slot, $checksum, true); $this->fail('Exception expected due to invalid checksum.'); } catch (moodle_exception $e) { $this->assertEquals('errorsavingflags', $e->errorcode); } }
/** * Run all the question tests for all variants of all questions belonging to * a given context. * * Does output as we go along. * * @param context $context the context to run the tests for. * @return array with two elements: * bool true if all the tests passed, else false. * array of messages relating to the questions with failures. */ public function run_all_tests_for_context(context $context) { global $DB, $OUTPUT; // Load the necessary data. $categories = question_category_options(array($context)); $categories = reset($categories); $questiontestsurl = new moodle_url('/question/type/stack/questiontestrun.php'); if ($context->contextlevel == CONTEXT_COURSE) { $questiontestsurl->param('courseid', $context->instanceid); } else { if ($context->contextlevel == CONTEXT_MODULE) { $questiontestsurl->param('cmid', $context->instanceid); } } $allpassed = true; $failingtests = array(); foreach ($categories as $key => $category) { list($categoryid) = explode(',', $key); echo $OUTPUT->heading($category, 3); $questionids = $DB->get_records_menu('question', array('category' => $categoryid, 'qtype' => 'stack'), 'name', 'id,name'); if (!$questionids) { continue; } echo html_writer::tag('p', stack_string('replacedollarscount', count($questionids))); foreach ($questionids as $questionid => $name) { $tests = question_bank::get_qtype('stack')->load_question_tests($questionid); if (!$tests) { echo $OUTPUT->heading(html_writer::link(new moodle_url($questiontestsurl, array('questionid' => $questionid)), format_string($name)), 4); echo html_writer::tag('p', stack_string('bulktestnotests')); continue; } $question = question_bank::load_question($questionid); $questionname = format_string($name); $previewurl = new moodle_url($questiontestsurl, array('questionid' => $questionid)); if (empty($question->deployedseeds)) { $questionnamelink = html_writer::link($previewurl, $questionname); echo $OUTPUT->heading($questionnamelink, 4); list($ok, $message) = $this->qtype_stack_test_question($question, $tests); if (!$ok) { $allpassed = false; $failingtests[] = $questionnamelink . ': ' . $message; } } else { echo $OUTPUT->heading(format_string($name), 4); foreach ($question->deployedseeds as $seed) { $previewurl->param('seed', $seed); $questionnamelink = html_writer::link($previewurl, stack_string('seedx', $seed)); echo $OUTPUT->heading($questionnamelink, 4); list($ok, $message) = $this->qtype_stack_test_question($question, $tests, $seed); if (!$ok) { $allpassed = false; $failingtests[] = $questionname . ' ' . $questionnamelink . ': ' . $message; } } } } } return array($allpassed, $failingtests); }
/** * We create two usages, each with two questions, a short-answer marked * out of 5, and and essay marked out of 10. We just start these attempts. * * Then we change the max mark for the short-answer question in one of the * usages to 20, using a qubaid_list, and verify. * * Then we change the max mark for the essay question in the other * usage to 2, using a qubaid_join, and verify. */ public function test_set_max_mark_in_attempts() { // Set up some things the tests will need. $this->resetAfterTest(); $dm = new question_engine_data_mapper(); // Create the questions. $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); $sa = $generator->create_question('shortanswer', null, array('category' => $cat->id)); $essay = $generator->create_question('essay', null, array('category' => $cat->id)); // Create the first usage. $q = question_bank::load_question($sa->id); $this->start_attempt_at_question($q, 'interactive', 5); $q = question_bank::load_question($essay->id); $this->start_attempt_at_question($q, 'interactive', 10); $this->finish(); $this->save_quba(); $usage1id = $this->quba->get_id(); // Create the second usage. $this->quba = question_engine::make_questions_usage_by_activity('unit_test', context_system::instance()); $q = question_bank::load_question($sa->id); $this->start_attempt_at_question($q, 'interactive', 5); $this->process_submission(array('answer' => 'fish')); $q = question_bank::load_question($essay->id); $this->start_attempt_at_question($q, 'interactive', 10); $this->finish(); $this->save_quba(); $usage2id = $this->quba->get_id(); // Test set_max_mark_in_attempts with a qubaid_list. $usagestoupdate = new qubaid_list(array($usage1id)); $dm->set_max_mark_in_attempts($usagestoupdate, 1, 20.0); $quba1 = question_engine::load_questions_usage_by_activity($usage1id); $quba2 = question_engine::load_questions_usage_by_activity($usage2id); $this->assertEquals(20, $quba1->get_question_max_mark(1)); $this->assertEquals(10, $quba1->get_question_max_mark(2)); $this->assertEquals(5, $quba2->get_question_max_mark(1)); $this->assertEquals(10, $quba2->get_question_max_mark(2)); // Test set_max_mark_in_attempts with a qubaid_join. $usagestoupdate = new qubaid_join('{question_usages} qu', 'qu.id', 'qu.id = :usageid', array('usageid' => $usage2id)); $dm->set_max_mark_in_attempts($usagestoupdate, 2, 2.0); $quba1 = question_engine::load_questions_usage_by_activity($usage1id); $quba2 = question_engine::load_questions_usage_by_activity($usage2id); $this->assertEquals(20, $quba1->get_question_max_mark(1)); $this->assertEquals(10, $quba1->get_question_max_mark(2)); $this->assertEquals(5, $quba2->get_question_max_mark(1)); $this->assertEquals(2, $quba2->get_question_max_mark(2)); // Test the nothing to do case. $usagestoupdate = new qubaid_join('{question_usages} qu', 'qu.id', 'qu.id = :usageid', array('usageid' => -1)); $dm->set_max_mark_in_attempts($usagestoupdate, 2, 2.0); $quba1 = question_engine::load_questions_usage_by_activity($usage1id); $quba2 = question_engine::load_questions_usage_by_activity($usage2id); $this->assertEquals(20, $quba1->get_question_max_mark(1)); $this->assertEquals(10, $quba1->get_question_max_mark(2)); $this->assertEquals(5, $quba2->get_question_max_mark(1)); $this->assertEquals(2, $quba2->get_question_max_mark(2)); }
public function test_question_creation() { $this->resetAfterTest(); question_bank::get_qtype('random')->clear_caches_before_testing(); $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); $question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); $question2 = $generator->create_question('numerical', null, array('category' => $cat->id)); $randomquestion = $generator->create_question('random', null, array('category' => $cat->id)); $expectedids = array($question1->id, $question2->id); $actualids = question_bank::get_qtype('random')->get_available_questions_from_category($cat->id, 0); sort($expectedids); sort($actualids); $this->assertEquals($expectedids, $actualids); $q = question_bank::load_question($randomquestion->id); $this->assertContains($q->id, array($question1->id, $question2->id)); }
public function test_get_questions_from_categories_with_usage_counts() { $this->resetAfterTest(); $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); $questiondata1 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); $questiondata2 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); $questiondata3 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); $quba = question_engine::make_questions_usage_by_activity('test', context_system::instance()); $quba->set_preferred_behaviour('deferredfeedback'); $question1 = question_bank::load_question($questiondata1->id); $question3 = question_bank::load_question($questiondata3->id); $quba->add_question($question1); $quba->add_question($question1); $quba->add_question($question3); $quba->start_all_questions(); question_engine::save_questions_usage_by_activity($quba); $this->assertEquals(array($questiondata2->id => 0, $questiondata3->id => 1, $questiondata1->id => 2), question_bank::get_finder()->get_questions_from_categories_with_usage_counts(array($cat->id), new qubaid_list(array($quba->get_id())))); }
public function test_second_attempt_uses_other_dataset() { global $DB; $this->resetAfterTest(); $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); $questiondata = $generator->create_question('calculated', null, array('category' => $cat->id)); // Create two dataset items. $adefinitionid = $DB->get_field_sql("\n SELECT qdd.id\n FROM {question_dataset_definitions} qdd\n JOIN {question_datasets} qd ON qd.datasetdefinition = qdd.id\n WHERE qd.question = ?\n AND qdd.name = ?", array($questiondata->id, 'a')); $bdefinitionid = $DB->get_field_sql("\n SELECT qdd.id\n FROM {question_dataset_definitions} qdd\n JOIN {question_datasets} qd ON qd.datasetdefinition = qdd.id\n WHERE qd.question = ?\n AND qdd.name = ?", array($questiondata->id, 'b')); $DB->set_field('question_dataset_definitions', 'itemcount', 2, array('id' => $adefinitionid)); $DB->set_field('question_dataset_definitions', 'itemcount', 2, array('id' => $bdefinitionid)); $DB->insert_record('question_dataset_items', array('definition' => $adefinitionid, 'itemnumber' => 1, 'value' => 3)); $DB->insert_record('question_dataset_items', array('definition' => $bdefinitionid, 'itemnumber' => 1, 'value' => 7)); $DB->insert_record('question_dataset_items', array('definition' => $adefinitionid, 'itemnumber' => 2, 'value' => 6)); $DB->insert_record('question_dataset_items', array('definition' => $bdefinitionid, 'itemnumber' => 2, 'value' => 4)); $question = question_bank::load_question($questiondata->id); $quba1 = question_engine::make_questions_usage_by_activity('test', context_system::instance()); $quba1->set_preferred_behaviour('deferredfeedback'); $slot1 = $quba1->add_question($question); $quba1->start_all_questions(new core_question\engine\variants\least_used_strategy($quba1, new qubaid_list(array()))); question_engine::save_questions_usage_by_activity($quba1); $variant1 = $quba1->get_variant($slot1); // Second attempt should use the other variant. $quba2 = question_engine::make_questions_usage_by_activity('test', context_system::instance()); $quba2->set_preferred_behaviour('deferredfeedback'); $slot2 = $quba2->add_question($question); $quba2->start_all_questions(new core_question\engine\variants\least_used_strategy($quba1, new qubaid_list(array($quba1->get_id())))); question_engine::save_questions_usage_by_activity($quba2); $variant2 = $quba2->get_variant($slot2); $this->assertNotEquals($variant1, $variant2); // Third attempt uses either variant at random. $quba3 = question_engine::make_questions_usage_by_activity('test', context_system::instance()); $quba3->set_preferred_behaviour('deferredfeedback'); $slot3 = $quba3->add_question($question); $quba3->start_all_questions(new core_question\engine\variants\least_used_strategy($quba1, new qubaid_list(array($quba1->get_id(), $quba2->get_id())))); $variant3 = $quba3->get_variant($slot3); $this->assertTrue($variant3 == $variant1 || $variant3 == $variant2); }
public function test_autosave_with_wrong_seq_number_ignored() { $this->resetAfterTest(); $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); $question = $generator->create_question('shortanswer', null, array('category' => $cat->id)); // Start attempt at a shortanswer question. $q = question_bank::load_question($question->id); $this->start_attempt_at_question($q, 'deferredfeedback', 1); $this->check_current_state(question_state::$todo); $this->check_current_mark(null); $this->check_step_count(1); // Process a response and check the expected result. $this->process_submission(array('answer' => 'first response')); $this->check_current_state(question_state::$complete); $this->check_current_mark(null); $this->check_step_count(2); $this->save_quba(); // Now check how that is re-displayed. $this->render(); $this->check_output_contains_text_input('answer', 'first response'); $this->check_output_contains_hidden_input(':sequencecheck', 2); // Process an autosave with a sequence number 1 too small (so from the past). $this->load_quba(); $postdata = $this->response_data_to_post(array('answer' => 'obsolete response')); $postdata[$this->quba->get_field_prefix($this->slot) . ':sequencecheck'] = $this->get_question_attempt()->get_sequence_check_count() - 1; $this->quba->process_all_autosaves(null, $postdata); $this->check_current_state(question_state::$complete); $this->check_current_mark(null); $this->check_step_count(2); $this->save_quba(); // Now check how that is re-displayed. $this->load_quba(); $this->render(); $this->check_output_contains_text_input('answer', 'first response'); $this->check_output_contains_hidden_input(':sequencecheck', 2); $this->delete_quba(); }
$maxfailedattempts = 3; $failedattempts = 0; $numberdeployed = 0; while ($failedattempts < $maxfailedattempts && $numberdeployed < $deploy) { // Genrate a new seed. $seed = mt_rand(); $variantdeployed = false; // Reload the question to ensure any new deployed version is included. $question = question_bank::load_question($questionid); $question->seed = (int) $seed; $quba = question_engine::make_questions_usage_by_activity('qtype_stack', $context); $quba->set_preferred_behaviour('adaptive'); $slot = $quba->add_question($question, $question->defaultmark); $quba->start_question($slot); foreach ($question->deployedseeds as $key => $deployedseed) { $qn = question_bank::load_question($questionid); $qn->seed = (int) $deployedseed; $cn = $qn->get_context(); $qunote = question_engine::make_questions_usage_by_activity('qtype_stack', $cn); $qunote->set_preferred_behaviour('adaptive'); $slotnote = $qunote->add_question($qn, $qn->defaultmark); $qunote->start_question($slotnote); // Check if the question note has already been deployed. if ($qn->get_question_summary() == $question->get_question_summary()) { $variantdeployed = true; $failedattempts++; } } if (!$variantdeployed) { // Load the list of test cases. $testscases = question_bank::get_qtype('stack')->load_question_tests($question->id);
/** * 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 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 */ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, $questionids = array(), $forcedvariantsbyslot = array()) { // Usages for this user's previous quiz attempts. $qubaids = new \mod_quiz\question\qubaids_for_users_attempts($quizobj->get_quizid(), $attempt->userid); // Fully load all the questions in this quiz. $quizobj->preload_questions(); $quizobj->load_questions(); // First load all the non-random questions. $randomfound = false; $slot = 0; $questions = array(); $maxmark = array(); $page = array(); foreach ($quizobj->get_questions() as $questiondata) { $slot += 1; $maxmark[$slot] = $questiondata->maxmark; $page[$slot] = $questiondata->page; if ($questiondata->qtype == 'random') { $randomfound = true; continue; } if (!$quizobj->get_quiz()->shuffleanswers) { $questiondata->options->shuffleanswers = false; } $questions[$slot] = question_bank::make_question($questiondata); } // Then find a question to go in place of each random question. if ($randomfound) { $slot = 0; $usedquestionids = array(); foreach ($questions as $question) { if (isset($usedquestions[$question->id])) { $usedquestionids[$question->id] += 1; } else { $usedquestionids[$question->id] = 1; } } $randomloader = new \core_question\bank\random_question_loader($qubaids, $usedquestionids); foreach ($quizobj->get_questions() as $questiondata) { $slot += 1; if ($questiondata->qtype != 'random') { continue; } // Deal with fixed random choices for testing. if (isset($questionids[$quba->next_slot_number()])) { if ($randomloader->is_question_available($questiondata->category, (bool) $questiondata->questiontext, $questionids[$quba->next_slot_number()])) { $questions[$slot] = question_bank::load_question($questionids[$quba->next_slot_number()], $quizobj->get_quiz()->shuffleanswers); continue; } else { throw new coding_exception('Forced question id not available.'); } } // Normal case, pick one at random. $questionid = $randomloader->get_next_question_id($questiondata->category, (bool) $questiondata->questiontext); if ($questionid === null) { throw new moodle_exception('notenoughrandomquestions', 'quiz', $quizobj->view_url(), $questiondata); } $questions[$slot] = question_bank::load_question($questionid, $quizobj->get_quiz()->shuffleanswers); } } // Finally add them all to the usage. ksort($questions); foreach ($questions as $slot => $question) { $newslot = $quba->add_question($question, $maxmark[$slot]); if ($newslot != $slot) { throw new coding_exception('Slot numbers have got confused.'); } } // Start all the questions. $variantstrategy = new core_question\engine\variants\least_used_strategy($quba, $qubaids); 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); // Work out the attempt layout. $sections = $quizobj->get_sections(); foreach ($sections as $i => $section) { if (isset($sections[$i + 1])) { $sections[$i]->lastslot = $sections[$i + 1]->firstslot - 1; } else { $sections[$i]->lastslot = count($questions); } } $layout = array(); foreach ($sections as $section) { if ($section->shufflequestions) { $questionsinthissection = array(); for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) { $questionsinthissection[] = $slot; } shuffle($questionsinthissection); $questionsonthispage = 0; foreach ($questionsinthissection as $slot) { if ($questionsonthispage && $questionsonthispage == $quizobj->get_quiz()->questionsperpage) { $layout[] = 0; $questionsonthispage = 0; } $layout[] = $slot; $questionsonthispage += 1; } } else { $currentpage = $page[$section->firstslot]; for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) { if ($currentpage !== null && $page[$slot] != $currentpage) { $layout[] = 0; } $layout[] = $slot; $currentpage = $page[$slot]; } } // Each section ends with a page break. $layout[] = 0; } $attempt->layout = implode(',', $layout); return $attempt; }
public function test_deferred_feedback_plain_attempt_on_last() { global $CFG, $USER; $this->resetAfterTest(true); $this->setAdminUser(); $usercontextid = context_user::instance($USER->id)->id; // Create an essay question in the DB. $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); $question = $generator->create_question('essay', 'plain', array('category' => $cat->id)); // Start attempt at the question. $q = question_bank::load_question($question->id); $this->start_attempt_at_question($q, 'deferredfeedback', 1); $this->check_current_state(question_state::$todo); $this->check_current_mark(null); $this->check_step_count(1); // Process a response and check the expected result. $this->process_submission(array( 'answer' => 'Once upon a time there was a frog called Freddy. He lived happily ever after.', 'answerformat' => FORMAT_PLAIN, )); $this->check_current_state(question_state::$complete); $this->check_current_mark(null); $this->check_step_count(2); $this->save_quba(); // Now submit all and finish. $this->finish(); $this->check_current_state(question_state::$needsgrading); $this->check_current_mark(null); $this->check_step_count(3); $this->save_quba(); // Now start a new attempt based on the old one. $this->load_quba(); $oldqa = $this->get_question_attempt(); $q = question_bank::load_question($question->id); $this->quba = question_engine::make_questions_usage_by_activity('unit_test', context_system::instance()); $this->quba->set_preferred_behaviour('deferredfeedback'); $this->slot = $this->quba->add_question($q, 1); $this->quba->start_question_based_on($this->slot, $oldqa); $this->check_current_state(question_state::$complete); $this->check_current_mark(null); $this->check_step_count(1); $this->save_quba(); // Check the display. $this->load_quba(); $this->render(); // Test taht no HTML comment has been added to the response. $this->assertRegExp('/Once upon a time there was a frog called Freddy. He lived happily ever after.(?!<!--)/', $this->currentoutput); // Test for the hash of an empty file area. $this->assertNotContains('d41d8cd98f00b204e9800998ecf8427e', $this->currentoutput); }
/** * Load the definition of another question picked randomly by this question. * @param object $questiondata the data defining a random question. * @param array $excludedquestions of question ids. We will no pick any * question whose id is in this list. * @param bool $allowshuffle if false, then any shuffle option on the * selected quetsion is disabled. * @return question_definition|null the definition of the question that was * selected, or null if no suitable question could be found. */ public function choose_other_question($questiondata, $excludedquestions, $allowshuffle = true) { $available = $this->get_available_questions_from_category($questiondata->category, !empty($questiondata->questiontext)); shuffle($available); foreach ($available as $questionid) { if (in_array($questionid, $excludedquestions)) { continue; } $question = question_bank::load_question($questionid, $allowshuffle); $this->set_selected_question_name($question, $questiondata->name); return $question; } return null; }
/** * Choose and load the desired number of questions. * @return array of short answer questions. */ public function load_questions() { if ($this->choose > count($this->availablequestions)) { throw new coding_exception('notenoughtshortanswerquestions'); } $questionids = draw_rand_array($this->availablequestions, $this->choose); $questions = array(); foreach ($questionids as $questionid) { $questions[] = question_bank::load_question($questionid); } return $questions; }
/** * Whether it is possible for another question to depend on this one finishing. * Note that the answer is not exact, because of random questions, and sometimes * questions cannot be depended upon because of quiz options. * @param int $slotnumber the index of the slot in question. * @return bool can this question finish naturally during the attempt? */ public function can_finish_during_the_attempt($slotnumber) { if ($this->quizobj->get_quiz()->shufflequestions || $this->quizobj->get_navigation_method() == QUIZ_NAVMETHOD_SEQ) { return false; } if ($this->get_question_type_for_slot($slotnumber) == 'random') { return true; } if (isset($this->slotsinorder[$slotnumber]->canfinish)) { return $this->slotsinorder[$slotnumber]->canfinish; } $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $this->quizobj->get_context()); $tempslot = $quba->add_question(\question_bank::load_question($this->slotsinorder[$slotnumber]->questionid)); $quba->set_preferred_behaviour($this->quizobj->get_quiz()->preferredbehaviour); $quba->start_all_questions(); $this->slotsinorder[$slotnumber]->canfinish = $quba->can_question_finish_during_attempt($tempslot); return $this->slotsinorder[$slotnumber]->canfinish; }
* * @package moodlecore * @subpackage questionengine * @copyright Alex Smith {@link http://maths.york.ac.uk/serving_maths} and * numerous contributors. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(dirname(__FILE__) . '/../config.php'); require_once($CFG->libdir . '/questionlib.php'); require_once(dirname(__FILE__) . '/previewlib.php'); // Get and validate question id. $id = required_param('id', PARAM_INT); $question = question_bank::load_question($id); require_login(); $category = $DB->get_record('question_categories', array('id' => $question->category), '*', MUST_EXIST); question_require_capability_on($question, 'use'); $PAGE->set_pagelayout('popup'); $PAGE->set_context(get_context_instance_by_id($category->contextid)); // Get and validate display options. $maxvariant = $question->get_num_variants(); $options = new question_preview_options($question); $options->load_user_defaults(); $options->set_from_request(); $PAGE->set_url(question_preview_url($id, $options->behaviour, $options->maxmark, $options)); // Get and validate exitsing preview, or start a new one.
public function test_finish_with_unhandled_autosave_data() { $this->resetAfterTest(); $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); $question = $generator->create_question('shortanswer', null, array('category' => $cat->id)); // Start attempt at a shortanswer question. $q = question_bank::load_question($question->id); $this->start_attempt_at_question($q, 'deferredfeedback', 1); $this->check_current_state(question_state::$todo); $this->check_current_mark(null); $this->check_step_count(1); // Process a response and check the expected result. $this->process_submission(array('answer' => 'cat')); $this->check_current_state(question_state::$complete); $this->check_current_mark(null); $this->check_step_count(2); $this->save_quba(); // Now check how that is re-displayed. $this->render(); $this->check_output_contains_text_input('answer', 'cat'); $this->check_output_contains_hidden_input(':sequencecheck', 2); // Process an autosave. $this->load_quba(); $this->process_autosave(array('answer' => 'frog')); $this->check_current_state(question_state::$complete); $this->check_current_mark(null); $this->check_step_count(3); $this->save_quba(); // Now check how that is re-displayed. $this->load_quba(); $this->render(); $this->check_output_contains_text_input('answer', 'frog'); $this->check_output_contains_hidden_input(':sequencecheck', 2); // Now finishe the attempt, without having done anything since the autosave. $this->finish(); $this->save_quba(); // Now check how that has been graded and is re-displayed. $this->load_quba(); $this->check_current_state(question_state::$gradedright); $this->check_current_mark(1); $this->render(); $this->check_output_contains_text_input('answer', 'frog', false); $this->check_output_contains_hidden_input(':sequencecheck', 4); $this->delete_quba(); }
public function test_deferred_feedback_html_editor_with_files_attempt_on_last_no_files_uploaded() { global $CFG, $USER; $this->resetAfterTest(true); $this->setAdminUser(); $usercontextid = context_user::instance($USER->id)->id; $fs = get_file_storage(); // Create an essay question in the DB. $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); $question = $generator->create_question('essay', 'editorfilepicker', array('category' => $cat->id)); // Start attempt at the question. $q = question_bank::load_question($question->id); $this->start_attempt_at_question($q, 'deferredfeedback', 1); $this->check_current_state(question_state::$todo); $this->check_current_mark(null); $this->check_step_count(1); // Process a response and check the expected result. // First we need to get the draft item ids. $this->render(); if (!preg_match('/env=editor&.*?itemid=(\\d+)&/', $this->currentoutput, $matches)) { throw new coding_exception('Editor draft item id not found.'); } $editordraftid = $matches[1]; if (!preg_match('/env=filemanager&action=browse&.*?itemid=(\\d+)&/', $this->currentoutput, $matches)) { throw new coding_exception('File manager draft item id not found.'); } $attachementsdraftid = $matches[1]; $this->process_submission(array('answer' => 'I refuse to draw you a picture, so there!', 'answerformat' => FORMAT_HTML, 'answer:itemid' => $editordraftid, 'attachments' => $attachementsdraftid)); $this->check_current_state(question_state::$complete); $this->check_current_mark(null); $this->check_step_count(2); $this->save_quba(); // Now submit all and finish. $this->finish(); $this->check_current_state(question_state::$needsgrading); $this->check_current_mark(null); $this->check_step_count(3); $this->save_quba(); // Now start a new attempt based on the old one. $this->load_quba(); $oldqa = $this->get_question_attempt(); $q = question_bank::load_question($question->id); $this->quba = question_engine::make_questions_usage_by_activity('unit_test', context_system::instance()); $this->quba->set_preferred_behaviour('deferredfeedback'); $this->slot = $this->quba->add_question($q, 1); $this->quba->start_question_based_on($this->slot, $oldqa); $this->check_current_state(question_state::$complete); $this->check_current_mark(null); $this->check_step_count(1); $this->save_quba(); // Check the display. $this->load_quba(); $this->render(); $this->assertRegExp('/I refuse to draw you a picture, so there!/', $this->currentoutput); }
/** * Whether it is possible for another question to depend on this one finishing. * Note that the answer is not exact, because of random questions, and sometimes * questions cannot be depended upon because of quiz options. * @param int $slotnumber the index of the slot in question. * @return bool can this question finish naturally during the attempt? */ public function can_finish_during_the_attempt($slotnumber) { if ($this->quizobj->get_navigation_method() == QUIZ_NAVMETHOD_SEQ) { return false; } if ($this->slotsinorder[$slotnumber]->section->shufflequestions) { return false; } if (in_array($this->get_question_type_for_slot($slotnumber), array('random', 'missingtype'))) { return \question_engine::can_questions_finish_during_the_attempt($this->quizobj->get_quiz()->preferredbehaviour); } if (isset($this->slotsinorder[$slotnumber]->canfinish)) { return $this->slotsinorder[$slotnumber]->canfinish; } try { $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $this->quizobj->get_context()); $tempslot = $quba->add_question(\question_bank::load_question($this->slotsinorder[$slotnumber]->questionid)); $quba->set_preferred_behaviour($this->quizobj->get_quiz()->preferredbehaviour); $quba->start_all_questions(); $this->slotsinorder[$slotnumber]->canfinish = $quba->can_question_finish_during_attempt($tempslot); return $this->slotsinorder[$slotnumber]->canfinish; } catch (\Exception $e) { // If the question fails to start, this should not block editing. return false; } }
/** * Test the various methods that load data for reporting. * * Since these methods need an expensive set-up, and then only do read-only * operations on the data, we use a single method to do the set-up, which * calls diffents methods to test each query. */ public function test_reporting_queries() { // We create two usages, each with two questions, a short-answer marked // out of 5, and and essay marked out of 10. // // In the first usage, the student answers the short-answer // question correctly, and enters something in the essay. // // In the second useage, the student answers the short-answer question // wrongly, and leaves the essay blank. $this->resetAfterTest(); $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); $this->sa = $generator->create_question('shortanswer', null, array('category' => $cat->id)); $this->essay = $generator->create_question('essay', null, array('category' => $cat->id)); $this->usageids = array(); // Create the first usage. $q = question_bank::load_question($this->sa->id); $this->start_attempt_at_question($q, 'interactive', 5); $this->allslots[] = $this->slot; $this->process_submission(array('answer' => 'cat')); $this->process_submission(array('answer' => 'frog', '-submit' => 1)); $q = question_bank::load_question($this->essay->id); $this->start_attempt_at_question($q, 'interactive', 10); $this->allslots[] = $this->slot; $this->process_submission(array('answer' => '<p>The cat sat on the mat.</p>', 'answerformat' => FORMAT_HTML)); $this->finish(); $this->save_quba(); $this->usageids[] = $this->quba->get_id(); // Create the second usage. $this->quba = question_engine::make_questions_usage_by_activity('unit_test', context_system::instance()); $q = question_bank::load_question($this->sa->id); $this->start_attempt_at_question($q, 'interactive', 5); $this->process_submission(array('answer' => 'fish')); $q = question_bank::load_question($this->essay->id); $this->start_attempt_at_question($q, 'interactive', 10); $this->finish(); $this->save_quba(); $this->usageids[] = $this->quba->get_id(); // Set up some things the tests will need. $this->dm = new question_engine_data_mapper(); $this->bothusages = new qubaid_list($this->usageids); // Now test the various queries. $this->dotest_load_questions_usages_latest_steps(); $this->dotest_load_questions_usages_question_state_summary(); $this->dotest_load_questions_usages_where_question_in_state(); $this->dotest_load_average_marks(); $this->dotest_sum_usage_marks_subquery(); $this->dotest_question_attempt_latest_state_view(); }
/** * Load the definition of another question picked randomly by this question. * @param object $questiondata the data defining a random question. * @param array $excludedquestions of question ids. We will no pick any question whose id is in this list. * @param bool $allowshuffle if false, then any shuffle option on the selected quetsion is disabled. * @param null|integer $forcequestionid if not null then force the picking of question with id $forcequestionid. * @throws coding_exception * @return question_definition|null the definition of the question that was * selected, or null if no suitable question could be found. */ public function choose_other_question($questiondata, $excludedquestions, $allowshuffle = true, $forcequestionid = null) { $available = $this->get_available_questions_from_category($questiondata->category, !empty($questiondata->questiontext)); shuffle($available); if ($forcequestionid !== null) { $forcedquestionkey = array_search($forcequestionid, $available); if ($forcedquestionkey !== false) { unset($available[$forcedquestionkey]); array_unshift($available, $forcequestionid); } else { throw new coding_exception('thisquestionidisnotavailable', $forcequestionid); } } foreach ($available as $questionid) { if (in_array($questionid, $excludedquestions)) { continue; } $question = question_bank::load_question($questionid, $allowshuffle); $this->set_selected_question_name($question, $questiondata->name); return $question; } return null; }
/** * Create a question_attempt_step from records loaded from the database. * * For internal use only. * * @param Iterator $records Raw records loaded from the database. * @param int $questionattemptid The id of the question_attempt to extract. * @return question_attempt The newly constructed question_attempt. */ public static function load_from_records($records, $questionattemptid, question_usage_observer $observer, $preferredbehaviour) { $record = $records->current(); while ($record->questionattemptid != $questionattemptid) { $record = $records->next(); if (!$records->valid()) { throw new coding_exception("Question attempt {$questionattemptid} not found in the database."); } $record = $records->current(); } try { $question = question_bank::load_question($record->questionid); } catch (Exception $e) { // The question must have been deleted somehow. Create a missing // question to use in its place. $question = question_bank::get_qtype('missingtype')->make_deleted_instance($record->questionid, $record->maxmark + 0); } $qa = new question_attempt($question, $record->questionusageid, null, $record->maxmark + 0); $qa->set_database_id($record->questionattemptid); $qa->set_slot($record->slot); $qa->variant = $record->variant + 0; $qa->minfraction = $record->minfraction + 0; $qa->maxfraction = $record->maxfraction + 0; $qa->set_flagged($record->flagged); $qa->questionsummary = $record->questionsummary; $qa->rightanswer = $record->rightanswer; $qa->responsesummary = $record->responsesummary; $qa->timemodified = $record->timemodified; $qa->behaviour = question_engine::make_behaviour($record->behaviour, $qa, $preferredbehaviour); $qa->observer = $observer; // If attemptstepid is null (which should not happen, but has happened // due to corrupt data, see MDL-34251) then the current pointer in $records // will not be advanced in the while loop below, and we get stuck in an // infinite loop, since this method is supposed to always consume at // least one record. Therefore, in this case, advance the record here. if (is_null($record->attemptstepid)) { $records->next(); } $i = 0; $autosavedstep = null; $autosavedsequencenumber = null; while ($record && $record->questionattemptid == $questionattemptid && !is_null($record->attemptstepid)) { $sequencenumber = $record->sequencenumber; $nextstep = question_attempt_step::load_from_records($records, $record->attemptstepid, $qa->get_question()->get_type_name()); if ($sequencenumber < 0) { if (!$autosavedstep) { $autosavedstep = $nextstep; $autosavedsequencenumber = -$sequencenumber; } else { // Old redundant data. Mark it for deletion. $qa->observer->notify_step_deleted($nextstep, $qa); } } else { $qa->steps[$i] = $nextstep; if ($i == 0) { $question->apply_attempt_state($qa->steps[0]); } $i++; } if ($records->valid()) { $record = $records->current(); } else { $record = false; } } if ($autosavedstep) { if ($autosavedsequencenumber >= $i) { $qa->autosavedstep = $autosavedstep; $qa->steps[$i] = $qa->autosavedstep; } else { $qa->observer->notify_step_deleted($autosavedstep, $qa); } } return $qa; }
public function test_previously_used_question_not_returned_until_later() { $this->resetAfterTest(); $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); $question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); $question2 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); $quba = question_engine::make_questions_usage_by_activity('test', context_system::instance()); $quba->set_preferred_behaviour('deferredfeedback'); $question = question_bank::load_question($question2->id); $quba->add_question($question); $quba->add_question($question); $quba->start_all_questions(); question_engine::save_questions_usage_by_activity($quba); $loader = new \core_question\bank\random_question_loader(new qubaid_list(array($quba->get_id()))); $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 0)); $this->assertEquals($question2->id, $loader->get_next_question_id($cat->id, 0)); $this->assertNull($loader->get_next_question_id($cat->id, 0)); }
/** * Replace a question in an attempt with a new attempt at the same qestion. * @param int $slot the questoin to restart. * @param int $timestamp the timestamp to record for this action. */ public function process_redo_question($slot, $timestamp) { global $DB; if (!$this->can_question_be_redone_now($slot)) { throw new coding_exception('Attempt to restart the question in slot ' . $slot . ' when it is not in a state to be restarted.'); } $qubaids = new \mod_quiz\question\qubaids_for_users_attempts($this->get_quizid(), $this->get_userid()); $transaction = $DB->start_delegated_transaction(); $questiondata = $DB->get_record('question', array('id' => $this->slots[$slot]->questionid)); if ($questiondata->qtype != 'random') { $newqusetionid = $questiondata->id; } else { $randomloader = new \core_question\bank\random_question_loader($qubaids, array()); $newqusetionid = $randomloader->get_next_question_id($questiondata->category, (bool) $questiondata->questiontext); if ($newqusetionid === null) { throw new moodle_exception('notenoughrandomquestions', 'quiz', $quizobj->view_url(), $questiondata); } } $newquestion = question_bank::load_question($newqusetionid); if ($newquestion->get_num_variants() == 1) { $variant = 1; } else { $variantstrategy = new core_question\engine\variants\least_used_strategy($this->quba, $qubaids); $variant = $variantstrategy->choose_variant($newquestion->get_num_variants(), $newquestion->get_variants_selection_seed()); } $newslot = $this->quba->add_question_in_place_of_other($slot, $newquestion); $this->quba->start_question($slot); $this->quba->set_max_mark($newslot, 0); $this->quba->set_question_attempt_metadata($newslot, 'originalslot', $slot); question_engine::save_questions_usage_by_activity($this->quba); $transaction->allow_commit(); }
/** * Create a question_attempt_step from records loaded from the database. * * For internal use only. * * @param Iterator $records Raw records loaded from the database. * @param int $questionattemptid The id of the question_attempt to extract. * @return question_attempt The newly constructed question_attempt. */ public static function load_from_records($records, $questionattemptid, question_usage_observer $observer, $preferredbehaviour) { $record = $records->current(); while ($record->questionattemptid != $questionattemptid) { $record = $records->next(); if (!$records->valid()) { throw new coding_exception("Question attempt $questionattemptid not found in the database."); } $record = $records->current(); } try { $question = question_bank::load_question($record->questionid); } catch (Exception $e) { // The question must have been deleted somehow. Create a missing // question to use in its place. $question = question_bank::get_qtype('missingtype')->make_deleted_instance( $record->questionid, $record->maxmark + 0); } $qa = new question_attempt($question, $record->questionusageid, null, $record->maxmark + 0); $qa->set_database_id($record->questionattemptid); $qa->set_slot($record->slot); $qa->variant = $record->variant + 0; $qa->minfraction = $record->minfraction + 0; $qa->set_flagged($record->flagged); $qa->questionsummary = $record->questionsummary; $qa->rightanswer = $record->rightanswer; $qa->responsesummary = $record->responsesummary; $qa->timemodified = $record->timemodified; $qa->behaviour = question_engine::make_behaviour( $record->behaviour, $qa, $preferredbehaviour); $i = 0; while ($record && $record->questionattemptid == $questionattemptid && !is_null($record->attemptstepid)) { $qa->steps[$i] = question_attempt_step::load_from_records($records, $record->attemptstepid); if ($i == 0) { $question->apply_attempt_state($qa->steps[0]); } $i++; if ($records->valid()) { $record = $records->current(); } else { $record = false; } } $qa->observer = $observer; return $qa; }