MDL-72890 core_question : Question regrade for question versions

This commit implements a question regrade for the selected
version in the quiz for a slot.

Co-Authored-By: Safat Shahin <[email protected]>
Co-Authored-By: Tim Hunt <[email protected]>
This commit is contained in:
Marc-Alexandre Ghaly
2022-03-04 23:06:50 +11:00
committed by Safat Shahin
co-authored by Safat Shahin Tim Hunt
parent 9cd77c4130
commit 3e09319e7a
7 changed files with 286 additions and 35 deletions
@@ -56,7 +56,7 @@ class qbank_helper {
*/
public static function get_version_options($questionid): array {
global $DB;
$sql = "SELECT qv.id AS versionid, qv.version
$sql = "SELECT qv.id AS versionid, qv.version, qv.questionid
FROM {question_versions} qv
WHERE qv.questionbankentryid = (SELECT DISTINCT qbe.id
FROM {question_bank_entries} qbe
+1 -1
View File
@@ -51,7 +51,7 @@ abstract class quiz_attempts_report extends quiz_default_report {
/** @var string the mode this report is. */
protected $mode;
/** @var object the quiz context. */
/** @var context_module the quiz context. */
protected $context;
/** @var mod_quiz_attempts_report_form The settings form to use. */
+69 -2
View File
@@ -44,6 +44,20 @@ 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.
*/
protected $slotquestionids = null;
/**
* @var array|null used during regrades, to cache which new questionid to use for each old on.
* for random questions, stores oldquestionid => newquestionid.
* See get_new_question_for_regrade.
*/
protected $newquestionidsforold = null;
public function display($quiz, $cm, $course) {
global $DB, $OUTPUT, $PAGE;
@@ -323,7 +337,7 @@ class quiz_overview_report extends quiz_attempts_report {
* @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.
* the questions with those slots.
*/
protected function regrade_attempt($attempt, $dryrun = false, $slots = null) {
global $DB;
@@ -342,8 +356,9 @@ class quiz_overview_report extends quiz_attempts_report {
foreach ($slots as $slot) {
$qqr = new stdClass();
$qqr->oldfraction = $quba->get_question_fraction($slot);
$otherquestionversion = $this->get_new_question_for_regrade($attempt, $quba, $slot);
$quba->regrade_question($slot, $finished);
$quba->regrade_question($slot, $finished, null, $otherquestionversion);
$qqr->newfraction = $quba->get_question_fraction($slot);
@@ -379,6 +394,58 @@ class quiz_overview_report extends quiz_attempts_report {
gc_collect_cycles();
}
/**
* Work out of we should be using a new question version for a particular slot in a regrade.
*
* @param stdClass $attempt the attempt being regraded.
* @param question_usage_by_activity $quba the question_usage corresponding to that.
* @param int $slot which slot is currently being regraded.
* @return question_definition other question version to use for this slot.
*/
protected function get_new_question_for_regrade(stdClass $attempt,
question_usage_by_activity $quba, int $slot): question_definition {
global $DB;
// If the cache is empty, get information about all the slots.
if ($this->slotquestionids === 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]);
}
// If this is a non-random slot, we will have the right info cached.
if (array_key_exists($slot, $this->slotquestionids)) {
// This is a non-random slot.
return question_bank::load_question($this->slotquestionids[$slot]);
}
// 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)) {
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);
$latestversion = reset($versionsoptions);
$this->newquestionidsforold[$currentquestion->id] = $latestversion->questionid;
return question_bank::load_question($latestversion->questionid);
}
/**
* Regrade attempts for this quiz, exactly which attempts are regraded is
* controlled by the parameters.
@@ -10,6 +10,7 @@ Feature: Regrading quiz attempts using the Grades report
| teacher | Mark | Allwright |
| student1 | Student | One |
| student2 | Student | Two |
| student3 | Student | Three |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
@@ -18,6 +19,7 @@ Feature: Regrading quiz attempts using the Grades report
| teacher | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
@@ -73,38 +75,151 @@ Feature: Regrading quiz attempts using the Grades report
And I should see "Quiz for testing regrading"
And I should see "Overall number of students achieving grade ranges"
# @todo MDL-72890 uncomment this scenario and add some more for re-grading.
# Scenario: Dry-run a full regrade, then regrade the attempts that will need it.
# Given I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
# When I navigate to "Edit quiz" in current page administration
# And I follow "Edit question SA"
# And I set the field "id_fraction_1" to "50%"
# And I press "id_submitbutton"
# And I follow "Attempts: 2"
# And I press "Dry run a full regrade"
Scenario: Dry-run a full regrade, then regrade the attempts that will need it.
Given I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
When I navigate to "Questions" in current page administration
And I follow "Edit question SA"
And I set the field "id_fraction_1" to "50%"
And I press "id_submitbutton"
And I click on "v1" "option"
And I click on "v2" "option"
And I follow "Attempts: 2"
And I press "Dry run a full regrade"
# Note, the order is not defined, so we can only check part of the message.
# Then I should see "Quiz for testing regrading"
# And I should see "Successfully regraded (2/2)"
# And I should see "Regrade completed successfully"
# And I press "Continue"
Then I should see "Quiz for testing regrading"
And I should see "Successfully regraded (2/2)"
And I should see "Regrade completed successfully"
And I press "Continue"
# And "Student One" row "Regrade" column of "attempts" table should not contain "Needed"
# And "Student TwoReview attempt" row "Regrade" column of "attempts" table should contain "Needed"
And "Student One" row "Regrade" column of "attempts" table should not contain "Needed"
And "Student TwoReview attempt" row "Regrade" column of "attempts" table should contain "Needed"
# In the following, the first number is strike-through, and the second is not, but Behat can't see that.
# At this point, it is showing what would change.
# And "Student TwoReview attempt" row "Q. 2/50.00Sort by Q. 2/50.00 Ascending" column of "attempts" table should contain "40.00/25.00"
# And "Student TwoReview attempt" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "attempts" table should contain "90.00/75.00"
# And I press "Regrade attempts marked as needing regrading (1)"
# And I should see "Quiz for testing regrading"
# And I should see "Successfully regraded (1/1)"
# And I should see "Regrade completed successfully"
# And I press "Continue"
And "Student TwoReview attempt" row "Q. 2/50.00Sort by Q. 2/50.00 Ascending" column of "attempts" table should contain "40.00/25.00"
And "Student TwoReview attempt" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "attempts" table should contain "90.00/75.00"
And I press "Regrade attempts marked as needing regrading (1)"
And I should see "Quiz for testing regrading"
And I should see "Successfully regraded (1/1)"
And I should see "Regrade completed successfully"
And I press "Continue"
# These next tests just serve to check we got back to the report.
# And I should see "Quiz for testing regrading"
# And I should see "Overall number of students achieving grade ranges"
And I should see "Quiz for testing regrading"
And I should see "Overall number of students achieving grade ranges"
# Now, both old-score strike-through and new score plain, are still shown, but now it indicates what did change.
# And "Student TwoReview attempt" row "Q. 2/50.00Sort by Q. 2/50.00 Ascending" column of "attempts" table should contain "40.00/25.00"
# And "Student TwoReview attempt" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "attempts" table should contain "90.00/75.00"
# And "Regrade attempts marked as needing regrading" "button" should not exist
And "Student TwoReview attempt" row "Q. 2/50.00Sort by Q. 2/50.00 Ascending" column of "attempts" table should contain "40.00/25.00"
And "Student TwoReview attempt" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "attempts" table should contain "90.00/75.00"
And "Regrade attempts marked as needing regrading" "button" should not exist
Scenario: Regrade all attempts works against quiz selected question version
Given I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
And I navigate to "Results" in current page administration
When I press "Dry run a full regrade"
Then I should see "Quiz for testing regrading"
And I should see "Successfully regraded (2/2)"
And I should see "Regrade completed successfully"
And I press "Continue"
And I should see "Quiz for testing regrading"
And I should see "Overall number of students achieving grade ranges"
And "Student One" row "Regrade" column of "attempts" table should not contain "Needed"
And I navigate to "Question bank" in current page administration
And I choose "Edit question" action for "TF" in the question bank
And I set the field "Correct answer" to "False"
And I press "id_submitbutton"
And I navigate to "Questions" in current page administration
And I click on "version" "select" in the "TF" "list_item"
And I click on "v2 (latest)" "option"
And I navigate to "Results" in current page administration
And I press "Dry run a full regrade"
And I should see "Regrade completed successfully"
And I press "Continue"
And "student1@example.com" row "Regrade" column of "attempts" table should contain "Needed"
And "Correct" "icon" should appear before "50.00/0.00" "text"
And I press "Regrade all"
And I should see "Regrade completed successfully"
And I press "Continue"
Then "student1@example.com" row "Regrade" column of "attempts" table should contain "Done"
And "Student OneReview attempt" row "Q. 1/50.00Sort by Q. 1/50.00 Ascending" column of "attempts" table should contain "50.00/0.00"
And "Incorrect" "icon" should appear before "50.00/0.00" "text"
Scenario: Regrade all attempts works against quiz selected latest question version
Given I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
And I navigate to "Results" in current page administration
And I click on "mod-quiz-report-overview-report-selectall-attempts" "checkbox"
And I click on "Delete selected attempts" "button"
And I click on "Yes" "button"
And I navigate to "Questions" in current page administration
And I should see "(latest)" in the "TF" "list_item"
# Create multiple question versions.
And I navigate to "Question bank" in current page administration
And I choose "Edit question" action for "TF" in the question bank
And I set the field "Correct answer" to "True"
And I press "id_submitbutton"
And I choose "Edit question" action for "TF" in the question bank
And I set the field "Question name" to "New version of TF"
And I set the field "Correct answer" to "False"
And I press "id_submitbutton"
And I navigate to "Questions" in current page administration
And I should see "(latest)" in the "TF" "list_item"
And I click on "version" "select" in the "TF" "list_item"
And I should see "v1"
And I should see "v2"
And I should see "v3 (latest)"
# Set version that is going to be attempted to an older one.
And I click on "v1" "option"
And user "student3" has attempted "Quiz for testing regrading" with responses:
| slot | response |
| 1 | True |
| 2 | toad |
And I navigate to "Questions" in current page administration
And I click on "version" "select" in the "TF" "list_item"
And I click on "Always latest" "option"
And I navigate to "Results" in current page administration
And I press "Regrade all"
And I should see "Successfully regraded (1/1)"
And I should see "Regrade completed successfully"
And I press "Continue"
Then "student3@example.com" row "Q. 1/50.00Sort by Q. 1/50.00 Ascending" column of "attempts" table should contain "50.00/0.00"
And "Incorrect" "icon" should appear before "50.00/0.00" "text"
Scenario: Regrade attempts should always regrade against latest random question version
Given I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
And I navigate to "Results" in current page administration
And I click on "mod-quiz-report-overview-report-selectall-attempts" "checkbox"
And I click on "Delete selected attempts" "button"
And I click on "Yes" "button"
# Create multiple question versions.
And I navigate to "Question bank" in current page administration
And I choose "Delete" action for "SA" in the question bank
And I press "Delete"
And I navigate to "Questions" in current page administration
And I click on "Delete" "link" in the "TF" "list_item"
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I click on "Delete" "link" in the "SA" "list_item"
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I click on "Add" "link"
And I follow "a random question"
And I set the field "Number of random questions" to "1"
And I press "Add random question"
And I am on the "Quiz for testing regrading" "quiz activity" page logged in as student3
And I click on "Attempt quiz" "button"
And I should see "The answer is true."
And I set the field "True" to "1"
And I click on "Finish attempt ..." "button"
And I press "Submit all and finish"
And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
And I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
And I navigate to "Question bank" in current page administration
And I choose "Edit question" action for "TF" in the question bank
And I set the field "Correct answer" to "False"
And I press "id_submitbutton"
And I navigate to "Results" in current page administration
And "student3@example.com" row "Q. 1/100.00Sort by Q. 1/100.00 Ascending" column of "attempts" table should contain "100.00"
And "Correct" "icon" should be visible
And I press "Regrade all"
And I should see "Successfully regraded (1/1)"
And I should see "Regrade completed successfully"
And I press "Continue"
Then "student3@example.com" row "Q. 1/100.00Sort by Q. 1/100.00 Ascending" column of "attempts" table should contain "100.00/0.00"
And "Incorrect" "icon" should be visible
@@ -22,6 +22,9 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_quiz\external\submit_question_version;
use mod_quiz\question\bank\qbank_helper;
defined('MOODLE_INTERNAL') || die();
global $CFG;
@@ -30,6 +33,7 @@ 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/tests/helpers.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
@@ -37,8 +41,10 @@ require_once($CFG->dirroot . '/mod/quiz/report/overview/tests/helpers.php');
*
* @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;
/**
* Data provider for test_report_sql.
@@ -314,4 +320,59 @@ class quiz_overview_report_testcase extends advanced_testcase {
$quizattemptsreport->delete_selected_attempts($quiz, $cm, [$attempt->id], $allowedjoins);
}
/**
* Test question regrade for selected versions.
*
* @covers ::regrade_question
*/
public function test_regrade_question() {
$this->resetAfterTest();
$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);
$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);
$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());
// 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());
}
}
+1
View File
@@ -938,6 +938,7 @@ ORDER BY
$record = new stdClass();
$record->id = $qa->get_database_id();
$record->slot = $qa->get_slot();
$record->questionid = $qa->get_question(false)->id;
$record->variant = $qa->get_variant();
$record->maxmark = $qa->get_max_mark();
$record->minfraction = $qa->get_min_fraction();
+11 -4
View File
@@ -882,15 +882,22 @@ class question_usage_by_activity {
* @param bool $finished whether the question attempt should be forced to be finished
* after the regrade, or whether it may still be in progress (default false).
* @param number $newmaxmark (optional) if given, will change the max mark while regrading.
* @param question_definition|null $otherversion a different version of the question to use
* in the regrade. (By default, the regrode will use exactly the same question version.)
*/
public function regrade_question($slot, $finished = false, $newmaxmark = null) {
public function regrade_question($slot, $finished = false, $newmaxmark = null,
question_definition $otherversion = null) {
$oldqa = $this->get_question_attempt($slot);
if ($otherversion &&
$otherversion->questionbankentryid !== $oldqa->get_question(false)->questionbankentryid) {
throw new coding_exception('You can only regrade using a different version of the same question, ' .
'not a completely different question.');
}
if (is_null($newmaxmark)) {
$newmaxmark = $oldqa->get_max_mark();
}
$newqa = new question_attempt($oldqa->get_question(false), $oldqa->get_usage_id(),
$this->observer, $newmaxmark);
$newqa = new question_attempt($otherversion ?? $oldqa->get_question(false),
$oldqa->get_usage_id(), $this->observer, $newmaxmark);
$newqa->set_database_id($oldqa->get_database_id());
$newqa->set_slot($oldqa->get_slot());
$newqa->regrade($oldqa, $finished);