MDL-83784 mod_quiz: Fix quiz unusable if question category missing.

Random questions set to question categories which no longer exist
was throwing an error making it impossible to use or edit the quiz
to fix it. This will now allow the user to view the questions and
edit the quiz in order to fix the problem of the missing category.
This commit is contained in:
Conn Warwicker 2025-01-31 10:53:41 +00:00
parent 139a0ad5f0
commit 2499276c65
No known key found for this signature in database
GPG key ID: DA5D0AD62F47BE87
9 changed files with 117 additions and 3 deletions

View file

@ -0,0 +1,8 @@
issueNumber: MDL-83784
notes:
core_question:
- message: >-
Question bank Condition classes can now implement a function called
"filter_invalid_values($filterconditions)" to remove anything from the
filterconditions array which is invalid or should not be there.
type: improved

View file

@ -1096,6 +1096,13 @@ class edit_renderer extends \plugin_renderer_base {
$temp->questiontext = '';
$temp->name = $structure->describe_random_slot($slot->id);
$instancename = quiz_question_tostring($temp);
if (strpos($instancename, structure::MISSING_QUESTION_CATEGORY_PLACEHOLDER) !== false) {
$label = html_writer::span(
get_string('missingcategory', 'mod_quiz'),
'badge bg-danger text-white h-50'
);
$instancename = str_replace(structure::MISSING_QUESTION_CATEGORY_PLACEHOLDER, $label, $instancename);
}
$configuretitle = get_string('configurerandomquestion', 'quiz');
$qtype = \question_bank::get_qtype($question->qtype, false);

View file

@ -44,6 +44,12 @@ use stdClass;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class structure {
/**
* Placeholder string used when a question category is missing.
*/
const MISSING_QUESTION_CATEGORY_PLACEHOLDER = 'missing_question_category';
/** @var quiz_settings the quiz this is the structure of. */
protected $quizobj = null;
@ -1801,6 +1807,10 @@ class structure {
// Now, put the data required for each slot into $this->randomslotcategories and $this->randomslottags.
foreach ($randomcategoriesandtags as $slotid => $catandtags) {
$qcategoryid = $catandtags['cat']['values'];
if (!array_key_exists($qcategoryid, $categories)) {
$this->randomslotcategories[$slotid] = self::MISSING_QUESTION_CATEGORY_PLACEHOLDER;
continue;
}
$qcategory = $categories[$qcategoryid];
$includesubcategories = $catandtags['cat']['includesubcategories'];
$this->randomslotcategories[$slotid] = $this->get_used_category_description($qcategory, $includesubcategories);
@ -1811,6 +1821,7 @@ class structure {
}
$this->randomslottags[$slotid] = implode(', ', $slottagnames);
}
}
}

View file

@ -23,6 +23,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_question\local\bank\filter_condition_manager;
use core_question\question_reference_manager;
use mod_quiz\quiz_settings;
use mod_quiz\question\bank\random_question_view;
@ -59,6 +60,7 @@ $setreference = $DB->get_record('question_set_references',
['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
$filterconditions = json_decode($setreference->filtercondition, true);
$filterconditions = question_reference_manager::convert_legacy_set_reference_filter_condition($filterconditions);
$filterconditions = filter_condition_manager::filter_invalid_values($filterconditions);
$params = $filterconditions;
$params['cmid'] = $cm->id;
@ -135,9 +137,10 @@ if ($mform->is_cancelled()) {
redirect($returnurl);
}
$PAGE->set_title('Random question');
$heading = get_string('randomediting', 'mod_quiz');
$PAGE->set_title($heading);
$PAGE->set_heading($COURSE->fullname);
$PAGE->navbar->add('Random question');
$PAGE->navbar->add($heading);
// Custom View.
$questionbank = new random_question_view($contexts, $thispageurl, $course, $cm, $params, $extraparams);
@ -154,7 +157,6 @@ $PAGE->requires->js_call_amd('mod_quiz/update_random_question_filter_condition',
// Display a heading, question editing form.
echo $OUTPUT->header();
$heading = get_string('randomediting', 'mod_quiz');
echo $OUTPUT->heading_with_help($heading, 'randomquestion', 'mod_quiz');
echo $updateform;
echo $OUTPUT->footer();

View file

@ -556,6 +556,7 @@ $string['maxmarks_help'] = 'The maximum mark available for each question.';
$string['min'] = 'Min';
$string['minutes'] = 'Minutes';
$string['missingcategory'] = 'Missing question category';
$string['missingcorrectanswer'] = 'Correct answer must be specified';
$string['missingitemtypename'] = 'Missing name';
$string['missingquestion'] = 'This question no longer seems to exist';

View file

@ -82,3 +82,22 @@ Feature: Moving a question to another category should not affect random question
And I should see "I was edited" in the "Used category new" "list_item"
And I am on the "Quiz 1" "mod_quiz > Edit" page
And I should see "Random (Used category new) based on filter condition" on quiz page "1"
@javascript
Scenario: A random question with an invalid category should still be editable
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
Then I click on "Switch bank" "button"
And I click on "Qbank 1" "link" in the "Select question bank" "dialogue"
And I apply question bank filter "Category" with value "Used category"
And I press "Add random question"
And I should see "Random (Used category) based on filter condition" on quiz page "1"
And I am on the "Qbank 1" "core_question > question categories" page
And I open the action menu in "Used category" "list_item"
And I choose "Delete" in the open action menu
And I click on "Delete" "button" in the "Delete" "dialogue"
And I press "Save in category"
And I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
Then I should not see "Random (Used category) based on filter condition" on quiz page "1"
And I should see "Missing question category" on quiz page "1"

View file

@ -342,4 +342,35 @@ class category_condition extends condition {
public function is_required(): bool {
return true;
}
#[\Override]
public function filter_invalid_values(array $filterconditions): array {
global $DB;
$defaultcatid = explode(',', $filterconditions['cat'])[0];
[$insql, $inparams] = $DB->get_in_or_equal($filterconditions['filter']['category']['values']);
$categories = $DB->get_records_select('question_categories', "id {$insql}",
$inparams, null, 'id');
$categoryids = array_keys($categories);
foreach ($filterconditions['filter']['category']['values'] as $key => $catid) {
// Check that the category still exists, and if not, remove it from the conditions.
if (!in_array($catid, $categoryids)) {
unset($filterconditions['filter']['category']['values'][$key]);
}
}
// If we now don't have any valid categories, use the default loaded from the page.
if (count($filterconditions['filter']['category']['values']) === 0) {
$filterconditions['filter']['category']['values'] = [$defaultcatid];
}
return $filterconditions;
}
}

View file

@ -205,6 +205,21 @@ abstract class condition {
];
}
/**
* Method to be overridden in condition classes to filter out anything invalid from the filterconditions array.
*
* This can be applied anywhere where the $filterconditions array exists, to let condition plugins remove elements
* from the array, based on their own internal logic/validation. For example, this is used on the
* /mod/quiz/editrandom.php page to filter out question categories which no longer exist, which previously
* broke the editrandom page.
*
* @param array $filterconditions
* @return array
*/
public function filter_invalid_values(array $filterconditions): array {
return $filterconditions;
}
/**
* Given an array of filters, pick the entry that matches the condition key and return it.
*

View file

@ -147,4 +147,24 @@ class filter_condition_manager {
return $filter;
}
/**
* Filter out invalid values from the filterconditions array,
*
* @param array $filterconditions
* @return array
* @throws \dml_exception
*/
public static function filter_invalid_values(array $filterconditions): array {
$classes = self::get_condition_classes();
foreach ($classes as $class) {
$condition = new $class();
$filterconditions = $condition->filter_invalid_values($filterconditions);
}
return $filterconditions;
}
}