diff --git a/lang/en/question.php b/lang/en/question.php
index 30a1fb45330..3b4ef630d4b 100644
--- a/lang/en/question.php
+++ b/lang/en/question.php
@@ -235,7 +235,7 @@ $string['movedquestionsandcategories'] = 'Moved questions and question categorie
$string['movelinksonly'] = 'Just change where links point to, do not move or copy files.';
$string['moveq'] = 'Move question(s)';
$string['moveqtoanothercontext'] = 'Move question to another context.';
-$string['moveto'] = 'Move to >>';
+$string['moveto'] = 'Move to';
$string['movingcategory'] = 'Moving category';
$string['movingcategoryandfiles'] = 'Are you sure you want to move category {$a->name} and all child categories to context for "{$a->contextto}"? We have detected {$a->urlcount} files linked from questions in {$a->fromareaname}, would you like to copy or move these to {$a->toareaname}?';
$string['movingcategorynofiles'] = 'Are you sure you want to move category "{$a->name}" and all child categories to context for "{$a->contextto}"?';
diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php
index 96b5dbe84d5..dd74d02af8d 100644
--- a/lib/classes/plugin_manager.php
+++ b/lib/classes/plugin_manager.php
@@ -1950,6 +1950,7 @@ class core_plugin_manager {
),
'qbank' => [
+ 'bulkmove',
'comment',
'deletequestion',
'editquestion',
diff --git a/mod/quiz/classes/question/bank/custom_view.php b/mod/quiz/classes/question/bank/custom_view.php
index 4f4f4036e3a..6e86dd37fca 100644
--- a/mod/quiz/classes/question/bank/custom_view.php
+++ b/mod/quiz/classes/question/bank/custom_view.php
@@ -142,7 +142,7 @@ class custom_view extends \core_question\local\bank\view {
return $out;
}
- protected function display_bottom_controls($totalnumber, $recurse, $category, \context $catcontext, array $addcontexts): void {
+ protected function display_bottom_controls(\context $catcontext): void {
$cmoptions = new \stdClass();
$cmoptions->hasattempts = !empty($this->quizhasattempts);
diff --git a/mod/quiz/edit.php b/mod/quiz/edit.php
index 4950902156e..49f71597ca0 100644
--- a/mod/quiz/edit.php
+++ b/mod/quiz/edit.php
@@ -171,7 +171,6 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
// Get the question bank view.
$questionbank = new mod_quiz\question\bank\custom_view($contexts, $thispageurl, $course, $cm, $quiz);
$questionbank->set_quiz_has_attempts($quizhasattempts);
-$questionbank->process_actions();
// End of process commands =====================================================.
diff --git a/question/bank/bulkmove/classes/bulk_move_action.php b/question/bank/bulkmove/classes/bulk_move_action.php
new file mode 100644
index 00000000000..60bbf1065d4
--- /dev/null
+++ b/question/bank/bulkmove/classes/bulk_move_action.php
@@ -0,0 +1,47 @@
+.
+
+namespace qbank_bulkmove;
+
+/**
+ * Class bulk_move_action is the base class for moving questions.
+ *
+ * @package qbank_bulkmove
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class bulk_move_action extends \core_question\local\bank\bulk_action_base {
+
+ public function get_bulk_action_title(): string {
+ return get_string('movetobulkaction', 'qbank_bulkmove');
+ }
+
+ public function get_bulk_action_key(): string {
+ return 'move';
+ }
+
+ public function get_bulk_action_url(): \moodle_url {
+ return new \moodle_url('/question/bank/bulkmove/move.php');
+ }
+
+ public function get_bulk_action_capabilities(): ?array {
+ return [
+ 'moodle/question:moveall',
+ 'moodle/question:add',
+ ];
+ }
+}
diff --git a/question/bank/bulkmove/classes/helper.php b/question/bank/bulkmove/classes/helper.php
new file mode 100644
index 00000000000..879ee906bcb
--- /dev/null
+++ b/question/bank/bulkmove/classes/helper.php
@@ -0,0 +1,90 @@
+.
+
+namespace qbank_bulkmove;
+
+/**
+ * Bulk move helper.
+ *
+ * @package qbank_bulkmove
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+
+ /**
+ * Bulk move questions to a category.
+ *
+ * @param string $movequestionselected comma separated string of questions to be moved.
+ * @param \stdClass $tocategory the category where the questions will be moved to.
+ */
+ public static function bulk_move_questions(string $movequestionselected, \stdClass $tocategory): void {
+ global $DB;
+ if ($questionids = explode(',', $movequestionselected)) {
+ list($usql, $params) = $DB->get_in_or_equal($questionids);
+ $sql = "SELECT q.*, c.contextid
+ FROM {question} q
+ JOIN {question_categories} c ON c.id = q.category
+ WHERE q.id
+ {$usql}";
+ $questions = $DB->get_records_sql($sql, $params);
+ foreach ($questions as $question) {
+ question_require_capability_on($question, 'move');
+ }
+ question_move_questions_to_category($questionids, $tocategory->id);
+ }
+ }
+
+ /**
+ * Get the display data for the move form.
+ *
+ * @param array $addcontexts the array of contexts to be considered in order to render the category select menu.
+ * @param \moodle_url $moveurl the url where the move script will point to.
+ * @param \moodle_url $returnurl return url in case the form is cancelled.
+ * @return array the data to be rendered in the mustache where it contains the dropdown, move url and return url.
+ */
+ public static function get_displaydata(array $addcontexts, \moodle_url $moveurl, \moodle_url $returnurl): array {
+ $displaydata = [];
+ $displaydata ['categorydropdown'] = \qbank_managecategories\helper::question_category_select_menu($addcontexts,
+ false, 0, '', -1, true);
+ $displaydata ['moveurl'] = $moveurl;
+ $displaydata['returnurl'] = $returnurl;
+ return $displaydata;
+ }
+
+ /**
+ * Process the question came from the form post.
+ *
+ * @param array $rawquestions raw questions came as a part of post.
+ * @return array question ids got from the post are processed and structured in an array.
+ */
+ public static function process_question_ids(array $rawquestions): array {
+ $questionids = [];
+ $questionlist = '';
+ foreach ($rawquestions as $key => $notused) {
+ // Parse input for question ids.
+ if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
+ $key = $matches[1];
+ $questionids[] = $key;
+ }
+ }
+ if (!empty($questionids)) {
+ $questionlist = implode(',', $questionids);
+ }
+ return [$questionids, $questionlist];
+ }
+}
diff --git a/question/bank/bulkmove/classes/output/renderer.php b/question/bank/bulkmove/classes/output/renderer.php
new file mode 100644
index 00000000000..b57a214da28
--- /dev/null
+++ b/question/bank/bulkmove/classes/output/renderer.php
@@ -0,0 +1,39 @@
+.
+
+namespace qbank_bulkmove\output;
+
+/**
+ * Class renderer.
+ *
+ * @package qbank_bulkmove
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends \plugin_renderer_base {
+
+ /**
+ * Render bulk move.
+ *
+ * @param array $displaydata
+ * @return string
+ */
+ public function render_bulk_move_form($displaydata) {
+ return $this->render_from_template('qbank_bulkmove/bulk_move', $displaydata);
+ }
+
+}
diff --git a/question/bank/bulkmove/classes/plugin_feature.php b/question/bank/bulkmove/classes/plugin_feature.php
new file mode 100644
index 00000000000..4c9eda185f3
--- /dev/null
+++ b/question/bank/bulkmove/classes/plugin_feature.php
@@ -0,0 +1,34 @@
+.
+
+namespace qbank_bulkmove;
+
+use core_question\local\bank\bulk_action_base;
+use core_question\local\bank\plugin_features_base;
+
+/**
+ * Class plugin_feature is the entrypoint for the features.
+ *
+ * @package qbank_bulkmove
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class plugin_feature extends plugin_features_base {
+ public function get_bulk_actions(): ?bulk_action_base {
+ return new bulk_move_action();
+ }
+}
diff --git a/question/bank/bulkmove/classes/privacy/provider.php b/question/bank/bulkmove/classes/privacy/provider.php
new file mode 100644
index 00000000000..e17cec73b85
--- /dev/null
+++ b/question/bank/bulkmove/classes/privacy/provider.php
@@ -0,0 +1,32 @@
+.
+
+namespace qbank_bulkmove\privacy;
+
+/**
+ * Privacy Subsystem for qbank_deletequestion implementing null_provider.
+ *
+ * @package qbank_bulkmove
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+ public static function get_reason(): string {
+ return 'privacy:metadata';
+ }
+}
diff --git a/question/bank/bulkmove/lang/en/qbank_bulkmove.php b/question/bank/bulkmove/lang/en/qbank_bulkmove.php
new file mode 100644
index 00000000000..a99ad3271d6
--- /dev/null
+++ b/question/bank/bulkmove/lang/en/qbank_bulkmove.php
@@ -0,0 +1,31 @@
+.
+
+/**
+ * Strings for component qbank_bulkmove, language 'en'
+ *
+ * @package qbank_bulkmove
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['bulkmoveheader'] = 'Move the selected questions';
+$string['close'] = 'Close';
+$string['movequestions'] = 'Move questions';
+$string['movetobulkaction'] = 'Move to...';
+$string['pluginname'] = 'Bulk move questions';
+$string['privacy:metadata'] = 'The bulk move questions plugin does not store any personal data.';
diff --git a/question/bank/bulkmove/move.php b/question/bank/bulkmove/move.php
new file mode 100644
index 00000000000..26f97558144
--- /dev/null
+++ b/question/bank/bulkmove/move.php
@@ -0,0 +1,103 @@
+.
+
+/**
+ * Move questions page.
+ *
+ * @package qbank_bulkmove
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+require_once(__DIR__ . '/../../editlib.php');
+global $DB, $OUTPUT, $PAGE, $COURSE;
+
+$moveselected = optional_param('move', false, PARAM_BOOL);
+$returnurl = optional_param('returnurl', 0, PARAM_LOCALURL);
+$cmid = optional_param('cmid', 0, PARAM_INT);
+$courseid = optional_param('courseid', 0, PARAM_INT);
+$category = optional_param('category', null, PARAM_SEQUENCE);
+$confirm = optional_param('confirm', '', PARAM_ALPHANUM);
+$movequestionselected = optional_param('movequestionsselected', null, PARAM_RAW);
+
+if ($returnurl) {
+ $returnurl = new moodle_url($returnurl);
+}
+
+\core_question\local\bank\helper::require_plugin_enabled('qbank_bulkmove');
+
+if ($cmid) {
+ list($module, $cm) = get_module_from_cmid($cmid);
+ require_login($cm->course, false, $cm);
+ $thiscontext = context_module::instance($cmid);
+} else if ($courseid) {
+ require_login($courseid, false);
+ $thiscontext = context_course::instance($courseid);
+} else {
+ throw new moodle_exception('missingcourseorcmid', 'question');
+}
+
+$contexts = new question_edit_contexts($thiscontext);
+$url = new moodle_url('/question/bank/bulkmove/move.php');
+
+$PAGE->set_url($url);
+$streditingquestions = get_string('movequestions', 'qbank_bulkmove');
+$PAGE->set_title($streditingquestions);
+$PAGE->set_heading($COURSE->fullname);
+
+if ($category) {
+ list($tocategoryid, $contextid) = explode(',', $category);
+ if (! $tocategory = $DB->get_record('question_categories',
+ ['id' => $tocategoryid, 'contextid' => $contextid])) {
+ throw new \moodle_exception('cannotfindcate', 'question');
+ }
+}
+
+if ($movequestionselected && $confirm && confirm_sesskey()) {
+ if ($confirm == md5($movequestionselected)) {
+ \qbank_bulkmove\helper::bulk_move_questions($movequestionselected, $tocategory);
+ }
+ redirect(new moodle_url($returnurl, ['category' => "{$tocategoryid},{$contextid}"]));
+}
+
+echo $OUTPUT->header();
+
+if ($moveselected) {
+ $rawquestions = $_REQUEST;
+ list($questionids, $questionlist) = \qbank_bulkmove\helper::process_question_ids($rawquestions);
+ // No questions were selected.
+ if (!$questionids) {
+ redirect($returnurl);
+ }
+ // Create the urls.
+ $moveparam = [
+ 'movequestionsselected' => $questionlist,
+ 'confirm' => md5($questionlist),
+ 'sesskey' => sesskey(),
+ 'returnurl' => $returnurl,
+ 'cmid' => $cmid,
+ 'courseid' => $courseid,
+ ];
+ $moveurl = new \moodle_url($url, $moveparam);
+
+ $addcontexts = $contexts->having_cap('moodle/question:add');
+ $displaydata = \qbank_bulkmove\helper::get_displaydata($addcontexts, $moveurl, $returnurl);
+ echo $PAGE->get_renderer('qbank_bulkmove')->render_bulk_move_form($displaydata);
+}
+
+echo $OUTPUT->footer();
diff --git a/question/bank/bulkmove/templates/bulk_move.mustache b/question/bank/bulkmove/templates/bulk_move.mustache
new file mode 100644
index 00000000000..0c44b41d6ac
--- /dev/null
+++ b/question/bank/bulkmove/templates/bulk_move.mustache
@@ -0,0 +1,43 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template qbank_bulkmove/bulk_move
+
+ The move form to move selested questions.
+
+ Context variables required for this template:
+ * categorydropdown - dropdown html from the managecategories plugin for the list of categories
+ * moveurl - the url to post the selected category
+ * returnurl - the base page to return to
+
+ Example context (json):
+ {
+ "categorydropdown": "",
+ "moveurl": "/question/bank/bulkmove/move.php?courseid=2",
+ "returnurl": "/question/edit.php?courseid=2"
+ }
+}}
+
+
+
{{#str}} bulkmoveheader, qbank_bulkmove {{/str}}
+
+
diff --git a/question/bank/bulkmove/tests/behat/bulk_move.feature b/question/bank/bulkmove/tests/behat/bulk_move.feature
new file mode 100644
index 00000000000..c901035dda9
--- /dev/null
+++ b/question/bank/bulkmove/tests/behat/bulk_move.feature
@@ -0,0 +1,36 @@
+@qbank @qbank_bulkmove
+Feature: Use the qbank plugin manager page for bulkmove
+ In order to check the plugin behaviour with enable and disable
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "activities" exist:
+ | activity | name | course | idnumber |
+ | quiz | Test quiz | C1 | quiz1 |
+ And the following "question categories" exist:
+ | contextlevel | reference | name |
+ | Course | C1 | Test questions |
+ And the following "questions" exist:
+ | questioncategory | qtype | name | questiontext |
+ | Test questions | truefalse | First question | Answer the first question |
+
+ @javascript
+ Scenario: Enable/disable bulk move questions bulk action from the base view
+ Given I log in as "admin"
+ When I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
+ And I should see "Bulk move questions"
+ And I click on "Disable" "link" in the "Bulk move questions" "table_row"
+ And I am on the "Test quiz" "quiz activity" page
+ And I navigate to "Question bank > Questions" in current page administration
+ And I click on "First question" "checkbox"
+ And I click on "With selected" "button"
+ Then I should not see question bulk action "move"
+ And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
+ And I click on "Enable" "link" in the "Bulk move questions" "table_row"
+ And I am on the "Test quiz" "quiz activity" page
+ And I navigate to "Question bank > Questions" in current page administration
+ And I click on "First question" "checkbox"
+ And I click on "With selected" "button"
+ And I should see question bulk action "move"
diff --git a/question/bank/bulkmove/tests/helper_test.php b/question/bank/bulkmove/tests/helper_test.php
new file mode 100644
index 00000000000..22591569c41
--- /dev/null
+++ b/question/bank/bulkmove/tests/helper_test.php
@@ -0,0 +1,214 @@
+.
+
+namespace qbank_bulkmove;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/question/editlib.php');
+
+/**
+ * Bulk move helper tests.
+ *
+ * @package qbank_bulkmove
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \qbank_bulkmove\helper
+ */
+class helper_test extends \advanced_testcase {
+
+ /**
+ * @var false|object|\stdClass|null $cat
+ */
+ protected $cat;
+
+ /**
+ * @var \stdClass $questiondata1
+ */
+ protected $questiondata1;
+
+ /**
+ * @var \stdClass $questiondata2
+ */
+ protected $questiondata2;
+
+ /**
+ * @var bool|\context|\context_course $context
+ */
+ protected $context;
+
+ /**
+ * @var \question_edit_contexts $contexts
+ */
+ protected $contexts;
+
+ /**
+ * @var \stdClass $course
+ */
+ protected $course;
+
+ /**
+ * @var array $rawdata
+ */
+ protected $rawdata;
+
+ /**
+ * @var object $secondcategory
+ */
+ protected $secondcategory;
+
+ /**
+ * Setup the test.
+ */
+ protected function helper_setup(): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+ $generator = $this->getDataGenerator();
+ /** @var \core_question_generator $questiongenerator */
+ $questiongenerator = $generator->get_plugin_generator('core_question');
+
+ // Create a course.
+ $this->course = $generator->create_course();
+ $this->context = \context_course::instance($this->course->id);
+
+ // Create a question in the default category.
+ $this->contexts = new \question_edit_contexts($this->context);
+ $this->cat = question_make_default_categories($this->contexts->all());
+ $this->questiondata1 = $questiongenerator->create_question('numerical', null,
+ ['name' => 'Example question', 'category' => $this->cat->id]);
+
+ // Create a second category to move questions.
+ $this->secondcategory = $questiongenerator->create_question_category(['contextid' => $this->context->id,
+ 'parent' => $this->cat->id]);
+
+ // Ensure the question is not in the cache.
+ $cache = \cache::make('core', 'questiondata');
+ $cache->delete($this->questiondata1->id);
+
+ $this->questiondata2 = $questiongenerator->create_question('numerical', null,
+ ['name' => 'Example question second', 'category' => $this->cat->id]);
+
+ // Ensure the question is not in the cache.
+ $cache = \cache::make('core', 'questiondata');
+ $cache->delete($this->questiondata2->id);
+
+ // Posted raw data.
+ $this->rawdata = [
+ 'courseid' => $this->course->id,
+ 'cat' => "{$this->cat->id},{$this->context->id}",
+ 'qpage' => '0',
+ "q{$this->questiondata1->id}" => '1',
+ "q{$this->questiondata2->id}" => '1',
+ 'move' => 'Move to'
+ ];
+ }
+
+ /**
+ * Test bulk move of questions.
+ *
+ * @covers ::bulk_move_questions
+ */
+ public function test_bulk_move_questions() {
+ $this->helper_setup();
+ // Verify that the questions are available in the current view.
+ $view = new \core_question\local\bank\view($this->contexts, new \moodle_url('/'), $this->course);
+ ob_start();
+ $pagevars = [
+ 'qpage' => 0,
+ 'qperpage' => 20,
+ 'cat' => $this->cat->id . ',' . $this->context->id,
+ 'recurse' => false,
+ 'showhidden' => false,
+ 'qbshowtext' => false
+ ];
+ $view->display($pagevars, 'editq');
+ $html = ob_get_clean();
+ $this->assertStringContainsString('Example question', $html);
+ $this->assertStringContainsString('Example question second', $html);
+
+ // Get the processed question ids.
+ $questionlist = $this->process_question_ids_test();
+
+ helper::bulk_move_questions($questionlist, $this->secondcategory);
+
+ // Verify the questions are not in the current category.
+ $view = new \core_question\local\bank\view($this->contexts, new \moodle_url('/'), $this->course);
+ ob_start();
+ $pagevars = [
+ 'qpage' => 0,
+ 'qperpage' => 20,
+ 'cat' => $this->cat->id . ',' . $this->context->id,
+ 'recurse' => false,
+ 'showhidden' => false,
+ 'qbshowtext' => false
+ ];
+ $view->display($pagevars, 'editq');
+ $html = ob_get_clean();
+ $this->assertStringNotContainsString('Example question', $html);
+ $this->assertStringNotContainsString('Example question second', $html);
+
+ // Verify the questions are in the new category.
+ $view = new \core_question\local\bank\view($this->contexts, new \moodle_url('/'), $this->course);
+ ob_start();
+ $pagevars = [
+ 'qpage' => 0,
+ 'qperpage' => 20,
+ 'cat' => $this->secondcategory->id . ',' . $this->context->id,
+ 'category' => $this->secondcategory->id . ',' . $this->context->id,
+ 'recurse' => false,
+ 'showhidden' => false,
+ 'qbshowtext' => false
+ ];
+ $view->display($pagevars, 'editq');
+ $html = ob_get_clean();
+ $this->assertStringContainsString('Example question', $html);
+ $this->assertStringContainsString('Example question second', $html);
+ }
+
+ /**
+ * Test the question processing and return the question list.
+ *
+ * @return mixed
+ * @covers ::process_question_ids
+ */
+ protected function process_question_ids_test() {
+ // Test the raw data processing.
+ list($questionids, $questionlist) = helper::process_question_ids($this->rawdata);
+ $this->assertEquals([$this->questiondata1->id, $this->questiondata2->id], $questionids);
+ $this->assertEquals("{$this->questiondata1->id},{$this->questiondata2->id}", $questionlist);
+ return $questionlist;
+ }
+
+ /**
+ * Test the question displaydata.
+ *
+ * @covers ::get_displaydata
+ */
+ public function test_get_displaydata() {
+ $this->helper_setup();
+ $coursecontext = \context_course::instance($this->course->id);
+ $contexts = new \question_edit_contexts($coursecontext);
+ $addcontexts = $contexts->having_cap('moodle/question:add');
+ $url = new \moodle_url('/question/bank/bulkmove/move.php');
+ $displaydata = \qbank_bulkmove\helper::get_displaydata($addcontexts, $url, $url);
+ $this->assertStringContainsString('Test question category 1', $displaydata['categorydropdown']);
+ $this->assertStringContainsString('Default for Category 1', $displaydata['categorydropdown']);
+ $this->assertEquals($url, $displaydata ['moveurl']);
+ $this->assertEquals($url, $displaydata ['returnurl']);
+ }
+}
diff --git a/question/bank/bulkmove/version.php b/question/bank/bulkmove/version.php
new file mode 100644
index 00000000000..604239a5403
--- /dev/null
+++ b/question/bank/bulkmove/version.php
@@ -0,0 +1,31 @@
+.
+
+/**
+ * Version information for qbank_bulkmove.
+ *
+ * @package qbank_bulkmove
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->component = 'qbank_bulkmove';
+$plugin->version = 2021092600;
+$plugin->requires = 2021052500;
+$plugin->maturity = MATURITY_STABLE;
diff --git a/question/bank/deletequestion/classes/bulk_delete_action.php b/question/bank/deletequestion/classes/bulk_delete_action.php
new file mode 100644
index 00000000000..f2fc6931064
--- /dev/null
+++ b/question/bank/deletequestion/classes/bulk_delete_action.php
@@ -0,0 +1,46 @@
+.
+
+namespace qbank_deletequestion;
+
+/**
+ * Class bulk_delete_action is the base class for delete bulk actions ui.
+ *
+ * @package qbank_deletequestion
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class bulk_delete_action extends \core_question\local\bank\bulk_action_base {
+
+ public function get_bulk_action_title(): string {
+ return get_string('delete');
+ }
+
+ public function get_bulk_action_key(): string {
+ return 'deleteselected';
+ }
+
+ public function get_bulk_action_url(): \moodle_url {
+ return new \moodle_url('/question/bank/deletequestion/delete.php');
+ }
+
+ public function get_bulk_action_capabilities(): ?array {
+ return [
+ 'moodle/question:editall',
+ ];
+ }
+}
diff --git a/question/bank/deletequestion/classes/plugin_feature.php b/question/bank/deletequestion/classes/plugin_feature.php
index a7d4039674b..c11fae01d21 100644
--- a/question/bank/deletequestion/classes/plugin_feature.php
+++ b/question/bank/deletequestion/classes/plugin_feature.php
@@ -25,6 +25,7 @@
namespace qbank_deletequestion;
+use core_question\local\bank\bulk_action_base;
use core_question\local\bank\plugin_features_base;
/**
@@ -41,4 +42,8 @@ class plugin_feature extends plugin_features_base {
new delete_action_column($qbank),
];
}
+
+ public function get_bulk_actions(): ?bulk_action_base {
+ return new bulk_delete_action();
+ }
}
diff --git a/question/bank/deletequestion/tests/behat/delete_question_column.feature b/question/bank/deletequestion/tests/behat/delete_question_column.feature
new file mode 100644
index 00000000000..67d4f429f16
--- /dev/null
+++ b/question/bank/deletequestion/tests/behat/delete_question_column.feature
@@ -0,0 +1,63 @@
+@qbank @qbank_deletequestion
+Feature: Use the qbank plugin manager page for deletequestion
+ In order to check the plugin behaviour with enable and disable
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "activities" exist:
+ | activity | name | course | idnumber |
+ | quiz | Test quiz | C1 | quiz1 |
+ And the following "question categories" exist:
+ | contextlevel | reference | name |
+ | Course | C1 | Test questions |
+ And the following "questions" exist:
+ | questioncategory | qtype | name | questiontext |
+ | Test questions | truefalse | First question | Answer the first question |
+ | Test questions | truefalse | First question second | Answer the first question |
+
+ Scenario: Enable/disable delete question column from the base view
+ Given I log in as "admin"
+ When I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
+ And I should see "Delete question"
+ And I click on "Disable" "link" in the "Delete question" "table_row"
+ And I am on the "Test quiz" "quiz activity" page
+ And I navigate to "Question bank > Questions" in current page administration
+ And I click on "#action-menu-toggle-2" "css_element" in the "First question" "table_row"
+ Then I should not see "Delete" in the "region-main" "region"
+ And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
+ And I click on "Enable" "link" in the "Delete question" "table_row"
+ And I am on the "Test quiz" "quiz activity" page
+ And I navigate to "Question bank > Questions" in current page administration
+ And I click on "#action-menu-toggle-2" "css_element" in the "First question" "table_row"
+ And I should see "Delete" in the "region-main" "region"
+
+ @javascript
+ Scenario: Enable/disable delete questions bulk action from the base view
+ Given I log in as "admin"
+ When I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
+ And I should see "Delete question"
+ And I click on "Disable" "link" in the "Delete question" "table_row"
+ And I am on the "Test quiz" "quiz activity" page
+ And I navigate to "Question bank > Questions" in current page administration
+ And I click on "With selected" "button"
+ Then I should not see question bulk action "deleteselected"
+ And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
+ And I click on "Enable" "link" in the "Delete question" "table_row"
+ And I am on the "Test quiz" "quiz activity" page
+ And I navigate to "Question bank > Questions" in current page administration
+ And I click on "With selected" "button"
+ And I should see question bulk action "deleteselected"
+
+ @javascript
+ Scenario: I should not see the deleted questions in the base view
+ Given I log in as "admin"
+ And I am on the "Test quiz" "quiz activity" page
+ And I navigate to "Question bank > Questions" in current page administration
+ And I click on "Select all" "checkbox"
+ And I click on "With selected" "button"
+ And I click on question bulk action "deleteselected"
+ And I click on "Delete" "button" in the "Confirm" "dialogue"
+ Then I should not see "First question"
+ And I should not see "First question second"
diff --git a/question/bank/exportquestions/classes/plugin_feature.php b/question/bank/exportquestions/classes/plugin_feature.php
index e2f245dc23d..bde2f6dfd0e 100644
--- a/question/bank/exportquestions/classes/plugin_feature.php
+++ b/question/bank/exportquestions/classes/plugin_feature.php
@@ -27,6 +27,8 @@
namespace qbank_exportquestions;
+use core_question\local\bank\navigation_node_base;
+
/**
* Class plugin_feature.
*
@@ -37,7 +39,7 @@ namespace qbank_exportquestions;
*/
class plugin_feature extends \core_question\local\bank\plugin_features_base {
- public function get_navigation_node(): ?object {
+ public function get_navigation_node(): ?navigation_node_base {
return new navigation();
}
}
diff --git a/question/bank/importquestions/classes/plugin_feature.php b/question/bank/importquestions/classes/plugin_feature.php
index fc876200366..f16f321047b 100644
--- a/question/bank/importquestions/classes/plugin_feature.php
+++ b/question/bank/importquestions/classes/plugin_feature.php
@@ -25,6 +25,8 @@
namespace qbank_importquestions;
+use core_question\local\bank\navigation_node_base;
+
/**
* Class plugin_feature.
*
@@ -35,7 +37,7 @@ namespace qbank_importquestions;
*/
class plugin_feature extends \core_question\local\bank\plugin_features_base {
- public function get_navigation_node(): ?object {
+ public function get_navigation_node(): ?navigation_node_base {
return new navigation();
}
}
diff --git a/question/bank/managecategories/classes/helper.php b/question/bank/managecategories/classes/helper.php
index f84abf45ed8..cb4f1080ce0 100644
--- a/question/bank/managecategories/classes/helper.php
+++ b/question/bank/managecategories/classes/helper.php
@@ -206,32 +206,34 @@ class helper {
* @param string $selected optionally, the id of a category to be selected by
* default in the dropdown.
* @param int $nochildrenof
+ * @param bool $return to return the string of the select menu or echo that from the method
* @throws \coding_exception|\dml_exception
*/
public static function question_category_select_menu(array $contexts, bool $top = false, int $currentcat = 0,
- string $selected = "", int $nochildrenof = -1): void {
+ string $selected = "", int $nochildrenof = -1, bool $return = false) {
$categoriesarray = self::question_category_options($contexts, $top, $currentcat,
false, $nochildrenof, false);
- if ($selected) {
- $choose = '';
- } else {
- $choose = 'choosedots';
- }
+ $choose = '';
$options = [];
foreach ($categoriesarray as $group => $opts) {
$options[] = [$group => $opts];
}
- echo html_writer::label(get_string('questioncategory', 'core_question'),
- 'id_movetocategory', false, ['class' => 'accesshide']);
+ $outputhtml = html_writer::label(get_string('questioncategory', 'core_question'),
+ 'id_movetocategory', false, ['class' => 'accesshide']);
$attrs = [
'id' => 'id_movetocategory',
'class' => 'custom-select',
'data-action' => 'toggle',
'data-togglegroup' => 'qbank',
'data-toggle' => 'action',
- 'disabled' => true,
+ 'disabled' => false,
];
- echo html_writer::select($options, 'category', $selected, $choose, $attrs);
+ $outputhtml .= html_writer::select($options, 'category', $selected, $choose, $attrs);
+ if ($return) {
+ return $outputhtml;
+ } else {
+ echo $outputhtml;
+ }
}
/**
diff --git a/question/bank/managecategories/classes/plugin_feature.php b/question/bank/managecategories/classes/plugin_feature.php
index 312ba63195e..81fc258e357 100644
--- a/question/bank/managecategories/classes/plugin_feature.php
+++ b/question/bank/managecategories/classes/plugin_feature.php
@@ -16,6 +16,8 @@
namespace qbank_managecategories;
+use core_question\local\bank\navigation_node_base;
+
/**
* Class plugin_feature.
*
@@ -29,7 +31,7 @@ namespace qbank_managecategories;
*/
class plugin_feature extends \core_question\local\bank\plugin_features_base {
- public function get_navigation_node(): ?object {
+ public function get_navigation_node(): ?navigation_node_base {
return new navigation();
}
}
diff --git a/question/bank/managecategories/tests/helper_test.php b/question/bank/managecategories/tests/helper_test.php
index a875edbc09c..b033c211241 100644
--- a/question/bank/managecategories/tests/helper_test.php
+++ b/question/bank/managecategories/tests/helper_test.php
@@ -192,7 +192,6 @@ class helper_test extends \advanced_testcase {
// Test the select menu of question categories output.
$this->assertStringContainsString('Question category', $output);
- $this->assertStringContainsString('', $output);
$this->assertStringContainsString('Test this question category', $output);
}
diff --git a/question/classes/local/bank/bulk_action_base.php b/question/classes/local/bank/bulk_action_base.php
new file mode 100644
index 00000000000..775c5a3cd5a
--- /dev/null
+++ b/question/classes/local/bank/bulk_action_base.php
@@ -0,0 +1,71 @@
+.
+
+namespace core_question\local\bank;
+
+/**
+ * Class bulk_action_base is the base class for bulk actions ui.
+ *
+ * Every plugin wants to implement a bulk action, should extend this class, add appropriate values to the methods
+ * and finally pass this object via plugin_feature class.
+ *
+ * @package core_question
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class bulk_action_base {
+
+ /**
+ * Title of the bulk action.
+ * Every bulk action will have a string to show in the list.
+ *
+ * @return string
+ */
+ abstract public function get_bulk_action_title(): string;
+
+ /**
+ * A unique key for the bulk action, this will be used in the api to identify the action data.
+ * Every bulk must have a unique key to perform the action as a part of the form post in the base view.
+ * When questions are selected, it will post according to the key its selected from the dropdown.
+ *
+ * @return string
+ */
+ abstract public function get_bulk_action_key(): string;
+
+ /**
+ * URL of the bulk action redirect page.
+ * Bulk action can be performed by redirecting to a page and doing the appropriate selection
+ * and finally doing the action. The url will be url of the page where users will be redirected to
+ * select what to do with the selected questions.
+ *
+ * @return \moodle_url
+ */
+ abstract public function get_bulk_action_url(): \moodle_url;
+
+ /**
+ * Get the capabilities for the bulk action.
+ * The bulk actions might have some capabilities to action them as a user.
+ * This method helps to get those caps which will be used by the base view before actioning the bulk action.
+ * For ex: ['moodle/question:moveall', 'moodle/question:add']
+ * At least one of the cap need to be true for the user to use this action.
+ *
+ * @return array|null
+ */
+ public function get_bulk_action_capabilities(): ?array {
+ return null;
+ }
+}
diff --git a/question/classes/local/bank/plugin_features_base.php b/question/classes/local/bank/plugin_features_base.php
index 6c8bccde55f..de06126dd74 100644
--- a/question/classes/local/bank/plugin_features_base.php
+++ b/question/classes/local/bank/plugin_features_base.php
@@ -38,7 +38,7 @@ namespace core_question\local\bank;
class plugin_features_base {
/**
- * This method will return the array of objects to be rendered as a prt of question bank columns/actions.
+ * This method will return the array of objects to be rendered as a part of question bank columns/actions.
*
* @param view $qbank
* @return array
@@ -50,9 +50,18 @@ class plugin_features_base {
/**
* This method will return the object for the navigation node.
*
- * @return null|object
+ * @return null|navigation_node_base
*/
- public function get_navigation_node(): ?object {
+ public function get_navigation_node(): ?navigation_node_base {
+ return null;
+ }
+
+ /**
+ * This method will return the array objects for the bulk actions ui.
+ *
+ * @return null|bulk_action_base
+ */
+ public function get_bulk_actions(): ?bulk_action_base {
return null;
}
diff --git a/question/classes/local/bank/view.php b/question/classes/local/bank/view.php
index c89c21532cb..fb1405e80de 100644
--- a/question/classes/local/bank/view.php
+++ b/question/classes/local/bank/view.php
@@ -149,6 +149,11 @@ class view {
*/
public $customfilterobjects = null;
+ /**
+ * @var array $bulkactions to identify the bulk actions for the api.
+ */
+ public $bulkactions = [];
+
/**
* Constructor for view.
*
@@ -178,6 +183,32 @@ class view {
$this->init_columns($this->wanted_columns(), $this->heading_column());
$this->init_sort();
$this->init_search_conditions();
+ $this->init_bulk_actions();
+ }
+
+ /**
+ * Initialize bulk actions.
+ */
+ protected function init_bulk_actions(): void {
+ $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
+ foreach ($plugins as $componentname => $plugin) {
+ $pluginentrypoint = new $plugin();
+ $pluginentrypointobject = $pluginentrypoint->get_bulk_actions();
+ // Don't need the plugins without bulk actions.
+ if ($pluginentrypointobject === null) {
+ unset($plugins[$componentname]);
+ continue;
+ }
+ if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
+ unset($plugins[$componentname]);
+ continue;
+ }
+ $this->bulkactions[$pluginentrypointobject->get_bulk_action_key()] = [
+ 'title' => $pluginentrypointobject->get_bulk_action_title(),
+ 'url' => $pluginentrypointobject->get_bulk_action_url(),
+ 'capabilities' => $pluginentrypointobject->get_bulk_action_capabilities()
+ ];
+ }
}
/**
@@ -696,11 +727,6 @@ class view {
echo \html_writer::start_div('questionbankwindow boxwidthwide boxaligncenter');
- // This one will become redundant after implementing bulk actions plugin.
- if ($this->process_actions_needing_ui()) {
- return;
- }
-
$editcontexts = $this->contexts->having_one_edit_tab_cap($tabname);
// Show the filters and search options.
@@ -933,7 +959,7 @@ class view {
$this->display_top_pagnation($OUTPUT->render($pagingbar));
// This html will be refactored in the bulk actions implementation.
- echo \html_writer::start_tag('form', ['action' => $pageurl, 'method' => 'post']);
+ echo \html_writer::start_tag('form', ['action' => $pageurl, 'method' => 'post', 'id' => 'questionsubmit']);
echo \html_writer::start_tag('fieldset', ['class' => 'invisiblefieldset', 'style' => "display: block;"]);
echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]);
echo \html_writer::input_hidden_params($this->baseurl);
@@ -942,7 +968,7 @@ class view {
$this->display_bottom_pagination($OUTPUT->render($pagingbar), $totalnumber, $perpage, $pageurl);
- $this->display_bottom_controls($totalnumber, $recurse, $category, $catcontext, $addcontexts);
+ $this->display_bottom_controls($catcontext);
echo \html_writer::end_tag('fieldset');
echo \html_writer::end_tag('form');
@@ -999,53 +1025,44 @@ class view {
/**
* Display the controls at the bottom of the list of questions.
- * @param int $totalnumber Total number of questions that might be shown (if it was not for paging).
- * @param bool $recurse Whether to include subcategories.
- * @param \stdClass $category The question_category row from the database.
- * @param \context $catcontext The context of the category being displayed.
- * @param array $addcontexts contexts where the user is allowed to add new questions.
+ *
+ * @param \context $catcontext The context of the category being displayed.
*/
- protected function display_bottom_controls($totalnumber, $recurse, $category, \context $catcontext, array $addcontexts): void {
+ protected function display_bottom_controls(\context $catcontext): void {
$caneditall = has_capability('moodle/question:editall', $catcontext);
$canuseall = has_capability('moodle/question:useall', $catcontext);
$canmoveall = has_capability('moodle/question:moveall', $catcontext);
-
- echo \html_writer::start_tag('div', ['class' => "modulespecificbuttonscontainer"]);
if ($caneditall || $canmoveall || $canuseall) {
- $withselectedcontent = ' ' . get_string('withselected', 'question') . ':';
- echo \html_writer::tag('strong', $withselectedcontent);
- echo \html_writer::empty_tag('br');
+ global $PAGE;
+ $bulkactiondatas = [];
+ $params = $this->base_url()->params();
+ $params['returnurl'] = $this->base_url();
+ foreach ($this->bulkactions as $key => $action) {
+ // Check capabilities.
+ $capcount = 0;
+ foreach ($action['capabilities'] as $capability) {
+ if (has_capability($capability, $catcontext)) {
+ $capcount ++;
+ }
+ }
+ // At least one cap need to be there.
+ if ($capcount === 0) {
+ unset($this->bulkactions[$key]);
+ continue;
+ }
+ $actiondata = new \stdClass();
+ $actiondata->actionname = $action['title'];
+ $actiondata->actionkey = $key;
+ $actiondata->actionurl = new \moodle_url($action['url'], $params);
+ $bulkactiondata[] = $actiondata;
- // Print delete and move selected question.
- if ($caneditall) {
- echo \html_writer::empty_tag('input', [
- 'type' => 'submit',
- 'class' => 'btn btn-secondary mr-1',
- 'name' => 'deleteselected',
- 'value' => get_string('delete'),
- 'data-action' => 'toggle',
- 'data-togglegroup' => 'qbank',
- 'data-toggle' => 'action',
- 'disabled' => true,
- ]);
+ $bulkactiondatas ['bulkactionitems'] = $bulkactiondata;
}
-
- if ($canmoveall && count($addcontexts)) {
- echo \html_writer::empty_tag('input', [
- 'type' => 'submit',
- 'class' => 'btn btn-secondary mr-1',
- 'name' => 'move',
- 'value' => get_string('moveto', 'question'),
- 'data-action' => 'toggle',
- 'data-togglegroup' => 'qbank',
- 'data-toggle' => 'action',
- 'disabled' => true,
- ]);
- helper::question_category_select_menu($addcontexts, false, 0, "{$category->id},{$category->contextid}");
+ // We dont need to show this section if none of the plugins are enabled.
+ if (!empty($bulkactiondatas)) {
+ echo $PAGE->get_renderer('core_question', 'bank')->render_bulk_actions_ui($bulkactiondatas);
}
}
-
- echo \html_writer::end_tag('div');
}
/**
@@ -1094,7 +1111,7 @@ class view {
*
* @deprecated since Moodle 4.0
* @see print_table()
- * @todo Final deprecation of this function in moodle 4.4
+ * @todo Final deprecation on Moodle 4.4 MDL-72438
*/
protected function start_table() {
debugging('Function start_table() is deprecated, please use print_table() instead.', DEBUG_DEVELOPER);
@@ -1110,7 +1127,7 @@ class view {
*
* @deprecated since Moodle 4.0
* @see print_table()
- * @todo Final deprecation of this function in moodle 4.4
+ * @todo Final deprecation on Moodle 4.4 MDL-72438
*/
protected function end_table() {
debugging('Function end_table() is deprecated, please use print_table() instead.', DEBUG_DEVELOPER);
@@ -1170,126 +1187,27 @@ class view {
/**
* Process actions for the selected action.
- *
+ * @deprecated since Moodle 4.0
+ * @todo Final deprecation on Moodle 4.4 MDL-72438
*/
public function process_actions(): void {
- global $DB;
- // Now, check for commands on this page and modify variables as necessary.
- if (optional_param('move', false, PARAM_BOOL) and confirm_sesskey()) {
- // Move selected questions to new category.
- $category = required_param('category', PARAM_SEQUENCE);
- list($tocategoryid, $contextid) = explode(',', $category);
- if (! $tocategory = $DB->get_record('question_categories',
- ['id' => $tocategoryid, 'contextid' => $contextid])) {
- throw new \moodle_exception('cannotfindcate', 'question');
- }
- $tocontext = \context::instance_by_id($contextid);
- require_capability('moodle/question:add', $tocontext);
- $rawdata = (array) data_submitted();
- $questionids = [];
- foreach ($rawdata as $key => $value) {
- // Parse input for question ids.
- if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
- $key = $matches[1];
- $questionids[] = $key;
- }
- }
- if ($questionids) {
- list($usql, $params) = $DB->get_in_or_equal($questionids);
- $questions = $DB->get_records_sql("
- SELECT q.*, c.contextid
- FROM {question} q
- JOIN {question_categories} c ON c.id = q.category
- WHERE q.id {$usql}", $params);
- foreach ($questions as $question) {
- question_require_capability_on($question, 'move');
- }
- question_move_questions_to_category($questionids, $tocategory->id);
- redirect($this->baseurl->out(false, ['category' => "{$tocategoryid},{$contextid}"]));
- }
- }
-
- if (optional_param('deleteselected', false, PARAM_BOOL)) {
- // Delete selected questions from the category.
- // If teacher has already confirmed the action.
- if (($confirm = optional_param('confirm', '', PARAM_ALPHANUM)) and confirm_sesskey()) {
- $deleteselected = required_param('deleteselected', PARAM_RAW);
- if ($confirm == md5($deleteselected)) {
- if ($questionlist = explode(',', $deleteselected)) {
- // For each question either hide it if it is in use or delete it.
- foreach ($questionlist as $questionid) {
- $questionid = (int)$questionid;
- question_require_capability_on($questionid, 'edit');
- if (questions_in_use([$questionid])) {
- $DB->set_field('question', 'hidden', 1, ['id' => $questionid]);
- } else {
- question_delete_question($questionid);
- }
- }
- }
- redirect($this->baseurl);
- } else {
- throw new \moodle_exception('invalidconfirm', 'question');
- }
- }
- }
-
- // Unhide a question.
- if (($unhide = optional_param('unhide', '', PARAM_INT)) and confirm_sesskey()) {
- question_require_capability_on($unhide, 'edit');
- $DB->set_field('question', 'hidden', 0, ['id' => $unhide]);
-
- // Purge these questions from the cache.
- \question_bank::notify_question_edited($unhide);
-
- redirect($this->baseurl);
- }
+ debugging('Function process_actions() is deprecated and its code has been completely deleted.
+ Please, remove the call from your code and check core_question\local\bank\bulk_action_base
+ to learn more about bulk actions in qbank.', DEBUG_DEVELOPER);
+ // Associated code is deleted to make sure any incorrect call doesnt not cause any data loss.
}
/**
* Process actions with ui.
* @return bool
+ * @deprecated since Moodle 4.0
+ * @todo Final deprecation on Moodle 4.4 MDL-72438
*/
public function process_actions_needing_ui(): bool {
- global $DB, $OUTPUT;
- if (optional_param('deleteselected', false, PARAM_BOOL)) {
- // Make a list of all the questions that are selected.
- $rawquestions = $_REQUEST; // This code is called by both POST forms and GET links, so cannot use data_submitted.
- $questionlist = ''; // Comma separated list of ids of questions to be deleted.
- $questionnames = ''; // String with names of questions separated by with an asterix
- // in front of those that are in use.
- $inuse = false; // Set to true if at least one of the questions is in use.
- foreach ($rawquestions as $key => $value) { // Parse input for question ids.
- if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
- $key = $matches[1];
- $questionlist .= $key.',';
- question_require_capability_on((int)$key, 'edit');
- if (questions_in_use([$key])) {
- $questionnames .= '* ';
- $inuse = true;
- }
- $questionnames .= $DB->get_field('question', 'name', ['id' => $key]) . ' ';
- }
- }
- if (!$questionlist) { // No questions were selected.
- redirect($this->baseurl);
- }
- $questionlist = rtrim($questionlist, ',');
-
- // Add an explanation about questions in use.
- if ($inuse) {
- $questionnames .= ' '.get_string('questionsinuse', 'question');
- }
- $baseurl = new \moodle_url($this->baseurl, $this->baseurl->params());
- $deleteurl = new \moodle_url($baseurl, ['deleteselected' => $questionlist, 'confirm' => md5($questionlist),
- 'sesskey' => sesskey()]);
-
- $continue = new \single_button($deleteurl, get_string('delete'), 'post');
- echo $OUTPUT->confirm(get_string('deletequestionscheck', 'question', $questionnames), $continue, $baseurl);
-
- return true;
- }
-
+ debugging('Function process_actions_needing_ui() is deprecated and its code has been completely deleted.
+ Please, remove the call from your code and check core_question\local\bank\bulk_action_base
+ to learn more about bulk actions in qbank.', DEBUG_DEVELOPER);
+ // Associated code is deleted to make sure any incorrect call doesnt not cause any data loss.
return false;
}
diff --git a/question/edit.php b/question/edit.php
index a5444594f95..b0d5ade4ae0 100644
--- a/question/edit.php
+++ b/question/edit.php
@@ -38,9 +38,6 @@ $PAGE->set_url($url);
$questionbank = new core_question\local\bank\view($contexts, $thispageurl, $COURSE, $cm);
-// TODO MDL-72076 - this one will become redundant after implementing bulk actions UI.
-$questionbank->process_actions();
-
$context = $contexts->lowest();
$streditingquestions = get_string('editquestions', 'question');
$PAGE->set_title($streditingquestions);
diff --git a/question/renderer.php b/question/renderer.php
index 7b8ca0bb0a2..8d4cb3c5a1b 100644
--- a/question/renderer.php
+++ b/question/renderer.php
@@ -156,6 +156,16 @@ class core_question_bank_renderer extends plugin_renderer_base {
return $this->render_from_template('core_question/showtext_checkbox', $displaydata);
}
+ /**
+ * Render bulk actions ui.
+ *
+ * @param array $displaydata
+ * @return bool|string
+ */
+ public function render_bulk_actions_ui($displaydata) {
+ return $this->render_from_template('core_question/bulk_actions_ui', $displaydata);
+ }
+
/**
* Build the HTML for the question chooser javascript popup.
*
diff --git a/question/templates/bulk_actions_ui.mustache b/question/templates/bulk_actions_ui.mustache
new file mode 100644
index 00000000000..4601cd09c1b
--- /dev/null
+++ b/question/templates/bulk_actions_ui.mustache
@@ -0,0 +1,43 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core_question/bulk_actions_ui
+
+ Example context (json):
+ {
+ "displaydata": [
+ {
+ "bulkactionitems": "",
+ "actionname": "Move to",
+ "actionkey": "deleteselected",
+ "actionurl": "/question/bank/bulkmove/move.php?courseid=2"
+ }
+ ]
+ }
+}}
+
+
+
+
+ {{#bulkactionitems}}
+
+ {{/bulkactionitems}}
+
+
diff --git a/question/tests/behat/behat_question.php b/question/tests/behat/behat_question.php
index 6a7c097fe54..1985da7bea1 100644
--- a/question/tests/behat/behat_question.php
+++ b/question/tests/behat/behat_question.php
@@ -103,4 +103,40 @@ class behat_question extends behat_question_base {
$this->execute("behat_general::i_click_on_in_the",
[$action, 'link', $questionname, 'table_row']);
}
+
+ /**
+ * A particular bulk action is visible in the question bank UI.
+ *
+ * @When I should see question bulk action :action
+ * @param string $action the value of the input for the action.
+ */
+ public function i_should_see_question_bulk_action($action) {
+ // Check if its visible.
+ $this->execute("behat_general::should_be_visible",
+ ["#bulkactionsui-container input[name='$action']", "css_element"]);
+ }
+
+ /**
+ * A particular bulk action should not be visible in the question bank UI.
+ *
+ * @When I should not see question bulk action :action
+ * @param string $action the value of the input for the action.
+ */
+ public function i_should_not_see_question_bulk_action($action) {
+ // Check if its visible.
+ $this->execute("behat_general::should_not_be_visible",
+ ["#bulkactionsui-container input[name='$action']", "css_element"]);
+ }
+
+ /**
+ * A click on a particular bulk action in the question bank UI.
+ *
+ * @When I click on question bulk action :action
+ * @param string $action the value of the input for the action.
+ */
+ public function i_click_on_question_bulk_action($action) {
+ // Click the bulk action.
+ $this->execute("behat_general::i_click_on",
+ ["#bulkactionsui-container input[name='$action']", "css_element"]);
+ }
}
diff --git a/question/tests/behat/question_categories.feature b/question/tests/behat/question_categories.feature
index bf2dffeff2a..42381d44ae9 100644
--- a/question/tests/behat/question_categories.feature
+++ b/question/tests/behat/question_categories.feature
@@ -31,8 +31,10 @@ Feature: A teacher can move questions between categories in the question bank
When I navigate to "Question bank > Questions" in current page administration
And I set the field "Select a category" to "Used category"
And I click on "Test question to be moved" "checkbox" in the "Test question to be moved" "table_row"
+ And I click on "With selected" "button"
+ And I click on question bulk action "move"
And I set the field "Question category" to "Subcategory"
- And I press "Move to >>"
+ And I press "Move to"
Then I should see "Test question to be moved"
And the field "Select a category" matches value " Subcategory (1)"
And the "Select a category" select box should contain "Used category"
diff --git a/question/tests/behat/question_categories_idnumber.feature b/question/tests/behat/question_categories_idnumber.feature
index 88bee950185..1bcf0409ae6 100644
--- a/question/tests/behat/question_categories_idnumber.feature
+++ b/question/tests/behat/question_categories_idnumber.feature
@@ -84,8 +84,10 @@ Feature: A teacher can put questions with idnumbers in categories in the questio
And I press "submitbutton"
# Javascript is required for the next step.
And I click on "Test question 3" "checkbox" in the "Test question 3" "table_row"
- And I set the field "Question category" to "Used category"
- And I press "Move to >>"
+ And I click on "With selected" "button"
+ And I click on question bulk action "move"
+ And I set the field "Question category" to "Subcategory"
+ And I press "Move to"
And I choose "Edit question" action for "Test question 3" in the question bank
# The question just moved into this category needs to have a unique idnumber, so a number is appended.
Then the field "ID number" matches value "q1_1"
diff --git a/question/tests/behat/select_questions.feature b/question/tests/behat/select_questions.feature
index b4b766e3f5f..90ff84c6432 100644
--- a/question/tests/behat/select_questions.feature
+++ b/question/tests/behat/select_questions.feature
@@ -49,8 +49,11 @@ Feature: The questions in the question bank can be selected in various ways
@javascript
Scenario: The action button can be disabled when the question not be chosen in the list of questions
- Given the "Delete" "button" should be disabled
- And the "Move to >>" "button" should be disabled
- When I click on "Select all" "checkbox"
- Then the "Delete" "button" should be enabled
- And the "Move to >>" "button" should be enabled
+ Given the field "Select all" matches value ""
+ When I click on "With selected" "button"
+ And I should not see "Delete"
+ And I should not see "Move to..."
+ And I click on "Select all" "checkbox"
+ And I click on "With selected" "button"
+ Then I should see question bulk action "move"
+ And I should see question bulk action "deleteselected"
diff --git a/question/upgrade.txt b/question/upgrade.txt
index 0bddd1d756a..ea09fa1c2ba 100644
--- a/question/upgrade.txt
+++ b/question/upgrade.txt
@@ -73,6 +73,12 @@ This files describes API changes for code that uses the question API.
restart_preview() => qbank_previewquestion\helper::restart_preview(),
core_question_output_fragment_tags_form() => /question/bank/qbank_tagquestion/lib.php.
+10)The qbank api now allows bulk actions from qbank plugins. Its possible to implement a qbank plugin and pass the bulk
+ bulk action object from the plugin feature using the get_bulk_actions(). The base class for this feature is bulk_action_base.
+ The following methods are deprecated as a part of bulk actions ui implementation and its no more required to call these methods
+ anymore as the api is now self sufficient, calling the api will fetch all the features:
+ process_actions(), process_actions_needing_ui().
+
=== 3.9 ==
1) For years, the ..._questions_in_use callback has been the right way for plugins to