From 17c3e8ecc80630f666b697bf6cf80395770add17 Mon Sep 17 00:00:00 2001 From: Conn Warwicker Date: Fri, 31 Jan 2025 10:53:41 +0000 Subject: [PATCH] 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. --- mod/quiz/classes/output/edit_renderer.php | 7 +++++ mod/quiz/classes/structure.php | 13 ++++++++ mod/quiz/editrandom.php | 8 +++-- mod/quiz/lang/en/quiz.php | 1 + mod/quiz/tests/behat/random_question.feature | 15 +++++++++ .../classes/category_condition.php | 31 +++++++++++++++++++ question/classes/local/bank/condition.php | 15 +++++++++ .../local/bank/filter_condition_manager.php | 20 ++++++++++++ 8 files changed, 107 insertions(+), 3 deletions(-) diff --git a/mod/quiz/classes/output/edit_renderer.php b/mod/quiz/classes/output/edit_renderer.php index 42bf01b3653..56a5cca68bf 100644 --- a/mod/quiz/classes/output/edit_renderer.php +++ b/mod/quiz/classes/output/edit_renderer.php @@ -1067,6 +1067,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); diff --git a/mod/quiz/classes/structure.php b/mod/quiz/classes/structure.php index 672db970864..6f4be4c4439 100644 --- a/mod/quiz/classes/structure.php +++ b/mod/quiz/classes/structure.php @@ -42,6 +42,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; @@ -1759,6 +1765,13 @@ 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 the category does not exist, replace with a temporary placeholder. + 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); diff --git a/mod/quiz/editrandom.php b/mod/quiz/editrandom.php index 416f172ef71..a0dfdc8fd44 100644 --- a/mod/quiz/editrandom.php +++ b/mod/quiz/editrandom.php @@ -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; @@ -58,6 +59,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; @@ -132,9 +134,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); @@ -151,7 +154,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(); diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index 5a7a1b51039..de617c2477d 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -555,6 +555,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'; diff --git a/mod/quiz/tests/behat/random_question.feature b/mod/quiz/tests/behat/random_question.feature index f6f53f1f120..cb78eb3b81f 100644 --- a/mod/quiz/tests/behat/random_question.feature +++ b/mod/quiz/tests/behat/random_question.feature @@ -71,3 +71,18 @@ 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" + 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 "Course 1" "core_question > course question categories" page + And I click on "Delete" "link" in the "Used category" "list_item" + 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" diff --git a/question/bank/managecategories/classes/category_condition.php b/question/bank/managecategories/classes/category_condition.php index 76ea895a741..c42483b88d8 100644 --- a/question/bank/managecategories/classes/category_condition.php +++ b/question/bank/managecategories/classes/category_condition.php @@ -331,4 +331,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; + + } + } diff --git a/question/classes/local/bank/condition.php b/question/classes/local/bank/condition.php index f1715adb60b..9363e3f4eff 100644 --- a/question/classes/local/bank/condition.php +++ b/question/classes/local/bank/condition.php @@ -200,6 +200,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. * diff --git a/question/classes/local/bank/filter_condition_manager.php b/question/classes/local/bank/filter_condition_manager.php index 7939feb7c34..6021c9c1154 100644 --- a/question/classes/local/bank/filter_condition_manager.php +++ b/question/classes/local/bank/filter_condition_manager.php @@ -117,4 +117,24 @@ class filter_condition_manager { } return $filters; } + + /** + * 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; + + } + }