/** * 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); } }
protected function prepare_quiz_data() { $this->resetAfterTest(true); // Create a course $course = $this->getDataGenerator()->create_course(); // Make a quiz. $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); $quiz = $quizgenerator->create_instance(array('course' => $course->id, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2)); $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id); // Create a couple of questions. $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $questiongenerator->create_question_category(); $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); // Add them to the quiz. quiz_add_quiz_question($saq->id, $quiz); quiz_add_quiz_question($numq->id, $quiz); // Make a user to do the quiz. $user1 = $this->getDataGenerator()->create_user(); $this->setUser($user1); $quizobj = quiz::create($quiz->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); quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); quiz_attempt_save_started($quizobj, $quba, $attempt); return array($quizobj, $quba, $attempt); }
public function definition() { $mform = $this->_form; $hiddenofvisible = array(question_display_options::HIDDEN => get_string('notshown', 'question'), question_display_options::VISIBLE => get_string('shown', 'question')); $mform->addElement('header', 'optionsheader', get_string('changeoptions', 'question')); $behaviours = question_engine::get_behaviour_options($this->_customdata['quba']->get_preferred_behaviour()); $mform->addElement('select', 'behaviour', get_string('howquestionsbehave', 'question'), $behaviours); $mform->addHelpButton('behaviour', 'howquestionsbehave', 'question'); $mform->addElement('text', 'maxmark', get_string('markedoutof', 'question'), array('size' => '5')); $mform->setType('maxmark', PARAM_NUMBER); if ($this->_customdata['maxvariant'] > 1) { $variants = range(1, $this->_customdata['maxvariant']); $mform->addElement('select', 'variant', get_string('questionvariant', 'question'), array_combine($variants, $variants)); } $mform->setType('maxmark', PARAM_INT); $mform->addElement('select', 'correctness', get_string('whethercorrect', 'question'), $hiddenofvisible); $marksoptions = array(question_display_options::HIDDEN => get_string('notshown', 'question'), question_display_options::MAX_ONLY => get_string('showmaxmarkonly', 'question'), question_display_options::MARK_AND_MAX => get_string('showmarkandmax', 'question')); $mform->addElement('select', 'marks', get_string('marks', 'question'), $marksoptions); $mform->addElement('select', 'markdp', get_string('decimalplacesingrades', 'question'), question_engine::get_dp_options()); $mform->addElement('select', 'feedback', get_string('specificfeedback', 'question'), $hiddenofvisible); $mform->addElement('select', 'generalfeedback', get_string('generalfeedback', 'question'), $hiddenofvisible); $mform->addElement('select', 'rightanswer', get_string('rightanswer', 'question'), $hiddenofvisible); $mform->addElement('select', 'history', get_string('responsehistory', 'question'), $hiddenofvisible); $mform->addElement('submit', 'submit', get_string('restartwiththeseoptions', 'question'), $hiddenofvisible); }
/** * Given the quiz "How questions behave" setting, can the fault-tolerant mode work * with that behaviour? * @param string $behaviour the internal name (e.g. 'interactive') of an archetypal behaviour. * @return boolean whether fault-tolerant mode can be used. */ public static function is_compatible_behaviour($behaviour) { $unusedoptions = question_engine::get_behaviour_unused_display_options($behaviour); // Sorry, double negative here. The heuristic is that: // The behaviour is compatible if we don't need to show specific feedback during the attempt. return in_array('specificfeedback', $unusedoptions); }
/** * Manual graded question behaviour upgrade code. */ function xmldb_qbehaviour_manualgraded_upgrade($oldversion) { global $CFG, $DB; $dbman = $DB->get_manager(); // Moodle v2.4.0 release upgrade line // Put any upgrade step following this if ($oldversion < 2013050200) { // Hide the manualgraded behaviour from the list of behaviours that users // can select in the user-interface. If a user accidentally chooses manual // graded behaviour for a quiz, there is no way to get the questions automatically // graded after the student has answered them. If teachers really want to do // this they can ask their admin to enable it on the manage behaviours // screen in the UI. $disabledbehaviours = get_config('question', 'disabledbehaviours'); if (!empty($disabledbehaviours)) { $disabledbehaviours = explode(',', $disabledbehaviours); } else { $disabledbehaviours = array(); } if (array_search('manualgraded', $disabledbehaviours) === false) { $disabledbehaviours[] = 'manualgraded'; set_config('disabledbehaviours', implode(',', $disabledbehaviours), 'question'); } // Manual graded question behaviour savepoint reached. upgrade_plugin_savepoint(true, 2013050200, 'qbehaviour', 'manualgraded'); } if ($oldversion < 2013050800) { // Also, fix any other admin settings that currently select manualgraded behaviour. // Work out a sensible default alternative to manualgraded. require_once $CFG->libdir . '/questionlib.php'; $behaviours = question_engine::get_behaviour_options(''); if (array_key_exists('deferredfeedback', $behaviours)) { $defaultbehaviour = 'deferredfeedback'; } else { reset($behaviours); $defaultbehaviour = key($behaviours); } // Fix the question preview default. if (get_config('question_preview', 'behaviour') == 'manualgraded') { set_config('behaviour', $defaultbehaviour, 'question_preview'); } // Fix the quiz settings default. if (get_config('quiz', 'preferredbehaviour') == 'manualgraded') { set_config('preferredbehaviour', $defaultbehaviour, 'quiz'); } // Manual graded question behaviour savepoint reached. upgrade_plugin_savepoint(true, 2013050800, 'qbehaviour', 'manualgraded'); } // Moodle v2.5.0 release upgrade line. // Put any upgrade step following this. // Moodle v2.6.0 release upgrade line. // Put any upgrade step following this. // Moodle v2.7.0 release upgrade line. // Put any upgrade step following this. // Moodle v2.8.0 release upgrade line. // Put any upgrade step following this. // Moodle v2.9.0 release upgrade line. // Put any upgrade step following this. return true; }
/** * Constructor. * @param question_usage_by_activity $quba the question usage we will be picking variants for. * @param qubaid_condition $qubaids ids of the usages to consider when counting previous uses of each variant. */ public function __construct(\question_usage_by_activity $quba, \qubaid_condition $qubaids) { $questionidtoseed = array(); foreach ($quba->get_attempt_iterator() as $qa) { $question = $qa->get_question(); if ($question->get_num_variants() > 1) { $questionidtoseed[$question->id] = $question->get_variants_selection_seed(); } } if (empty($questionidtoseed)) { return; } $this->variantsusecounts = array_fill_keys($questionidtoseed, array()); $variantsused = \question_engine::load_used_variants(array_keys($questionidtoseed), $qubaids); foreach ($variantsused as $questionid => $usagecounts) { $seed = $questionidtoseed[$questionid]; foreach ($usagecounts as $variant => $count) { if (isset($this->variantsusecounts[$seed][$variant])) { $this->variantsusecounts[$seed][$variant] += $count; } else { $this->variantsusecounts[$seed][$variant] = $count; } } } }
public function setUp() { $this->quba = question_engine::make_questions_usage_by_activity('unit_test', get_context_instance(CONTEXT_SYSTEM)); $this->quba->set_preferred_behaviour('deferredfeedback'); $slot = $this->quba->add_question(test_question_maker::make_a_description_question()); $this->qas[$slot] = $this->quba->get_question_attempt($slot); $slot = $this->quba->add_question(test_question_maker::make_a_description_question()); $this->qas[$slot] = $this->quba->get_question_attempt($slot); $this->iterator = $this->quba->get_attempt_iterator(); }
protected function setUp() { $this->quba = question_engine::make_questions_usage_by_activity('unit_test', context_system::instance()); $this->quba->set_preferred_behaviour('deferredfeedback'); $slot = $this->quba->add_question(test_question_maker::make_question('description')); $this->qas[$slot] = $this->quba->get_question_attempt($slot); $slot = $this->quba->add_question(test_question_maker::make_question('description')); $this->qas[$slot] = $this->quba->get_question_attempt($slot); $this->iterator = $this->quba->get_attempt_iterator(); }
/** * 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(); }
public function test_sort_behaviours() { $in = array('b1' => 'Behave 1', 'b2' => 'Behave 2', 'b3' => 'Behave 3', 'b4' => 'Behave 4', 'b5' => 'Behave 5', 'b6' => 'Behave 6'); $out = array('b1' => 'Behave 1', 'b2' => 'Behave 2', 'b3' => 'Behave 3', 'b4' => 'Behave 4', 'b5' => 'Behave 5', 'b6' => 'Behave 6'); $this->assertIdentical($out, question_engine::sort_behaviours($in, '', '', '')); $this->assertIdentical($out, question_engine::sort_behaviours($in, '', 'b4', 'b4')); $out = array('b4' => 'Behave 4', 'b5' => 'Behave 5', 'b6' => 'Behave 6'); $this->assertIdentical($out, question_engine::sort_behaviours($in, '', 'b1,b2,b3,b4', 'b4')); $out = array('b6' => 'Behave 6', 'b1' => 'Behave 1', 'b4' => 'Behave 4'); $this->assertIdentical($out, question_engine::sort_behaviours($in, 'b6,b1,b4', 'b2,b3,b4,b5', 'b4')); $out = array('b6' => 'Behave 6', 'b5' => 'Behave 5', 'b4' => 'Behave 4'); $this->assertIdentical($out, question_engine::sort_behaviours($in, 'b6,b5,b4', 'b1,b2,b3', 'b4')); $out = array('b6' => 'Behave 6', 'b5' => 'Behave 5', 'b4' => 'Behave 4'); $this->assertIdentical($out, question_engine::sort_behaviours($in, 'b1,b6,b5', 'b1,b2,b3,b4', 'b4')); $out = array('b2' => 'Behave 2', 'b4' => 'Behave 4', 'b6' => 'Behave 6'); $this->assertIdentical($out, question_engine::sort_behaviours($in, 'b2,b4,b6', 'b1,b3,b5', 'b2')); // Ignore unknown input in the order argument. $this->assertIdentical($in, question_engine::sort_behaviours($in, 'unknown', '', '')); // Ignore unknown input in the disabled argument. $this->assertIdentical($in, question_engine::sort_behaviours($in, '', 'unknown', '')); }
$archetypal[$behaviour] = question_engine::is_behaviour_archetypal($behaviour); } foreach ($counts as $behaviour => $count) { if (!array_key_exists($behaviour, $behaviours)) { $counts['missing'] += $count; } } $needed['missing'] = true; // Work of the correct sort order. $config = get_config('question'); $sortedbehaviours = array(); foreach ($behaviours as $behaviour => $notused) { $sortedbehaviours[$behaviour] = question_engine::get_behaviour_name($behaviour); } if (!empty($config->behavioursortorder)) { $sortedbehaviours = question_engine::sort_behaviours($sortedbehaviours, $config->behavioursortorder, ''); } if (!empty($config->disabledbehaviours)) { $disabledbehaviours = explode(',', $config->disabledbehaviours); } else { $disabledbehaviours = array(); } // Process actions ============================================================ // Disable. if (($disable = optional_param('disable', '', PARAM_PLUGIN)) && confirm_sesskey()) { if (!isset($behaviours[$disable])) { print_error('unknownbehaviour', 'question', $thispageurl, $disable); } if (array_search($disable, $disabledbehaviours) === false) { $disabledbehaviours[] = $disable; set_config('disabledbehaviours', implode(',', $disabledbehaviours), 'question');
protected function after_execute() { parent::after_execute(); // Restore any files belonging to responses. foreach (question_engine::get_all_response_file_areas() as $filearea) { $this->add_related_files('question', $filearea, 'question_attempt_step'); } }
/** * Search question behaviours for the specified string * * @param string $query The string to search for in question behaviours * @return array */ public function search($query) { global $CFG; if ($result = parent::search($query)) { return $result; } $found = false; $textlib = textlib_get_instance(); require_once $CFG->dirroot . '/question/engine/lib.php'; foreach (get_plugin_list('qbehaviour') as $behaviour => $notused) { if (strpos($textlib->strtolower(question_engine::get_behaviour_name($behaviour)), $query) !== false) { $found = true; break; } } if ($found) { $result = new stdClass(); $result->page = $this; $result->settings = array(); return array($this->name => $result); } else { return array(); } }
/** * Attach to $element (usually attempts) the needed backup structures * for question_usages and all the associated data. */ protected function add_question_usages($element, $usageidname) { global $CFG; require_once $CFG->dirroot . '/question/engine/lib.php'; // Check $element is one nested_backup_element if (!$element instanceof backup_nested_element) { throw new backup_step_exception('question_states_bad_parent_element', $element); } if (!$element->get_final_element($usageidname)) { throw new backup_step_exception('question_states_bad_question_attempt_element', $usageidname); } $quba = new backup_nested_element('question_usage', array('id'), array('component', 'preferredbehaviour')); $qas = new backup_nested_element('question_attempts'); $qa = new backup_nested_element('question_attempt', array('id'), array('slot', 'behaviour', 'questionid', 'maxmark', 'minfraction', 'flagged', 'questionsummary', 'rightanswer', 'responsesummary', 'timemodified')); $steps = new backup_nested_element('steps'); $step = new backup_nested_element('step', array('id'), array('sequencenumber', 'state', 'fraction', 'timecreated', 'userid')); $response = new backup_nested_element('response'); $variable = new backup_nested_element('variable', null, array('name', 'value')); // Build the tree $element->add_child($quba); $quba->add_child($qas); $qas->add_child($qa); $qa->add_child($steps); $steps->add_child($step); $step->add_child($response); $response->add_child($variable); // Set the sources $quba->set_source_table('question_usages', array('id' => '../' . $usageidname)); $qa->set_source_sql(' SELECT * FROM {question_attempts} WHERE questionusageid = :questionusageid ORDER BY slot', array('questionusageid' => backup::VAR_PARENTID)); $step->set_source_sql(' SELECT * FROM {question_attempt_steps} WHERE questionattemptid = :questionattemptid ORDER BY sequencenumber', array('questionattemptid' => backup::VAR_PARENTID)); $variable->set_source_table('question_attempt_step_data', array('attemptstepid' => backup::VAR_PARENTID)); // Annotate ids $qa->annotate_ids('question', 'questionid'); $step->annotate_ids('user', 'userid'); // Annotate files $fileareas = question_engine::get_all_response_file_areas(); foreach ($fileareas as $filearea) { $step->annotate_files('question', $filearea, 'id'); } }
public function test_access_out_of_sequence_throws_exception() { // Start a deferred feedback attempt with CBM and add the question to it. $tf = test_question_maker::make_question('truefalse', 'true'); $quba = question_engine::make_questions_usage_by_activity('unit_test', context_system::instance()); $quba->set_preferred_behaviour('deferredcbm'); $slot = $quba->add_question($tf); $quba->start_all_questions(); // Prepare data to be submitted $prefix = $quba->get_field_prefix($slot); $answername = $prefix . 'answer'; $certaintyname = $prefix . '-certainty'; $postdata = array( $answername => 1, $certaintyname => 3, $prefix . ':sequencecheck' => 1, 'irrelevant' => 'should be ignored', ); // Exercise SUT - no exception yet. $quba->process_all_actions($slot, $postdata); $postdata = array( $answername => 1, $certaintyname => 3, $prefix . ':sequencecheck' => 3, 'irrelevant' => 'should be ignored', ); // Exercise SUT - now it should fail. $this->setExpectedException('question_out_of_sequence_exception'); $quba->process_all_actions($slot, $postdata); }
/** * Change the max mark for a slot. * * Saves changes to the question grades in the quiz_slots table and any * corresponding question_attempts. * It does not update 'sumgrades' in the quiz table. * * @param \stdClass $slot row from the quiz_slots table. * @param float $maxmark the new maxmark. * @return bool true if the new grade is different from the old one. */ public function update_slot_maxmark($slot, $maxmark) { global $DB; if (abs($maxmark - $slot->maxmark) < 1.0E-7) { // Grade has not changed. Nothing to do. return false; } $trans = $DB->start_delegated_transaction(); $slot->maxmark = $maxmark; $DB->update_record('quiz_slots', $slot); \question_engine::set_max_mark_in_attempts(new \qubaids_for_quiz($slot->quizid), $slot->slot, $maxmark); $trans->allow_commit(); return true; }
$oldnumberstonew[$oldslot] = $newslot; } // Update attempt layout. $newlayout = array(); foreach (explode(',', $lastattempt->layout) as $oldslot) { if ($oldslot != 0) { $newlayout[] = $oldnumberstonew[$oldslot]; } else { $newlayout[] = 0; } } $attempt->layout = implode(',', $newlayout); } // Save the attempt in the database. $transaction = $DB->start_delegated_transaction(); question_engine::save_questions_usage_by_activity($quba); $attempt->uniqueid = $quba->get_id(); $attempt->id = $DB->insert_record('quiz_attempts', $attempt); // Log the new attempt. if ($attempt->preview) { add_to_log($course->id, 'quiz', 'preview', 'view.php?id=' . $quizobj->get_cmid(), $quizobj->get_quizid(), $quizobj->get_cmid()); } else { add_to_log($course->id, 'quiz', 'attempt', 'review.php?attempt=' . $attempt->id, $quizobj->get_quizid(), $quizobj->get_cmid()); } // Trigger event. $eventdata = new stdClass(); $eventdata->component = 'mod_quiz'; $eventdata->attemptid = $attempt->id; $eventdata->timestart = $attempt->timestart; $eventdata->timestamp = $attempt->timestart; $eventdata->userid = $attempt->userid;
public function test_load_used_variants() { $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($questiondata1->id => array(1 => 2), $questiondata2->id => array(), $questiondata3->id => array(1 => 1)), question_engine::load_used_variants(array($questiondata1->id, $questiondata2->id, $questiondata3->id), new qubaid_list(array($quba->get_id())))); }
public function process_finish($timestamp, $processsubmitted) { global $DB; $transaction = $DB->start_delegated_transaction(); if ($processsubmitted) { $this->quba->process_all_actions($timestamp); } $this->quba->finish_all_questions($timestamp); question_engine::save_questions_usage_by_activity($this->quba); $this->attempt->timemodified = $timestamp; $this->attempt->timefinish = $timestamp; $this->attempt->sumgrades = $this->quba->get_total_mark(); $this->attempt->state = self::FINISHED; $this->attempt->timecheckstate = null; $DB->update_record('quiz_attempts', $this->attempt); if (!$this->is_preview()) { quiz_save_best_grade($this->get_quiz(), $this->attempt->userid); // Trigger event. $this->fire_state_transition_event('\\mod_quiz\\event\\attempt_submitted', $timestamp); // Tell any access rules that care that the attempt is over. $this->get_access_manager($timestamp)->current_attempt_finished(); } $transaction->allow_commit(); }
/** * (non-PHPdoc) * * @see offlinequiz_default_report::display() */ public function display($offlinequiz, $cm, $course) { global $CFG, $OUTPUT, $SESSION, $DB; // Define some strings. $strtimeformat = get_string('strftimedatetime'); $letterstr = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'; offlinequiz_load_useridentification(); $offlinequizconfig = get_config('offlinequiz'); // Deal with actions. $action = optional_param('action', '', PARAM_ACTION); // Only print headers if not asked to download data or delete data. if (!($download = optional_param('download', null, PARAM_TEXT)) && !$action == 'delete') { $this->print_header_and_tabs($cm, $course, $offlinequiz, 'overview'); echo $OUTPUT->box_start('linkbox'); echo $OUTPUT->heading(format_string($offlinequiz->name)); echo $OUTPUT->heading(get_string('results', 'offlinequiz')); require_once $CFG->libdir . '/grouplib.php'; echo groups_print_activity_menu($cm, $CFG->wwwroot . '/mod/offlinequiz/report.php?id=' . $cm->id . '&mode=overview', true); echo $OUTPUT->box_end(); echo '<br/>'; } $context = context_module::instance($cm->id); $systemcontext = context_system::instance(); // Set table options. $noresults = optional_param('noresults', 0, PARAM_INT); $pagesize = optional_param('pagesize', 10, PARAM_INT); $groupid = optional_param('group', 0, PARAM_INT); if ($pagesize < 1) { $pagesize = 10; } $answerletters = 'abcdefghijklmnopqrstuvwxyz'; if ($action == 'delete' && confirm_sesskey()) { $selectedresultids = array(); $params = (array) data_submitted(); foreach ($params as $key => $value) { if (preg_match('!^s([0-9]+)$!', $key, $matches)) { $selectedresultids[] = $matches[1]; } } if ($selectedresultids) { foreach ($selectedresultids as $resultid) { if ($resultid && ($todelete = $DB->get_record('offlinequiz_results', array('id' => $resultid)))) { offlinequiz_delete_result($resultid, $context); // Log this event. $params = array('objectid' => $resultid, 'relateduserid' => $todelete->userid, 'context' => context_module::instance($cm->id), 'other' => array('mode' => 'overview')); $event = \mod_offlinequiz\event\attempt_deleted::create($params); $event->trigger(); // Change the status of all related pages with error 'resultexists' to 'suspended'. $user = $DB->get_record('user', array('id' => $todelete->userid)); $group = $DB->get_record('offlinequiz_groups', array('id' => $todelete->offlinegroupid)); $sql = "SELECT id\n FROM {offlinequiz_scanned_pages}\n WHERE offlinequizid = :offlinequizid\n AND userkey = :userkey\n AND groupnumber = :groupnumber\n AND status = 'error'\n AND (error = 'resultexists' OR error = 'differentresultexists')"; $params = array('offlinequizid' => $offlinequiz->id, 'userkey' => $user->{$offlinequizconfig->ID_field}, 'groupnumber' => $group->number); $otherpages = $DB->get_records_sql($sql, $params); foreach ($otherpages as $page) { $DB->set_field('offlinequiz_scanned_pages', 'status', 'suspended', array('id' => $page->id)); $DB->set_field('offlinequiz_scanned_pages', 'error', '', array('id' => $page->id)); } } } offlinequiz_grade_item_update($offlinequiz, 'reset'); offlinequiz_update_grades($offlinequiz); redirect(new moodle_url('/mod/offlinequiz/report.php', array('mode' => 'overview', 'id' => $cm->id, 'noresults' => $noresults, 'pagesize' => $pagesize))); } } // Now check if asked download of data. if ($download) { $filename = clean_filename("{$course->shortname} " . format_string($offlinequiz->name, true)); $sort = ''; } // Fetch the group data. $groups = $DB->get_records('offlinequiz_groups', array('offlinequizid' => $offlinequiz->id), 'number', '*', 0, $offlinequiz->numgroups); // Define table columns. $tablecolumns = array('checkbox', 'picture', 'fullname', $offlinequizconfig->ID_field, 'timestart', 'offlinegroupid', 'sumgrades'); $tableheaders = array('<input type="checkbox" name="toggle" onClick="if (this.checked) {select_all_in(\'DIV\',null,\'tablecontainer\');} else {deselect_all_in(\'DIV\',null,\'tablecontainer\');}"/>', '', get_string('fullname'), get_string($offlinequizconfig->ID_field), get_string('importedon', 'offlinequiz'), get_string('group'), get_string('grade', 'offlinequiz')); $checked = array(); // Get participants list. $withparticipants = false; if ($lists = $DB->get_records('offlinequiz_p_lists', array('offlinequizid' => $offlinequiz->id))) { $withparticipants = true; $tablecolumns[] = 'checked'; $tableheaders[] = get_string('present', 'offlinequiz'); foreach ($lists as $list) { $participants = $DB->get_records('offlinequiz_participants', array('listid' => $list->id)); foreach ($participants as $participant) { $checked[$participant->userid] = $participant->checked; } } } if (!$download) { // Set up the table. $params = array('offlinequiz' => $offlinequiz, 'noresults' => $noresults, 'pagesize' => $pagesize); $table = new offlinequiz_results_table('mod-offlinequiz-report-overview-report', $params); $table->define_columns($tablecolumns); $table->define_headers($tableheaders); $table->define_baseurl($CFG->wwwroot . '/mod/offlinequiz/report.php?mode=overview&id=' . $cm->id . '&noresults=' . $noresults . '&pagesize=' . $pagesize); $table->sortable(true); $table->no_sorting('checkbox'); if ($withparticipants) { $table->no_sorting('checked'); } $table->column_suppress('picture'); $table->column_suppress('fullname'); $table->column_class('picture', 'picture'); $table->column_class($offlinequizconfig->ID_field, 'userkey'); $table->column_class('timestart', 'timestart'); $table->column_class('offlinegroupid', 'offlinegroupid'); $table->column_class('sumgrades', 'sumgrades'); $table->set_attribute('cellpadding', '2'); $table->set_attribute('id', 'attempts'); $table->set_attribute('class', 'generaltable generalbox'); // Start working -- this is necessary as soon as the niceties are over. $table->setup(); } else { if ($download == 'ODS') { require_once "{$CFG->libdir}/odslib.class.php"; $filename .= ".ods"; // Creating a workbook. $workbook = new MoodleODSWorkbook("-"); // Sending HTTP headers. $workbook->send($filename); // Creating the first worksheet. $sheettitle = get_string('reportoverview', 'offlinequiz'); $myxls = $workbook->add_worksheet($sheettitle); // Format types. $format = $workbook->add_format(); $format->set_bold(0); $formatbc = $workbook->add_format(); $formatbc->set_bold(1); $formatbc->set_align('center'); $formatb = $workbook->add_format(); $formatb->set_bold(1); $formaty = $workbook->add_format(); $formaty->set_bg_color('yellow'); $formatc = $workbook->add_format(); $formatc->set_align('center'); $formatr = $workbook->add_format(); $formatr->set_bold(1); $formatr->set_color('red'); $formatr->set_align('center'); $formatg = $workbook->add_format(); $formatg->set_bold(1); $formatg->set_color('green'); $formatg->set_align('center'); // Here starts workshhet headers. $headers = array(get_string($offlinequizconfig->ID_field), get_string('firstname'), get_string('lastname'), get_string('importedon', 'offlinequiz'), get_string('group'), get_string('grade', 'offlinequiz')); if (!empty($withparticipants)) { $headers[] = get_string('present', 'offlinequiz'); } $colnum = 0; foreach ($headers as $item) { $myxls->write(0, $colnum, $item, $formatbc); $colnum++; } $rownum = 1; } else { if ($download == 'Excel') { require_once "{$CFG->libdir}/excellib.class.php"; $filename .= ".xls"; // Creating a workbook. $workbook = new MoodleExcelWorkbook("-"); // Sending HTTP headers. $workbook->send($filename); // Creating the first worksheet. $sheettitle = get_string('results', 'offlinequiz'); $myxls = $workbook->add_worksheet($sheettitle); // Format types. $format = $workbook->add_format(); $format->set_bold(0); $formatbc = $workbook->add_format(); $formatbc->set_bold(1); $formatbc->set_align('center'); $formatb = $workbook->add_format(); $formatb->set_bold(1); $formaty = $workbook->add_format(); $formaty->set_bg_color('yellow'); $formatc = $workbook->add_format(); $formatc->set_align('center'); $formatr = $workbook->add_format(); $formatr->set_bold(1); $formatr->set_color('red'); $formatr->set_align('center'); $formatg = $workbook->add_format(); $formatg->set_bold(1); $formatg->set_color('green'); $formatg->set_align('center'); // Here starts worksheet headers. $headers = array(get_string($offlinequizconfig->ID_field), get_string('firstname'), get_string('lastname'), get_string('importedon', 'offlinequiz'), get_string('group'), get_string('grade', 'offlinequiz')); if (!empty($withparticipants)) { $headers[] = get_string('present', 'offlinequiz'); } $colnum = 0; foreach ($headers as $item) { $myxls->write(0, $colnum, $item, $formatbc); $colnum++; } $rownum = 1; } else { if ($download == 'CSV') { $filename .= ".csv"; header("Content-Encoding: UTF-8"); header("Content-Type: text/csv; charset=utf-8"); header("Content-Disposition: attachment; filename=\"{$filename}\""); header("Expires: 0"); header("Cache-Control: must-revalidate,post-check=0,pre-check=0"); header("Pragma: public"); echo ""; // UTF-8 BOM. $headers = get_string($offlinequizconfig->ID_field) . ", " . get_string('fullname') . ", " . get_string('importedon', 'offlinequiz') . ", " . get_string('group') . ", " . get_string('grade', 'offlinequiz'); if (!empty($withparticipants)) { $headers .= ", " . get_string('present', 'offlinequiz'); } echo $headers . " \n"; } else { if ($download == 'CSVplus1' || $download == 'CSVpluspoints') { $filename .= ".csv"; header("Content-Encoding: UTF-8"); header("Content-Type: text/csv; charset=utf-8"); header("Content-Disposition: attachment; filename=\"{$filename}\""); header("Expires: 0"); header("Cache-Control: must-revalidate,post-check=0,pre-check=0"); header("Pragma: public"); echo ""; // UTF-8 BOM. // Print the table headers. echo get_string('firstname') . ',' . get_string('lastname') . ',' . get_string($offlinequizconfig->ID_field) . ',' . get_string('group'); $maxquestions = offlinequiz_get_maxquestions($offlinequiz, $groups); for ($i = 0; $i < $maxquestions; $i++) { echo ', ' . get_string('question') . ' ' . ($i + 1); } echo "\n"; // Print the correct answer bit-strings. foreach ($groups as $group) { if ($group->templateusageid) { $quba = question_engine::load_questions_usage_by_activity($group->templateusageid); $slots = $quba->get_slots(); echo ', ,' . get_string('correct', 'offlinequiz'); echo ',' . $group->number; foreach ($slots as $slot) { $slotquestion = $quba->get_question($slot); $qtype = $slotquestion->get_type_name(); if ($qtype == 'multichoice' || $qtype == 'multichoiceset') { $attempt = $quba->get_question_attempt($slot); $order = $slotquestion->get_order($attempt); // Order of the answers. $tempstr = ","; $letters = array(); $counter = 0; foreach ($order as $key => $answerid) { $fraction = $DB->get_field('question_answers', 'fraction', array('id' => $answerid)); if ($fraction > 0) { $letters[] = $answerletters[$counter]; } $counter++; } if (empty($letters)) { $tempstr .= '99'; } else { $tempstr .= implode('/', $letters); } echo $tempstr; } } echo "\n"; } } } } } } } $coursecontext = context_course::instance($course->id); $contextids = $coursecontext->get_parent_context_ids(true); // Construct the SQL // First get roleids for students from leagcy. if (!($roles = get_roles_with_capability('mod/offlinequiz:attempt', CAP_ALLOW, $systemcontext))) { error("No roles with capability 'moodle/offlinequiz:attempt' defined in system context"); } $roleids = array(); foreach ($roles as $role) { $roleids[] = $role->id; } $rolelist = implode(',', $roleids); $select = "SELECT " . $DB->sql_concat('u.id', "'#'", "COALESCE(qa.usageid, 0)") . " AS uniqueid,\n qa.id AS resultid, u.id, qa.usageid, qa.offlinegroupid, qa.status,\n u.id AS userid, u.firstname, u.lastname,\n u.alternatename, u.middlename, u.firstnamephonetic, u.lastnamephonetic,\n u.picture, u." . $offlinequizconfig->ID_field . ",\n qa.sumgrades, qa.timefinish, qa.timestart, qa.timefinish - qa.timestart AS duration "; $result = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'ctx'); list($contexttest, $cparams) = $result; list($roletest, $rparams) = $DB->get_in_or_equal($roleids, SQL_PARAMS_NAMED, 'role'); $from = "FROM {user} u\n JOIN {role_assignments} ra ON ra.userid = u.id\n LEFT JOIN {offlinequiz_results} qa ON u.id = qa.userid AND qa.offlinequizid = :offlinequizid\n "; $where = " WHERE ra.contextid {$contexttest} AND ra.roleid {$roletest} "; $params = array('offlinequizid' => $offlinequiz->id); $params = array_merge($params, $cparams, $rparams); if ($groupid) { $from .= " JOIN {groups_members} gm ON gm.userid = u.id "; $where .= " AND gm.groupid = :groupid "; $params['groupid'] = $groupid; } if (empty($noresults)) { $where = $where . " AND qa.userid IS NOT NULL\n AND qa.status = 'complete'"; // Show ONLY students with results. } else { if ($noresults == 1) { // The value noresults = 1 means only no results, so make the left join ask for only records // where the right is null (no results). $where .= ' AND qa.userid IS NULL'; // Show ONLY students without results. } else { if ($noresults == 3) { // We want all results, also the partial ones. $from = "FROM {user} u\n JOIN {offlinequiz_results} qa ON u.id = qa.userid "; $where = " WHERE qa.offlinequizid = :offlinequizid"; } } } // The value noresults = 2 means we want all students, with or without results. $countsql = 'SELECT COUNT(DISTINCT(u.id)) ' . $from . $where; if (!$download) { // Count the records NOW, before funky question grade sorting messes up $from. $totalinitials = $DB->count_records_sql($countsql, $params); // Add extra limits due to initials bar. list($ttest, $tparams) = $table->get_sql_where(); if (!empty($ttest)) { $where .= ' AND ' . $ttest; $countsql .= ' AND ' . $ttest; $params = array_merge($params, $tparams); } $total = $DB->count_records_sql($countsql, $params); // Add extra limits due to sorting by question grade. $sort = $table->get_sql_sort(); // Fix some wired sorting. if (empty($sort)) { $sort = ' ORDER BY u.lastname'; } else { $sort = ' ORDER BY ' . $sort; } $table->pagesize($pagesize, $total); } // Fetch the results. if (!$download) { $results = $DB->get_records_sql($select . $from . $where . $sort, $params, $table->get_page_start(), $table->get_page_size()); } else { $results = $DB->get_records_sql($select . $from . $where . $sort, $params); } // Build table rows. if (!$download) { $table->initialbars(true); } if (!empty($results) || !empty($noresults)) { foreach ($results as $result) { $user = $DB->get_record('user', array('id' => $result->userid)); $picture = $OUTPUT->user_picture($user, array('courseid' => $course->id)); if (!empty($result->resultid)) { $checkbox = '<input type="checkbox" name="s' . $result->resultid . '" value="' . $result->resultid . '" />'; } else { $checkbox = ''; } if (!empty($result) && (empty($result->resultid) || $result->timefinish == 0)) { $resultdate = '-'; } else { $resultdate = userdate($result->timefinish, $strtimeformat); } if (!empty($result) && $result->offlinegroupid) { $groupletter = $letterstr[$groups[$result->offlinegroupid]->number]; } else { $groupletter = '-'; } $userlink = '<a href="' . $CFG->wwwroot . '/user/view.php?id=' . $result->userid . '&course=' . $course->id . '">' . fullname($result) . '</a>'; if (!$download) { $row = array($checkbox, $picture, $userlink, $result->{$offlinequizconfig->ID_field}, $resultdate, $groupletter); } else { $row = array($result->{$offlinequizconfig->ID_field}, $result->firstname, $result->lastname, $resultdate, $groupletter); } if (!empty($result) && $result->offlinegroupid) { $outputgrade = format_float($result->sumgrades / $groups[$result->offlinegroupid]->sumgrades * $offlinequiz->grade, $offlinequiz->decimalpoints); } else { $outputgrade = '-'; } if (!$download) { if ($result->status == 'partial') { $row[] = get_string('partial', 'offlinequiz'); } else { if ($result->sumgrades === null) { $row[] = '-'; } else { $row[] = '<a href="review.php?q=' . $offlinequiz->id . '&resultid=' . $result->resultid . '">' . $outputgrade . '</a>'; } } if ($withparticipants) { $row[] = !empty($checked[$result->userid]) ? "<img src=\"{$CFG->wwwroot}/mod/offlinequiz/pix/tick.gif\" alt=\"" . get_string('ischecked', 'offlinequiz') . "\">" : "<img src=\"{$CFG->wwwroot}/mod/offlinequiz/pix/cross.gif\" alt=\"" . get_string('isnotchecked', 'offlinequiz') . "\">"; } } else { if ($download != 'CSVplus1' || $download == 'CSVpluspoints') { $row[] = $result->sumgrades === null ? '-' : $outputgrade; if ($withparticipants) { if (array_key_exists($result->userid, $checked)) { $row[] = $checked[$result->userid] ? get_string('ok') : '-'; } else { $row[] = '-'; } } } } if (!$download) { $table->add_data($row); } else { if ($download == 'Excel' or $download == 'ODS') { $colnum = 0; foreach ($row as $item) { $myxls->write($rownum, $colnum, $item, $format); $colnum++; } $rownum++; } else { if ($download == 'CSV') { $text = implode(',', $row); echo $text . "\n"; } else { if ($download == 'CSVplus1' || $download == 'CSVpluspoints') { $text = $row[1] . ',' . $row[2] . ',' . $row[0] . ',' . $groups[$result->offlinegroupid]->number; if ($pages = $DB->get_records('offlinequiz_scanned_pages', array('resultid' => $result->resultid), 'pagenumber ASC')) { foreach ($pages as $page) { if ($page->status == 'ok' || $page->status == 'submitted') { $choices = $DB->get_records('offlinequiz_choices', array('scannedpageid' => $page->id), 'slotnumber, choicenumber'); $counter = 0; $oldslot = -1; $letters = array(); foreach ($choices as $choice) { if ($oldslot == -1) { $oldslot = $choice->slotnumber; } else { if ($oldslot != $choice->slotnumber) { if (empty($letters)) { $text .= ',99'; } else { $text .= ',' . implode('/', $letters); } $counter = 0; $oldslot = $choice->slotnumber; $letters = array(); } } if ($choice->value == 1) { $letters[] = $answerletters[$counter]; } $counter++; } if (empty($letters)) { $text .= ',99'; } else { $text .= ',' . implode('/', $letters); } } } } echo $text . "\n"; if ($download == 'CSVpluspoints') { $text = $row[1] . ',' . $row[2] . ',' . $row[0] . ',' . $groups[$result->offlinegroupid]->number; $quba = question_engine::load_questions_usage_by_activity($result->usageid); $slots = $quba->get_slots(); foreach ($slots as $slot) { $slotquestion = $quba->get_question($slot); $attempt = $quba->get_question_attempt($slot); $text .= ',' . format_float($attempt->get_mark(), $offlinequiz->decimalpoints, false); } echo $text . "\n"; } } } } } } // End foreach ($results... } else { if (!$download) { $table->print_initials_bar(); } } if (!$download) { // Print table. $table->finish_html(); if (!empty($results)) { echo '<form id="downloadoptions" action="report.php" method="get">'; echo ' <input type="hidden" name="id" value="' . $cm->id . '" />'; echo ' <input type="hidden" name="q" value="' . $offlinequiz->id . '" />'; echo ' <input type="hidden" name="mode" value="overview" />'; echo ' <input type="hidden" name="noheader" value="yes" />'; echo ' <table class="boxaligncenter"><tr><td>'; $options = array('Excel' => get_string('excelformat', 'offlinequiz'), 'ODS' => get_string('odsformat', 'offlinequiz'), 'CSV' => get_string('csvformat', 'offlinequiz'), 'CSVplus1' => get_string('csvplus1format', 'offlinequiz'), 'CSVpluspoints' => get_string('csvpluspointsformat', 'offlinequiz')); print_string('downloadresultsas', 'offlinequiz'); echo "</td><td>"; echo html_writer::select($options, 'download', '', false); echo ' <input type="submit" value="' . get_string('download') . '" />'; echo ' <script type="text/javascript">' . "\n<!--\n" . 'document.getElementById("noscriptmenuaction").style.display = "none";' . "\n-->\n" . '</script>'; echo " </td>\n"; echo "<td>"; echo "</td>\n"; echo '</tr></table></form>'; } } else { if ($download == 'Excel' || $download == 'ODS') { $workbook->close(); exit; } else { if ($download == 'CSV' || $download == 'CSVplus1' || $download == 'CSVpluspoints') { exit; } } } // Print display options. echo '<div class="controls">'; echo '<form id="options" action="report.php" method="get">'; echo '<div class=centerbox>'; echo '<p>' . get_string('displayoptions', 'offlinequiz') . ': </p>'; echo '<input type="hidden" name="id" value="' . $cm->id . '" />'; echo '<input type="hidden" name="q" value="' . $offlinequiz->id . '" />'; echo '<input type="hidden" name="mode" value="overview" />'; echo '<input type="hidden" name="detailedmarks" value="0" />'; echo '<table id="overview-options" class="boxaligncenter">'; echo '<tr align="left">'; echo '<td><label for="pagesize">' . get_string('pagesizeparts', 'offlinequiz') . '</label></td>'; echo '<td><input type="text" id="pagesize" name="pagesize" size="3" value="' . $pagesize . '" /></td>'; echo '</tr>'; echo '<tr align="left">'; echo '<td colspan="2">'; $options = array(); $options[] = get_string('attemptsonly', 'offlinequiz'); $options[] = get_string('noattemptsonly', 'offlinequiz'); $options[] = get_string('allstudents', 'offlinequiz'); $options[] = get_string('allresults', 'offlinequiz'); echo html_writer::select($options, 'noresults', $noresults, ''); echo '</td></tr>'; echo '<tr><td colspan="2" align="center">'; echo '<input type="submit" value="' . get_string('go') . '" />'; echo '</td></tr></table>'; echo '</div>'; echo '</form>'; echo '</div>'; echo "\n"; return true; }
/** * Test get_attempt_access_information */ public function test_get_attempt_access_information() { global $DB; // Create a new quiz with attempts. $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); $data = array('course' => $this->course->id, 'sumgrades' => 2); $quiz = $quizgenerator->create_instance($data); // Create some questions. $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $questiongenerator->create_question_category(); $question = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); quiz_add_quiz_question($question->id, $quiz); $question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); quiz_add_quiz_question($question->id, $quiz); // Add new question types in the category (for the random one). $question = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id)); $question = $questiongenerator->create_question('essay', null, array('category' => $cat->id)); $question = $questiongenerator->create_question('random', null, array('category' => $cat->id)); quiz_add_quiz_question($question->id, $quiz); $quizobj = quiz::create($quiz->id, $this->student->id); // Set grade to pass. $item = grade_item::fetch(array('courseid' => $this->course->id, 'itemtype' => 'mod', 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null)); $item->gradepass = 80; $item->update(); $this->setUser($this->student); // Default restrictions (none). $result = mod_quiz_external::get_attempt_access_information($quiz->id); $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result); $expected = array('isfinished' => false, 'preventnewattemptreasons' => [], 'warnings' => []); $this->assertEquals($expected, $result); // Limited attempts. $quiz->attempts = 1; $DB->update_record('quiz', $quiz); // Now, do one 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, false, $this->student->id); quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); quiz_attempt_save_started($quizobj, $quba, $attempt); // Process some responses from the student. $attemptobj = quiz_attempt::create($attempt->id); $tosubmit = array(1 => array('answer' => '3.14')); $attemptobj->process_submitted_actions($timenow, false, $tosubmit); // Finish the attempt. $attemptobj = quiz_attempt::create($attempt->id); $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question()); $attemptobj->process_finish($timenow, false); // Can we start a new attempt? We shall not! $result = mod_quiz_external::get_attempt_access_information($quiz->id, $attempt->id); $result = external_api::clean_returnvalue(mod_quiz_external::get_attempt_access_information_returns(), $result); // Now new attemps allowed. $this->assertCount(1, $result['preventnewattemptreasons']); $this->assertFalse($result['ispreflightcheckrequired']); $this->assertEquals(get_string('nomoreattempts', 'quiz'), $result['preventnewattemptreasons'][0]); }
public function test_is_manual_grade_in_range_ungraded() { $this->assertTrue(question_engine::is_manual_grade_in_range(1, 2)); }
public function make_behaviour(question_attempt $qa, $preferredbehaviour) { return question_engine::make_behaviour('manualgraded', $qa, $preferredbehaviour); }
public function test_quiz_get_user_attempts() { global $DB; $this->resetAfterTest(); $dg = $this->getDataGenerator(); $quizgen = $dg->get_plugin_generator('mod_quiz'); $course = $dg->create_course(); $u1 = $dg->create_user(); $u2 = $dg->create_user(); $u3 = $dg->create_user(); $u4 = $dg->create_user(); $role = $DB->get_record('role', ['shortname' => 'student']); $dg->enrol_user($u1->id, $course->id, $role->id); $dg->enrol_user($u2->id, $course->id, $role->id); $dg->enrol_user($u3->id, $course->id, $role->id); $dg->enrol_user($u4->id, $course->id, $role->id); $quiz1 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]); $quiz2 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]); // Questions. $questgen = $dg->get_plugin_generator('core_question'); $quizcat = $questgen->create_question_category(); $question = $questgen->create_question('numerical', null, ['category' => $quizcat->id]); quiz_add_quiz_question($question->id, $quiz1); quiz_add_quiz_question($question->id, $quiz2); $quizobj1a = quiz::create($quiz1->id, $u1->id); $quizobj1b = quiz::create($quiz1->id, $u2->id); $quizobj1c = quiz::create($quiz1->id, $u3->id); $quizobj1d = quiz::create($quiz1->id, $u4->id); $quizobj2a = quiz::create($quiz2->id, $u1->id); // Set attempts. $quba1a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1a->get_context()); $quba1a->set_preferred_behaviour($quizobj1a->get_quiz()->preferredbehaviour); $quba1b = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1b->get_context()); $quba1b->set_preferred_behaviour($quizobj1b->get_quiz()->preferredbehaviour); $quba1c = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1c->get_context()); $quba1c->set_preferred_behaviour($quizobj1c->get_quiz()->preferredbehaviour); $quba1d = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1d->get_context()); $quba1d->set_preferred_behaviour($quizobj1d->get_quiz()->preferredbehaviour); $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context()); $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour); $timenow = time(); // User 1 passes quiz 1. $attempt = quiz_create_attempt($quizobj1a, 1, false, $timenow, false, $u1->id); quiz_start_new_attempt($quizobj1a, $quba1a, $attempt, 1, $timenow); quiz_attempt_save_started($quizobj1a, $quba1a, $attempt); $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]); $attemptobj->process_finish($timenow, false); // User 2 goes overdue in quiz 1. $attempt = quiz_create_attempt($quizobj1b, 1, false, $timenow, false, $u2->id); quiz_start_new_attempt($quizobj1b, $quba1b, $attempt, 1, $timenow); quiz_attempt_save_started($quizobj1b, $quba1b, $attempt); $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_going_overdue($timenow, true); // User 3 does not finish quiz 1. $attempt = quiz_create_attempt($quizobj1c, 1, false, $timenow, false, $u3->id); quiz_start_new_attempt($quizobj1c, $quba1c, $attempt, 1, $timenow); quiz_attempt_save_started($quizobj1c, $quba1c, $attempt); // User 4 abandons the quiz 1. $attempt = quiz_create_attempt($quizobj1d, 1, false, $timenow, false, $u4->id); quiz_start_new_attempt($quizobj1d, $quba1d, $attempt, 1, $timenow); quiz_attempt_save_started($quizobj1d, $quba1d, $attempt); $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_abandon($timenow, true); // User 1 attempts the quiz three times (abandon, finish, in progress). $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context()); $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour); $attempt = quiz_create_attempt($quizobj2a, 1, false, $timenow, false, $u1->id); quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 1, $timenow); quiz_attempt_save_started($quizobj2a, $quba2a, $attempt); $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_abandon($timenow, true); $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context()); $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour); $attempt = quiz_create_attempt($quizobj2a, 2, false, $timenow, false, $u1->id); quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 2, $timenow); quiz_attempt_save_started($quizobj2a, $quba2a, $attempt); $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_finish($timenow, false); $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context()); $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour); $attempt = quiz_create_attempt($quizobj2a, 3, false, $timenow, false, $u1->id); quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 3, $timenow); quiz_attempt_save_started($quizobj2a, $quba2a, $attempt); // Check for user 1. $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'all'); $this->assertCount(1, $attempts); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::FINISHED, $attempt->state); $this->assertEquals($u1->id, $attempt->userid); $this->assertEquals($quiz1->id, $attempt->quiz); $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'finished'); $this->assertCount(1, $attempts); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::FINISHED, $attempt->state); $this->assertEquals($u1->id, $attempt->userid); $this->assertEquals($quiz1->id, $attempt->quiz); $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'unfinished'); $this->assertCount(0, $attempts); // Check for user 2. $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'all'); $this->assertCount(1, $attempts); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state); $this->assertEquals($u2->id, $attempt->userid); $this->assertEquals($quiz1->id, $attempt->quiz); $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'finished'); $this->assertCount(0, $attempts); $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'unfinished'); $this->assertCount(1, $attempts); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state); $this->assertEquals($u2->id, $attempt->userid); $this->assertEquals($quiz1->id, $attempt->quiz); // Check for user 3. $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'all'); $this->assertCount(1, $attempts); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state); $this->assertEquals($u3->id, $attempt->userid); $this->assertEquals($quiz1->id, $attempt->quiz); $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'finished'); $this->assertCount(0, $attempts); $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'unfinished'); $this->assertCount(1, $attempts); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state); $this->assertEquals($u3->id, $attempt->userid); $this->assertEquals($quiz1->id, $attempt->quiz); // Check for user 4. $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'all'); $this->assertCount(1, $attempts); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state); $this->assertEquals($u4->id, $attempt->userid); $this->assertEquals($quiz1->id, $attempt->quiz); $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'finished'); $this->assertCount(1, $attempts); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state); $this->assertEquals($u4->id, $attempt->userid); $this->assertEquals($quiz1->id, $attempt->quiz); $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'unfinished'); $this->assertCount(0, $attempts); // Multiple attempts for user 1 in quiz 2. $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'all'); $this->assertCount(3, $attempts); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state); $this->assertEquals($u1->id, $attempt->userid); $this->assertEquals($quiz2->id, $attempt->quiz); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::FINISHED, $attempt->state); $this->assertEquals($u1->id, $attempt->userid); $this->assertEquals($quiz2->id, $attempt->quiz); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state); $this->assertEquals($u1->id, $attempt->userid); $this->assertEquals($quiz2->id, $attempt->quiz); $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'finished'); $this->assertCount(2, $attempts); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::FINISHED, $attempt->state); $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'unfinished'); $this->assertCount(1, $attempts); $attempt = array_shift($attempts); // Multiple quiz attempts fetched at once. $attempts = quiz_get_user_attempts([$quiz1->id, $quiz2->id], $u1->id, 'all'); $this->assertCount(4, $attempts); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::FINISHED, $attempt->state); $this->assertEquals($u1->id, $attempt->userid); $this->assertEquals($quiz1->id, $attempt->quiz); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state); $this->assertEquals($u1->id, $attempt->userid); $this->assertEquals($quiz2->id, $attempt->quiz); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::FINISHED, $attempt->state); $this->assertEquals($u1->id, $attempt->userid); $this->assertEquals($quiz2->id, $attempt->quiz); $attempt = array_shift($attempts); $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state); $this->assertEquals($u1->id, $attempt->userid); $this->assertEquals($quiz2->id, $attempt->quiz); }
public function test_load_with_unnecessary_autosaved_data() { // The point here is that the somehow (probably due to two things // happening concurrently, we have autosaved data in the database that // has already been superceded by real data, so it should be ignored. // There is also a second lot of redundant data to delete. $records = new question_test_recordset(array(array('questionattemptid', 'contextid', 'questionusageid', 'slot', 'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', 'flagged', 'questionsummary', 'rightanswer', 'responsesummary', 'timemodified', 'attemptstepid', 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid', 'name', 'value'), array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0, 0.0, 1.0, 0, '', '', '', 1256233790, 5, -2, 'complete', null, 1256233715, 1, 'answer', '0'), array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0, 0.0, 1.0, 0, '', '', '', 1256233790, 4, -1, 'complete', null, 1256233715, 1, 'answer', '0'), array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0, 0.0, 1.0, 0, '', '', '', 1256233790, 1, 0, 'todo', null, 1256233700, 1, null, null), array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0, 0.0, 1.0, 0, '', '', '', 1256233790, 2, 1, 'complete', null, 1256233705, 1, 'answer', '1'), array(1, 123, 1, 1, 'deferredfeedback', -1, 1, 2.0, 0.0, 1.0, 1, '', '', '', 1256233790, 3, 2, 'complete', null, 1256233710, 1, 'answer', '0'))); $question = test_question_maker::make_question('truefalse', 'true'); $question->id = -1; question_bank::start_unit_test(); question_bank::load_test_question_data($question); $observer = new testable_question_engine_unit_of_work(question_engine::make_questions_usage_by_activity('unit_test', context_system::instance())); $qa = question_attempt::load_from_records($records, 1, $observer, 'deferredfeedback'); question_bank::end_unit_test(); $this->assertEquals($question->questiontext, $qa->get_question()->questiontext); $this->assertEquals(3, $qa->get_num_steps()); $this->assertFalse($qa->has_autosaved_step()); $step = $qa->get_step(0); $this->assertEquals(question_state::$todo, $step->get_state()); $this->assertNull($step->get_fraction()); $this->assertEquals(1256233700, $step->get_timecreated()); $this->assertEquals(1, $step->get_user_id()); $this->assertEquals(array(), $step->get_all_data()); $step = $qa->get_step(1); $this->assertEquals(question_state::$complete, $step->get_state()); $this->assertNull($step->get_fraction()); $this->assertEquals(1256233705, $step->get_timecreated()); $this->assertEquals(1, $step->get_user_id()); $this->assertEquals(array('answer' => '1'), $step->get_all_data()); $step = $qa->get_step(2); $this->assertEquals(question_state::$complete, $step->get_state()); $this->assertNull($step->get_fraction()); $this->assertEquals(1256233710, $step->get_timecreated()); $this->assertEquals(1, $step->get_user_id()); $this->assertEquals(array('answer' => '0'), $step->get_all_data()); $this->assertEquals(2, count($observer->get_steps_deleted())); }
/** * @param array $questionids of question ids. * @return boolean whether any of these questions are being used by any part of Moodle. */ function questions_in_use($questionids) { global $CFG; if (question_engine::questions_in_use($questionids)) { return true; } foreach (core_component::get_plugin_list('mod') as $module => $path) { $lib = $path . '/lib.php'; if (is_readable($lib)) { include_once $lib; $fn = $module . '_questions_in_use'; if (function_exists($fn)) { if ($fn($questionids)) { return true; } } else { // Fallback for legacy modules. $fn = $module . '_question_list_instances'; if (function_exists($fn)) { foreach ($questionids as $questionid) { $instances = $fn($questionid); if (!empty($instances)) { return true; } } } } } } return false; }
/** * Save changes to question instance * * Saves changes to the question grades in the quiz_question_instances table. * It does not update 'sumgrades' in the quiz table. * * @param int grade The maximal grade for the question * @param int $questionid The id of the question * @param int $quizid The id of the quiz to update / add the instances for. */ function quiz_update_question_instance($grade, $questionid, $quiz) { global $DB; $instance = $DB->get_record('quiz_question_instances', array('quiz' => $quiz->id, 'question' => $questionid)); $slot = quiz_get_slot_for_question($quiz, $questionid); if (!$instance || !$slot) { throw new coding_exception('Attempt to change the grade of a quesion not in the quiz.'); } if (abs($grade - $instance->grade) < 1e-7) { // Grade has not changed. Nothing to do. return; } $instance->grade = $grade; $DB->update_record('quiz_question_instances', $instance); question_engine::set_max_mark_in_attempts(new qubaids_for_quiz($quiz->id), $slot, $grade); }
/** * Create a question_attempt_with_restricted_history * @param question_attempt $baseqa The question_attempt to make a restricted version of. * @param int $lastseq the index of the last step to include. * @param string $preferredbehaviour the preferred behaviour. It is slightly * annoyting that this needs to be passed, but unavoidable for now. */ public function __construct(question_attempt $baseqa, $lastseq, $preferredbehaviour) { $this->baseqa = $baseqa->get_full_qa(); if ($lastseq < 0 || $lastseq >= $this->baseqa->get_num_steps()) { throw new coding_exception('$lastseq out of range', $lastseq); } $this->steps = array_slice($this->baseqa->steps, 0, $lastseq + 1); $this->observer = new question_usage_null_observer(); // This should be a straight copy of all the remaining fields. $this->id = $this->baseqa->id; $this->usageid = $this->baseqa->usageid; $this->slot = $this->baseqa->slot; $this->question = $this->baseqa->question; $this->maxmark = $this->baseqa->maxmark; $this->minfraction = $this->baseqa->minfraction; $this->maxfraction = $this->baseqa->maxfraction; $this->questionsummary = $this->baseqa->questionsummary; $this->responsesummary = $this->baseqa->responsesummary; $this->rightanswer = $this->baseqa->rightanswer; $this->flagged = $this->baseqa->flagged; // Except behaviour, where we need to create a new one. $this->behaviour = question_engine::make_behaviour($this->baseqa->get_behaviour_name(), $this, $preferredbehaviour); }
/** * Regrade a particular quiz attempt. Either for real ($dryrun = false), or * as a pretend regrade to see which fractions would change. The outcome is * stored in the quiz_overview_regrades table. * * Note, $attempt is not upgraded in the database. The caller needs to do that. * However, $attempt->sumgrades is updated, if this is not a dry run. * * @param object $attempt the quiz attempt to regrade. * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real. * @param array $slots if null, regrade all questions, otherwise, just regrade * the quetsions with those slots. */ protected function regrade_attempt($attempt, $dryrun = false, $slots = null) { global $DB; $transaction = $DB->start_delegated_transaction(); $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid); if (is_null($slots)) { $slots = $quba->get_slots(); } $finished = $attempt->timefinish > 0; foreach ($slots as $slot) { $qqr = new stdClass(); $qqr->oldfraction = $quba->get_question_fraction($slot); $quba->regrade_question($slot, $finished); $qqr->newfraction = $quba->get_question_fraction($slot); if (abs($qqr->oldfraction - $qqr->newfraction) > 1.0E-7) { $qqr->questionusageid = $quba->get_id(); $qqr->slot = $slot; $qqr->regraded = empty($dryrun); $qqr->timemodified = time(); $DB->insert_record('quiz_overview_regrades', $qqr, false); } } if (!$dryrun) { question_engine::save_questions_usage_by_activity($quba); } $transaction->allow_commit(); }
if (empty($quizobj->get_quiz()->showblocks)) { $PAGE->blocks->show_only_fake_blocks(); } echo $output->start_attempt_page($quizobj, $mform); die; } } // Pre-flight check passed. $accessmanager->notify_preflight_check_passed($currentattemptid); } if ($currentattemptid) { redirect($quizobj->attempt_url($currentattemptid, $page)); } // Delete any previous preview attempts belonging to this user. quiz_delete_previews($quizobj->get_quiz(), $USER->id); $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); // Create the new attempt and initialize the question sessions $timenow = time(); // Update time now, in case the server is running really slowly. $attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow, $quizobj->is_preview_user()); if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) { $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow); } else { $attempt = quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt); } $transaction = $DB->start_delegated_transaction(); $attempt = quiz_attempt_save_started($quizobj, $quba, $attempt); quiz_fire_attempt_started_event($attempt, $quizobj); $transaction->allow_commit(); // Redirect to the attempt page.