From c093cb3263f0e6a452cbaabfec2b69689820a5bb Mon Sep 17 00:00:00 2001 From: Khoa Nguyen Dang Date: Mon, 12 Sep 2022 15:17:54 +0700 Subject: [PATCH] MDL-75642 quiz: Delete multiple (random) questions from quiz --- mod/quiz/attemptlib.php | 2 +- mod/quiz/classes/output/edit_renderer.php | 2 +- mod/quiz/classes/structure.php | 19 ++++++ mod/quiz/edit_rest.php | 9 ++- mod/quiz/locallib.php | 5 ++ .../editing_remove_multiple_questions.feature | 58 +++++++++++++++++++ mod/quiz/tests/structure_test.php | 35 +++++++++++ mod/quiz/upgrade.txt | 5 ++ 8 files changed, 132 insertions(+), 3 deletions(-) diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 1c1aabb0375..3639cc8cd7f 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -271,7 +271,7 @@ class quiz { /** * Get the quiz context. * - * @return context the module context for this quiz. + * @return context_module the module context for this quiz. */ public function get_context() { return $this->context; diff --git a/mod/quiz/classes/output/edit_renderer.php b/mod/quiz/classes/output/edit_renderer.php index e9d7ec53816..a528648d4fe 100644 --- a/mod/quiz/classes/output/edit_renderer.php +++ b/mod/quiz/classes/output/edit_renderer.php @@ -823,7 +823,7 @@ class edit_renderer extends \plugin_renderer_base { $structure->get_question_in_slot($slot), null, null, $qtype); } - if ($structure->can_be_edited()) { + if ($structure->can_be_edited() && $structure->has_use_capability($slot)) { $questionicons .= $this->question_remove_icon($structure, $slot, $pageurl); } $questionicons .= $this->marked_out_of_field($structure, $slot); diff --git a/mod/quiz/classes/structure.php b/mod/quiz/classes/structure.php index a5f7cde24d4..f6f0b309e2d 100644 --- a/mod/quiz/classes/structure.php +++ b/mod/quiz/classes/structure.php @@ -244,6 +244,25 @@ class structure { return $this->get_question_in_slot($slotnumber)->length != 0; } + /** + * Does the current user have '...use' capability over the question(s) in a given slot? + * + * + * @param int $slotnumber the index of the slot in question. + * @return bool true if they have the required capability. + */ + public function has_use_capability(int $slotnumber): bool { + $slot = $this->slotsinorder[$slotnumber]; + if (is_numeric($slot->questionid)) { + // Non-random question. + return question_has_capability_on($this->get_question_by_id($slot->questionid), 'use'); + } else { + // Random question. + $context = \context::instance_by_id($slot->contextid); + return has_capability('moodle/question:useall', $context); + } + } + /** * Get the course id that the quiz belongs to. * @return int the course.id for the quiz. diff --git a/mod/quiz/edit_rest.php b/mod/quiz/edit_rest.php index f06ca37f1c3..4d20fde23a7 100644 --- a/mod/quiz/edit_rest.php +++ b/mod/quiz/edit_rest.php @@ -156,7 +156,7 @@ switch($requestmethod) { foreach ($ids as $id) { $slot = $DB->get_record('quiz_slots', array('quizid' => $quiz->id, 'id' => $id), '*', MUST_EXIST); - if (quiz_has_question_use($quiz, $slot->slot)) { + if ($structure->has_use_capability($slot->slot)) { $structure->remove_slot($slot->slot); } } @@ -192,6 +192,13 @@ switch($requestmethod) { if (!$slot = $DB->get_record('quiz_slots', array('quizid' => $quiz->id, 'id' => $id))) { throw new moodle_exception('AJAX commands.php: Bad slot ID '.$id); } + + if (!$structure->has_use_capability($slot->slot)) { + $slotdetail = $structure->get_slot_by_id($slot->id); + $context = context::instance_by_id($slotdetail->contextid); + throw new required_capability_exception($context, + 'moodle/question:useall', 'nopermissions', ''); + } $structure->remove_slot($slot->slot); quiz_delete_previews($quiz); quiz_update_sumgrades($quiz); diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 2cc66d3bfdb..087492ab0fa 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -2266,6 +2266,9 @@ function quiz_require_question_use($questionid) { /** * Verify that the question exists, and the user has permission to use it. + * + * @deprecated in 4.1 use mod_quiz\structure::has_use_capability(...) instead. + * * @param object $quiz the quiz settings. * @param int $slot which question in the quiz to test. * @return bool whether the user can use this question. @@ -2273,6 +2276,8 @@ function quiz_require_question_use($questionid) { function quiz_has_question_use($quiz, $slot) { global $DB; + debugging('Deprecated. Please use mod_quiz\structure::has_use_capability instead.'); + $sql = 'SELECT q.* FROM {quiz_slots} slot JOIN {question_references} qre ON qre.itemid = slot.id diff --git a/mod/quiz/tests/behat/editing_remove_multiple_questions.feature b/mod/quiz/tests/behat/editing_remove_multiple_questions.feature index 8c402ac685b..fc3d913c79b 100644 --- a/mod/quiz/tests/behat/editing_remove_multiple_questions.feature +++ b/mod/quiz/tests/behat/editing_remove_multiple_questions.feature @@ -236,3 +236,61 @@ Feature: Edit quiz page - remove multiple questions And I click on "Delete selected" "button" Then I should see "Cannot remove questions" + + @javascript + Scenario: Delete multiple random questions from sections. + Given the following "questions" exist: + | questioncategory | qtype | name | questiontext | + | Test questions | truefalse | Question A | First question | + | Test questions | truefalse | Question B | Second question | + | Test questions | truefalse | Question C | Third question | + | Test questions | truefalse | Question D | Fourth question | + | Test questions | truefalse | Question E | Fifth question | + | Test questions | truefalse | Question F | Sixth question | + And I am on the "Quiz 1" "mod_quiz > Edit" page + + When I open the "last" add to quiz menu + And I follow "a random question" + And I set the field "Number of random questions" to "3" + And I press "Add random question" + And I click on "Select multiple items" "button" + And I click on "selectquestion-1" "checkbox" + And I click on "selectquestion-2" "checkbox" + And I click on "Delete selected" "button" + And I click on "Yes" "button" in the "Confirm" "dialogue" + # To make sure question is deleted completely. + And I reload the page + Then I should see "Random (Test questions)" on quiz page "1" + And I should not see "Random (Test questions)" on quiz page "2" + And I should not see "Random (Test questions)" on quiz page "3" + And I should see "Total of marks: 1.00" + And I should see "Questions: 1" + + @javascript + Scenario: Delete all random questions by checking select all. + Given the following "questions" exist: + | questioncategory | qtype | name | questiontext | + | Test questions | truefalse | Question A | First question | + | Test questions | truefalse | Question B | Second question | + | Test questions | truefalse | Question C | Third question | + | Test questions | truefalse | Question D | Fourth question | + | Test questions | truefalse | Question E | Fifth question | + | Test questions | truefalse | Question F | Sixth question | + And I am on the "Quiz 1" "mod_quiz > Edit" page + + # Delete all questions in page. Page contains multiple questions. + When I open the "last" add to quiz menu + And I follow "a random question" + And I set the field "Number of random questions" to "3" + And I press "Add random question" + And I click on "Select multiple items" "button" + And I press "Select all" + And I click on "Delete selected" "button" + And I click on "Yes" "button" in the "Confirm" "dialogue" + # To make sure question is deleted completely. + And I reload the page + Then I should not see "Random (Test questions)" on quiz page "1" + And I should not see "Random (Test questions)" on quiz page "2" + And I should not see "Random (Test questions)" on quiz page "3" + And I should see "Total of marks: 0.00" + And I should see "Questions: 0" diff --git a/mod/quiz/tests/structure_test.php b/mod/quiz/tests/structure_test.php index 137ba7fbe7b..5a1c83d28ab 100644 --- a/mod/quiz/tests/structure_test.php +++ b/mod/quiz/tests/structure_test.php @@ -958,4 +958,39 @@ class structure_test extends \advanced_testcase { $this->assertFalse($versiondata[2]->selected); $this->assertFalse($versiondata[3]->selected); } + + /** + * Test the current user have '...use' capability over the question(s) in a given slot. + * + * @covers ::has_use_capability + */ + public function test_has_use_capability() { + $this->resetAfterTest(); + + // Create a quiz with question. + $quizobj = $this->create_test_quiz([]); + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(['contextid' => $quizobj->get_context()->id]); + $q = $questiongenerator->create_question('essay', null, + ['category' => $cat->id, 'name' => 'This is essay question']); + quiz_add_quiz_question($q->id, $quizobj->get_quiz()); + + // Create the quiz object. + $structure = structure::create_for_quiz($quizobj); + $slots = $structure->get_slots(); + + // Get slot. + $slotid = array_pop($slots)->slot; + + $course = $quizobj->get_course(); + $generator = $this->getDataGenerator(); + $teacher = $generator->create_and_enrol($course, 'editingteacher'); + $student = $generator->create_and_enrol($course); + + $this->setUser($teacher); + $this->assertTrue($structure->has_use_capability($slotid)); + + $this->setUser($student); + $this->assertFalse($structure->has_use_capability($slotid)); + } } diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 24afe9e9808..ac3a5e6cdac 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -1,5 +1,10 @@ This files describes API changes in the quiz code. +=== 4.1 === + +* quiz_has_question_use is now deprecated. Use mod_quiz\structure::has_use_capability istead. + + === 4.0.3, 4.1 === * If a quiz is sequential the external functions mod_quiz_external::get_attempt_data, mod_quiz_external::get_attempt_summary will not return any data