mirror of
https://github.com/moodle/moodle.git
synced 2025-01-18 22:08:20 +01:00
MDL-74255 quiz: fix redo and regrade question version handling
This commit is contained in:
parent
839cccead4
commit
a43f56836a
@ -2111,8 +2111,9 @@ class quiz_attempt {
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
|
||||
// Add the question to the usage. It is important we do this before we choose a variant.
|
||||
$newquestion = question_bank::load_question(
|
||||
\mod_quiz\question\bank\qbank_helper::choose_question_for_redo($this->slots[$slot]->id, $qubaids));
|
||||
$newquestionid = qbank_helper::choose_question_for_redo($this->get_quizid(),
|
||||
$this->get_quizobj()->get_context(), $this->slots[$slot]->id, $qubaids);
|
||||
$newquestion = question_bank::load_question($newquestionid, $this->get_quiz()->shuffleanswers);
|
||||
$newslot = $this->quba->add_question_in_place_of_other($slot, $newquestion);
|
||||
|
||||
// Choose the variant.
|
||||
|
@ -58,7 +58,7 @@ class submit_question_version extends external_api {
|
||||
* Set the questions slot parameters to display the question template.
|
||||
*
|
||||
* @param int $slotid Slot id to display.
|
||||
* @param int $newversion
|
||||
* @param int $newversion the version to set. 0 means 'always latest'.
|
||||
* @return array
|
||||
*/
|
||||
public static function execute(int $slotid, int $newversion): array {
|
||||
|
@ -17,6 +17,9 @@
|
||||
namespace mod_quiz\question\bank;
|
||||
|
||||
use core_question\local\bank\question_version_status;
|
||||
use core_question\local\bank\random_question_loader;
|
||||
use qubaid_condition;
|
||||
use quiz_attempt;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@ -34,22 +37,6 @@ require_once($CFG->dirroot . '/mod/quiz/attemptlib.php');
|
||||
*/
|
||||
class qbank_helper {
|
||||
|
||||
/**
|
||||
* Check if the slot is a random question or not.
|
||||
*
|
||||
* @param int $slotid
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_random(int $slotid): bool {
|
||||
global $DB;
|
||||
$params = [
|
||||
'itemid' => $slotid,
|
||||
'component' => 'mod_quiz',
|
||||
'questionarea' => 'slot'
|
||||
];
|
||||
return $DB->record_exists('question_set_references', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available versions of a question where one of the version has the given question id.
|
||||
*
|
||||
@ -78,39 +65,6 @@ class qbank_helper {
|
||||
", [$questionid, question_version_status::QUESTION_STATUS_DRAFT]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the question id from slot id.
|
||||
*
|
||||
* @param int $slotid
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_question_for_redo(int $slotid) {
|
||||
global $DB;
|
||||
$params = [
|
||||
'itemid' => $slotid,
|
||||
'component' => 'mod_quiz',
|
||||
'questionarea' => 'slot'
|
||||
];
|
||||
$referencerecord = $DB->get_record('question_references', $params);
|
||||
if ($referencerecord->version === null) {
|
||||
$questionsql = 'SELECT q.id
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
WHERE qv.version = (SELECT MAX(v.version)
|
||||
FROM {question_versions} v
|
||||
JOIN {question_bank_entries} be
|
||||
ON be.id = v.questionbankentryid
|
||||
WHERE be.id = qv.questionbankentryid)
|
||||
AND qv.questionbankentryid = ?';
|
||||
$questionid = $DB->get_record_sql($questionsql, [$referencerecord->questionbankentryid])->id;
|
||||
} else {
|
||||
$questionid = $DB->get_field('question_versions', 'questionid',
|
||||
['questionbankentryid' => $referencerecord->questionbankentryid,
|
||||
'version' => $referencerecord->version]);
|
||||
}
|
||||
return $questionid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the information about which questions should be used to create a quiz attempt.
|
||||
*
|
||||
@ -124,12 +78,25 @@ class qbank_helper {
|
||||
* randomtags, and note that these also have a ->name set and ->qtype set to 'random'.
|
||||
*
|
||||
* @param int $quizid the id of the quiz to load the data for.
|
||||
* @param \context $quizcontext
|
||||
* @param \context_module $quizcontext the context of this quiz.
|
||||
* @param int|null $slotid optional, if passed only load the data for this one slot (if it is in this quiz).
|
||||
* @return array indexed by slot, with information about the content of each slot.
|
||||
*/
|
||||
public static function get_question_structure(int $quizid, \context $quizcontext) {
|
||||
public static function get_question_structure(int $quizid, \context_module $quizcontext,
|
||||
int $slotid = null): array {
|
||||
global $DB;
|
||||
|
||||
$params = [
|
||||
'draft' => question_version_status::QUESTION_STATUS_DRAFT,
|
||||
'quizcontextid' => $quizcontext->id, 'quizcontextid2' => $quizcontext->id,
|
||||
'quizid' => $quizid
|
||||
];
|
||||
$slotidtest = '';
|
||||
if ($slotid !== null) {
|
||||
$params['slotid'] = $slotid;
|
||||
$slotidtest = ' AND slot.id = :slotid';
|
||||
}
|
||||
|
||||
// Load all the data about each slot.
|
||||
$slotdata = $DB->get_records_sql("
|
||||
SELECT slot.slot,
|
||||
@ -169,12 +136,12 @@ class qbank_helper {
|
||||
AND qsr.questionarea = 'slot' AND qsr.itemid = slot.id
|
||||
|
||||
WHERE slot.quizid = :quizid
|
||||
ORDER BY slot.slot
|
||||
", ['draft' => question_version_status::QUESTION_STATUS_DRAFT,
|
||||
'quizcontextid' => $quizcontext->id, 'quizcontextid2' => $quizcontext->id,
|
||||
'quizid' => $quizid]);
|
||||
$slotidtest
|
||||
|
||||
// Uppack the random info from question_set_reference.
|
||||
ORDER BY slot.slot
|
||||
", $params);
|
||||
|
||||
// Unpack the random info from question_set_reference.
|
||||
foreach ($slotdata as $slot) {
|
||||
// Ensure the right id is the id.
|
||||
$slot->id = $slot->slotid;
|
||||
@ -184,7 +151,7 @@ class qbank_helper {
|
||||
$filtercondition = json_decode($slot->filtercondition);
|
||||
$slot->questionid = 's' . $slot->id; // Sometimes this is used as an array key, so needs to be unique.
|
||||
$slot->category = $filtercondition->questioncategoryid;
|
||||
$slot->randomrecurse = $filtercondition->includingsubcategories;
|
||||
$slot->randomrecurse = (bool) $filtercondition->includingsubcategories;
|
||||
$slot->randomtags = isset($filtercondition->tags) ? (array) $filtercondition->tags : [];
|
||||
$slot->qtype = 'random';
|
||||
$slot->name = get_string('random', 'quiz');
|
||||
@ -229,34 +196,31 @@ class qbank_helper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose question for redo.
|
||||
* Choose question for redo in a particular slot.
|
||||
*
|
||||
* @param int $slotid
|
||||
* @param \qubaid_condition $qubaids
|
||||
* @return int
|
||||
* @param int $quizid the id of the quiz to load the data for.
|
||||
* @param \context_module $quizcontext the context of this quiz.
|
||||
* @param int $slotid optional, if passed only load the data for this one slot (if it is in this quiz).
|
||||
* @param qubaid_condition $qubaids attempts to consider when avoiding picking repeats of random questions.
|
||||
* @return int the id of the question to use.
|
||||
*/
|
||||
public static function choose_question_for_redo($slotid, $qubaids): int {
|
||||
// Choose the replacement question.
|
||||
if (!self::is_random($slotid)) {
|
||||
$newqusetionid = self::get_question_for_redo($slotid);
|
||||
} else {
|
||||
$tagids = [];
|
||||
$randomquestiondata = self::get_random_question_data_from_slot($slotid);
|
||||
$filtercondition = json_decode($randomquestiondata->filtercondition);
|
||||
if (isset($filtercondition->tags)) {
|
||||
foreach ($filtercondition->tags as $tag) {
|
||||
$tagstring = explode(',', $tag);
|
||||
$tagids [] = $tagstring[0];
|
||||
}
|
||||
}
|
||||
public static function choose_question_for_redo(int $quizid, \context_module $quizcontext,
|
||||
int $slotid, qubaid_condition $qubaids): int {
|
||||
$slotdata = self::get_question_structure($quizid, $quizcontext, $slotid);
|
||||
$slotdata = reset($slotdata);
|
||||
|
||||
$randomloader = new \core_question\local\bank\random_question_loader($qubaids, []);
|
||||
$newqusetionid = $randomloader->get_next_question_id($filtercondition->questioncategoryid,
|
||||
(bool) $filtercondition->includingsubcategories, $tagids);
|
||||
if ($newqusetionid === null) {
|
||||
throw new \moodle_exception('notenoughrandomquestions', 'quiz');
|
||||
}
|
||||
// Non-random question.
|
||||
if ($slotdata->qtype != 'random') {
|
||||
return $slotdata->questionid;
|
||||
}
|
||||
|
||||
// Random question.
|
||||
$randomloader = new random_question_loader($qubaids, []);
|
||||
$newqusetionid = $randomloader->get_next_question_id($slotdata->category,
|
||||
$slotdata->randomrecurse, self::get_tag_ids_for_slot($slotdata));
|
||||
|
||||
if ($newqusetionid === null) {
|
||||
throw new \moodle_exception('notenoughrandomquestions', 'quiz');
|
||||
}
|
||||
return $newqusetionid;
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ abstract class quiz_attempts_report extends quiz_default_report {
|
||||
* 3 => \core\dml\sql_join Contains joins, wheres, params for all the students to show in the report.
|
||||
* Will be the same as either element 1 or 2.
|
||||
*/
|
||||
protected function init($mode, $formclass, $quiz, $cm, $course) {
|
||||
public function init($mode, $formclass, $quiz, $cm, $course) {
|
||||
$this->mode = $mode;
|
||||
|
||||
$this->context = context_module::instance($cm->id);
|
||||
|
@ -22,6 +22,7 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use mod_quiz\question\bank\qbank_helper;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@ -45,11 +46,9 @@ class quiz_overview_report extends quiz_attempts_report {
|
||||
protected $hasgroupstudents;
|
||||
|
||||
/**
|
||||
* @var array|null used during regrades, to cache which new questionid to use for each old on.
|
||||
* for slots which are not random, stores slot => questionid for the question to use.
|
||||
* See get_new_question_for_regrade.
|
||||
* @var array|null cached copy of qbank_helper::get_question_structure for use during regrades.
|
||||
*/
|
||||
protected $slotquestionids = null;
|
||||
protected $structureforregrade = null;
|
||||
|
||||
/**
|
||||
* @var array|null used during regrades, to cache which new questionid to use for each old on.
|
||||
@ -339,7 +338,7 @@ class quiz_overview_report extends quiz_attempts_report {
|
||||
* @param array $slots if null, regrade all questions, otherwise, just regrade
|
||||
* the questions with those slots.
|
||||
*/
|
||||
protected function regrade_attempt($attempt, $dryrun = false, $slots = null) {
|
||||
public function regrade_attempt($attempt, $dryrun = false, $slots = null) {
|
||||
global $DB;
|
||||
// Need more time for a quiz with many questions.
|
||||
core_php_time_limit::raise(300);
|
||||
@ -394,6 +393,14 @@ class quiz_overview_report extends quiz_attempts_report {
|
||||
gc_collect_cycles();
|
||||
}
|
||||
|
||||
/**
|
||||
* For use in tests only. Clear the cached regrade data.
|
||||
*/
|
||||
public function clear_regrade_date_cache(): void {
|
||||
$this->structureforregrade = null;
|
||||
$this->newquestionidsforold = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Work out of we should be using a new question version for a particular slot in a regrade.
|
||||
*
|
||||
@ -407,40 +414,27 @@ class quiz_overview_report extends quiz_attempts_report {
|
||||
global $DB;
|
||||
|
||||
// If the cache is empty, get information about all the slots.
|
||||
if ($this->slotquestionids === null) {
|
||||
if ($this->structureforregrade === null) {
|
||||
$this->newquestionidsforold = [];
|
||||
// Load the data about all the non-random slots now.
|
||||
$this->slotquestionids = $DB->get_records_sql_menu("
|
||||
SELECT slot.slot, qve.questionid
|
||||
FROM {quiz_slots} slot
|
||||
JOIN {question_references} qref ON qref.itemid = slot.id
|
||||
AND qref.component = 'mod_quiz' AND qref.questionarea = 'slot' AND qref.usingcontextid = ?
|
||||
JOIN {question_versions} qve ON qve.questionbankentryid = qref.questionbankentryid
|
||||
WHERE slot.quizid = ?
|
||||
AND (qve.version = qref.version OR
|
||||
qref.version IS NULL AND qve.version = (
|
||||
SELECT MAX(version)
|
||||
FROM {question_versions} iqve
|
||||
WHERE iqve.questionbankentryid = qref.questionbankentryid
|
||||
)
|
||||
)
|
||||
", [$this->context->id, $attempt->quiz]);
|
||||
$this->structureforregrade = qbank_helper::get_question_structure(
|
||||
$attempt->quiz, $this->context);
|
||||
}
|
||||
|
||||
// If this is a non-random slot, we will have the right info cached.
|
||||
if (array_key_exists($slot, $this->slotquestionids)) {
|
||||
if ($this->structureforregrade[$slot]->qtype != 'random') {
|
||||
// This is a non-random slot.
|
||||
return question_bank::load_question($this->slotquestionids[$slot]);
|
||||
return question_bank::load_question($this->structureforregrade[$slot]->questionid);
|
||||
}
|
||||
|
||||
// We must be dealing with a random question. Check that cache.
|
||||
$currentquestion = $quba->get_question_attempt($slot)->get_question(false);
|
||||
if (array_key_exists($currentquestion->id, $this->newquestionidsforold)) {
|
||||
if (isset($this->newquestionidsforold[$currentquestion->id])) {
|
||||
return question_bank::load_question($this->newquestionidsforold[$currentquestion->id]);
|
||||
}
|
||||
|
||||
// This is a random question we have not seen yet. Find the latest version.
|
||||
$versionsoptions = \mod_quiz\question\bank\qbank_helper::get_version_options($currentquestion->id);
|
||||
$versionsoptions = qbank_helper::get_version_options($currentquestion->id);
|
||||
$latestversion = reset($versionsoptions);
|
||||
$this->newquestionidsforold[$currentquestion->id] = $latestversion->questionid;
|
||||
return question_bank::load_question($latestversion->questionid);
|
||||
|
@ -22,8 +22,8 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use core_question\local\bank\question_version_status;
|
||||
use mod_quiz\external\submit_question_version;
|
||||
use mod_quiz\question\bank\qbank_helper;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@ -32,6 +32,7 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/default.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/overview/report.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_form.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/overview/tests/helpers.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
|
||||
|
||||
@ -41,7 +42,6 @@ require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.ph
|
||||
*
|
||||
* @copyright 2014 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @coversDefaultClass \question_usage_by_activity
|
||||
*/
|
||||
class quiz_overview_report_testcase extends advanced_testcase {
|
||||
use \quiz_question_helper_test_trait;
|
||||
@ -51,19 +51,19 @@ class quiz_overview_report_testcase extends advanced_testcase {
|
||||
*
|
||||
* @return array the data for the test sub-cases.
|
||||
*/
|
||||
public function report_sql_cases() {
|
||||
public function report_sql_cases(): array {
|
||||
return [[null], ['csv']]; // Only need to test on or off, not all download types.
|
||||
}
|
||||
|
||||
/**
|
||||
* Test how the report queries the database.
|
||||
*
|
||||
* @param bool $isdownloading a download type, or null.
|
||||
* @param string|null $isdownloading a download type, or null.
|
||||
* @dataProvider report_sql_cases
|
||||
*/
|
||||
public function test_report_sql($isdownloading) {
|
||||
public function test_report_sql(?string $isdownloading): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest(true);
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create a course and a quiz.
|
||||
$generator = $this->getDataGenerator();
|
||||
@ -74,6 +74,7 @@ class quiz_overview_report_testcase extends advanced_testcase {
|
||||
'attempts' => 10));
|
||||
|
||||
// Add one question.
|
||||
/** @var core_question_generator $questiongenerator */
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
$q = $questiongenerator->create_question('essay', 'plain', ['category' => $cat->id]);
|
||||
@ -244,7 +245,7 @@ class quiz_overview_report_testcase extends advanced_testcase {
|
||||
* Bands provider.
|
||||
* @return array
|
||||
*/
|
||||
public function get_bands_count_and_width_provider() {
|
||||
public function get_bands_count_and_width_provider(): array {
|
||||
return [
|
||||
[10, [20, .5]],
|
||||
[20, [20, 1]],
|
||||
@ -263,8 +264,8 @@ class quiz_overview_report_testcase extends advanced_testcase {
|
||||
* @param int $grade grade
|
||||
* @param array $expected
|
||||
*/
|
||||
public function test_get_bands_count_and_width($grade, $expected) {
|
||||
$this->resetAfterTest(true);
|
||||
public function test_get_bands_count_and_width(int $grade, array $expected): void {
|
||||
$this->resetAfterTest();
|
||||
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||
$quiz = $quizgenerator->create_instance(['course' => SITEID, 'grade' => $grade]);
|
||||
$this->assertEquals($expected, quiz_overview_report::get_bands_count_and_width($quiz));
|
||||
@ -273,8 +274,8 @@ class quiz_overview_report_testcase extends advanced_testcase {
|
||||
/**
|
||||
* Test delete_selected_attempts function.
|
||||
*/
|
||||
public function test_delete_selected_attempts() {
|
||||
$this->resetAfterTest(true);
|
||||
public function test_delete_selected_attempts(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$timestamp = 1234567890;
|
||||
$timestart = $timestamp + 3600;
|
||||
@ -292,6 +293,7 @@ class quiz_overview_report_testcase extends advanced_testcase {
|
||||
]);
|
||||
|
||||
// Add one question.
|
||||
/** @var core_question_generator $questiongenerator */
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
$q = $questiongenerator->create_question('essay', 'plain', ['category' => $cat->id]);
|
||||
@ -326,53 +328,80 @@ class quiz_overview_report_testcase extends advanced_testcase {
|
||||
* @covers ::regrade_question
|
||||
*/
|
||||
public function test_regrade_question() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$quiz = $this->create_test_quiz($course);
|
||||
$student = $this->getDataGenerator()->create_user();
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $course->id)->id);
|
||||
$cm = get_fast_modinfo($course->id)->get_cm($quiz->cmid);
|
||||
$context = context_module::instance($quiz->cmid);
|
||||
|
||||
/** @var core_question_generator $questiongenerator */
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
// Create a couple of questions.
|
||||
$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
|
||||
$numq = $questiongenerator->create_question('essay', null,
|
||||
['category' => $cat->id, 'name' => 'This is the first version', 'correctanswer' => false]);
|
||||
// Create two version.
|
||||
$numq2 = $questiongenerator->update_question($numq, null,
|
||||
['name' => 'This is the second version', 'correctanswer' => true]);
|
||||
$numq3 = $questiongenerator->update_question($numq, null,
|
||||
['name' => 'This is the third version', 'correctanswer' => false]);
|
||||
quiz_add_quiz_question($numq->id, $quiz);
|
||||
// Create the quiz object.
|
||||
$quizobj = \quiz::create($quiz->id);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
$slots = $structure->get_slots();
|
||||
$slot = reset($slots);
|
||||
// Now change the version using the external service.
|
||||
$versions = qbank_helper::get_version_options($slot->questionid);
|
||||
// We dont want the current version.
|
||||
$selectversions = [];
|
||||
foreach ($versions as $version) {
|
||||
if ($version->version === $slot->version) {
|
||||
continue;
|
||||
}
|
||||
$selectversions[$version->version] = $version;
|
||||
}
|
||||
// Change to version 1, with correct response.
|
||||
$this->expectException('moodle_exception');
|
||||
submit_question_version::execute($slot->id, (int)$selectversions[1]->version);
|
||||
list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $student);
|
||||
$q = $questiongenerator->create_question('shortanswer', null,
|
||||
['category' => $cat->id, 'name' => 'Toad scores 0.8']);
|
||||
|
||||
// Create a version, the last one draft.
|
||||
// Sadly, update_question is a bit dodgy, so it can't handle updating the answer score.
|
||||
$q2 = $questiongenerator->update_question($q, null,
|
||||
['name' => 'Toad now scores 1.0']);
|
||||
$toadanswer = $DB->get_record_select('question_answers',
|
||||
'question = ? AND ' . $DB->sql_compare_text('answer') . ' = ?',
|
||||
[$q2->id, 'toad'], '*', MUST_EXIST);
|
||||
$DB->set_field('question_answers', 'fraction', 1, ['id' => $toadanswer->id]);
|
||||
|
||||
// Add the question to the quiz.
|
||||
quiz_add_quiz_question($q2->id, $quiz, 0, 10);
|
||||
|
||||
// Attempt the quiz, submitting response 'toad'.
|
||||
$quizobj = quiz::create($quiz->id);
|
||||
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$attemptobj->process_submitted_actions(time(), false, [1 => ['answer' => 'toad']]);
|
||||
$attemptobj->process_finish(time(), false);
|
||||
|
||||
// We should be using 'always latest' version, which is currently v2, so should be right.
|
||||
$this->assertEquals(10, $attemptobj->get_question_usage()->get_total_mark());
|
||||
|
||||
// Change to version 2, with wrong response.
|
||||
submit_question_version::execute($slot->id, (int)$selectversions[2]->version);
|
||||
$quba->regrade_question(1, quiz_attempt::FINISHED, null, $numq2->id);
|
||||
$this->assertEquals(0, $attemptobj->get_question_usage()->get_total_mark());
|
||||
// Now change the quiz to use fixed version 1.
|
||||
$slot = $quizobj->get_question($q2->id);
|
||||
submit_question_version::execute($slot->slotid, 1);
|
||||
|
||||
// Change to version 3, with correct response.
|
||||
submit_question_version::execute($slot->id, (int)$selectversions[3]->version);
|
||||
$quba->regrade_question(1, quiz_attempt::FINISHED, null, $numq3->id);
|
||||
$this->assertEquals(10, $attemptobj->get_question_usage()->get_total_mark());
|
||||
// Regrade.
|
||||
$report = new quiz_overview_report();
|
||||
$report->init('overview', 'quiz_overview_settings_form', $quiz, $cm, $course);
|
||||
$report->regrade_attempt($attempt);
|
||||
|
||||
// The mark should now be 8.
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$this->assertEquals(8, $attemptobj->get_question_usage()->get_total_mark());
|
||||
|
||||
// Now add two more versions, the second of which is draft.
|
||||
$q3 = $questiongenerator->update_question($q, null,
|
||||
['name' => 'Toad now scores 0.5']);
|
||||
$toadanswer = $DB->get_record_select('question_answers',
|
||||
'question = ? AND ' . $DB->sql_compare_text('answer') . ' = ?',
|
||||
[$q3->id, 'toad'], '*', MUST_EXIST);
|
||||
$DB->set_field('question_answers', 'fraction', 0.5, ['id' => $toadanswer->id]);
|
||||
|
||||
$q4 = $questiongenerator->update_question($q, null,
|
||||
['name' => 'Toad now scores 0.3',
|
||||
'status' => question_version_status::QUESTION_STATUS_DRAFT]);
|
||||
$toadanswer = $DB->get_record_select('question_answers',
|
||||
'question = ? AND ' . $DB->sql_compare_text('answer') . ' = ?',
|
||||
[$q4->id, 'toad'], '*', MUST_EXIST);
|
||||
$DB->set_field('question_answers', 'fraction', 0.3, ['id' => $toadanswer->id]);
|
||||
|
||||
// Now change the quiz back to always latest and regrade again.
|
||||
submit_question_version::execute($slot->slotid, 0);
|
||||
$report->clear_regrade_date_cache();
|
||||
$report->regrade_attempt($attempt);
|
||||
|
||||
// Score should now be 5, because v3 is the latest non-draft version.
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$this->assertEquals(5, $attemptobj->get_question_usage()->get_total_mark());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -86,9 +86,6 @@ class mod_quiz_attempt_walkthrough_testcase extends advanced_testcase {
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
|
||||
|
||||
$prefix1 = $quba->get_field_prefix(1);
|
||||
$prefix2 = $quba->get_field_prefix(2);
|
||||
|
||||
$tosubmit = array(1 => array('answer' => 'frog'),
|
||||
2 => array('answer' => '3.14'));
|
||||
|
||||
|
@ -6,16 +6,18 @@ Feature: Allow students to redo questions in a practice quiz, without starting a
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student | Student | One | student@example.com |
|
||||
| teacher | Teacher | One | teacher@example.com |
|
||||
| username | firstname | lastname |
|
||||
| student | Student | One |
|
||||
| teacher | Teacher | One |
|
||||
| editor | Question | Editor |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student | C1 | student |
|
||||
| teacher | C1 | teacher |
|
||||
| user | course | role |
|
||||
| student | C1 | student |
|
||||
| teacher | C1 | teacher |
|
||||
| editor | C1 | editingteacher |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
@ -41,6 +43,39 @@ Feature: Allow students to redo questions in a practice quiz, without starting a
|
||||
Then the state of "First question" question is shown as "Not complete"
|
||||
And I should see "Marked out of 2.00" in the "First question" "question"
|
||||
|
||||
@javascript
|
||||
Scenario: Start attempt, teacher edits question, redo picks up latest non-draft version
|
||||
# Start attempt as student.
|
||||
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
|
||||
And I press "Attempt quiz"
|
||||
And I click on "False" "radio" in the "First question" "question"
|
||||
And I click on "Check" "button" in the "First question" "question"
|
||||
And I log out
|
||||
|
||||
# Now edit the question as teacher to add a real version and a draft version.
|
||||
# Would be nice to do this with a generator, but I don't have time right now.
|
||||
And I am on the "TF1" "core_question > edit" page logged in as "editor"
|
||||
And I set the following fields to these values:
|
||||
| Question name | TF1-v2 |
|
||||
| Question text | The new first question |
|
||||
| Correct answer | False |
|
||||
And I press "id_submitbutton"
|
||||
And I am on the "TF1-v2" "core_question > edit" page
|
||||
And I set the following fields to these values:
|
||||
| Question name | TF1-v3 |
|
||||
| Question text | This is only draft for now |
|
||||
| Correct answer | True |
|
||||
| Question status | Draft |
|
||||
And I press "id_submitbutton"
|
||||
And I log out
|
||||
|
||||
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
|
||||
And I press "Continue your attempt"
|
||||
And I press "Try another question like this one"
|
||||
Then the state of "The new first question" question is shown as "Not complete"
|
||||
And I should see "Marked out of 2.00" in the "The new first question" "question"
|
||||
And I should not see "This is only draft for now"
|
||||
|
||||
@javascript
|
||||
Scenario: The redo question button is visible but disabled for teachers
|
||||
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
|
||||
|
@ -53,40 +53,17 @@ class qbank_helper_test extends \advanced_testcase {
|
||||
$this->user = $USER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test is random.
|
||||
*
|
||||
* @covers ::is_random
|
||||
* @covers ::get_random_question_data_from_slot
|
||||
*/
|
||||
public function test_is_random() {
|
||||
$this->resetAfterTest();
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $context->id]);
|
||||
// Create the quiz object.
|
||||
$quizobj = \quiz::create($quiz->id);
|
||||
$structure = structure::create_for_quiz($quizobj);
|
||||
$slots = $structure->get_slots();
|
||||
foreach ($slots as $slot) {
|
||||
$this->assertEquals(true, qbank_helper::is_random($slot->id));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test reference records.
|
||||
*
|
||||
* @covers ::get_version_options
|
||||
* @covers ::get_question_for_redo
|
||||
*/
|
||||
public function test_reference_records() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$quiz = $this->create_test_quiz($this->course);
|
||||
// Test for questions from a different context.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||
$context = \context_module::instance($quiz->cmid);
|
||||
|
||||
// Create a couple of questions.
|
||||
/** @var \core_question_generator $questiongenerator */
|
||||
@ -110,11 +87,8 @@ class qbank_helper_test extends \advanced_testcase {
|
||||
$slots = $structure->get_slots();
|
||||
$slot = reset($slots);
|
||||
$this->assertEquals(3, count(qbank_helper::get_version_options($question->id)));
|
||||
$quizobj->preload_questions();
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$question = reset($questions);
|
||||
$this->assertEquals($question->id, qbank_helper::get_question_for_redo($slot->id));
|
||||
$this->assertEquals($question->id, qbank_helper::choose_question_for_redo(
|
||||
$quiz->id, $context, $slot->id, new \qubaid_list([])));
|
||||
|
||||
// Create another version.
|
||||
$questiongenerator->update_question($numq, null, ['name' => 'This is the latest version']);
|
||||
@ -125,7 +99,8 @@ class qbank_helper_test extends \advanced_testcase {
|
||||
$quizobj->load_questions();
|
||||
$questions = $quizobj->get_questions();
|
||||
$question = reset($questions);
|
||||
$this->assertEquals($question->id, qbank_helper::get_question_for_redo($slot->id));
|
||||
$this->assertEquals($question->id, qbank_helper::choose_question_for_redo(
|
||||
$quiz->id, $context, $slot->id, new \qubaid_list([])));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -82,7 +82,7 @@ class quiz_question_restore_test extends \advanced_testcase {
|
||||
$this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure(
|
||||
$quiz->id, $oldquizcontext)));
|
||||
|
||||
/// Restore the course.
|
||||
// Restore the course.
|
||||
$newcourse = $this->getDataGenerator()->create_course();
|
||||
$this->restore_quiz($backupid, $newcourse, $this->user);
|
||||
|
||||
@ -120,7 +120,7 @@ class quiz_question_restore_test extends \advanced_testcase {
|
||||
$this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure(
|
||||
$quiz->id, $quizcontext)));
|
||||
|
||||
/// Restore the course.
|
||||
// Restore the course.
|
||||
$newcourse = $this->getDataGenerator()->create_course();
|
||||
$this->restore_quiz($backupid, $newcourse, $this->user);
|
||||
|
||||
|
@ -129,7 +129,7 @@ class core_question_generator extends component_generator_base {
|
||||
$fromform = test_question_maker::get_question_form_data($qtype, $which);
|
||||
$fromform = (object) $this->datagenerator->combine_defaults_and_record((array) $question, $fromform);
|
||||
$fromform = (object) $this->datagenerator->combine_defaults_and_record((array) $fromform, $overrides);
|
||||
$fromform->status = $question->status;
|
||||
$fromform->status = $fromform->status ?? $question->status;
|
||||
|
||||
$question = question_bank::get_qtype($qtype)->save_question($question, $fromform);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user