+ {{#hascapability}}
+ {{>qbank_managecategories/addcategory}}
+ {{/hascapability}}
+ {{#showdescriptions}}
+ {{>qbank_managecategories/showdescriptions}}
+ {{/showdescriptions}}
+
diff --git a/question/bank/managecategories/templates/category.mustache b/question/bank/managecategories/templates/category.mustache
new file mode 100644
index 0000000000000..68b2db3a11ddd
--- /dev/null
+++ b/question/bank/managecategories/templates/category.mustache
@@ -0,0 +1,84 @@
+{{!
+ 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
+
{{{categorydesc}}}
{{/categorydesc}}
diff --git a/question/bank/managecategories/templates/move_category_list.mustache b/question/bank/managecategories/templates/move_category_list.mustache
new file mode 100644
index 0000000000000..9004544e1ec00
--- /dev/null
+++ b/question/bank/managecategories/templates/move_category_list.mustache
@@ -0,0 +1,117 @@
+{{!
+ 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_managecategories/move_category_list
+
+ Partial to render a list of categories that the current category can be moved next to.
+
+ This partial is included recursively to render nested lists. Take care to pass categories = null for bottom-level
+ categories, to avoid infinite recursion.
+
+ Context variables required for this template:
+ * categories - Array of category objects.
+ * categoryname - The name of the category to move to.
+ * categoryid - The ID of the category.
+ * firstchild - Is this the first child of its parent? If so, a "before" option will be rendered as well as "after".
+ * current - Is this the category being moved? If so, skip rendering it as a target.
+ * categories - Array containing child categories of this category.
+ If there are none, this must be set null or [] to avoid infinite recursion.
+
+ Example context (json):
+ {
+ "categories": [
+ {
+ "categoryname": "Default category for course 1",
+ "categoryid": 3,
+ "firstchild": true,
+ "current": false,
+ "hascategories": false,
+ "categories": []
+ },
+ {
+ "categoryname": "Test category 1",
+ "categoryid": 4,
+ "firstchild": false,
+ "current": false,
+ "hascategories": false,
+ "categories": []
+ },
+ {
+ "categoryname": "Test category 2",
+ "categoryid": 5,
+ "firstchild": false,
+ "current": true,
+ "hascategories": false,
+ "categories": []
+ },
+ {
+ "categoryname": "Test category 3 x < 1 && y > 2 ",
+ "categoryid": 6,
+ "firstchild": false,
+ "current": false,
+ "hascategories": true,
+ "categories": [
+ {
+ "categoryname": "Test category 4",
+ "categoryid": 7,
+ "firstchild": true,
+ "current": false,
+ "hascategories": false,
+ "categories": []
+ }
+ ]
+ }
+ ]
+ }
+}}
+
+
diff --git a/question/bank/managecategories/templates/move_context_list.mustache b/question/bank/managecategories/templates/move_context_list.mustache
new file mode 100644
index 0000000000000..69abdabe2f8e2
--- /dev/null
+++ b/question/bank/managecategories/templates/move_context_list.mustache
@@ -0,0 +1,83 @@
+{{!
+ 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_managecategories/move_context_list
+
+ Modal to move a category.
+
+ Context variables required for this template:
+ * contexts - Array of contexts containing question categories
+ * contextid - The id of the context.
+ * contextname - The name of the context to display as a heading.
+ * categories - Array of category objects. See qbank_managecategories/move_category_list partial for details.
+
+ Example context (json):
+ {
+ "contexts": [
+ {
+ "contexid": 1,
+ "contextname": "Course 1",
+ "categories": [
+ {
+ "categoryname": "Default category for course 1",
+ "categoryid": 3,
+ "firstchild": true,
+ "current": false,
+ "hascategories": false,
+ "categories": []
+ },
+ {
+ "categoryname": "Default category for course category 1",
+ "categoryid": 4,
+ "firstchild": false,
+ "current": true,
+ "hascategories": false,
+ "categories": []
+ }
+ ]
+ },
+ {
+ "contexid": 3,
+ "contextname": "Course 2",
+ "categories": [
+ {
+ "categoryname": "Default category for course 2",
+ "categoryid": 5,
+ "firstchild": true,
+ "current": false,
+ "hascategories": false,
+ "categories": []
+ },
+ {
+ "categoryname": "Default category for course category 2",
+ "categoryid": 6,
+ "firstchild": false,
+ "current": false,
+ "hascategories": false,
+ "categories": []
+ }
+ ]
+ }
+ ]
+ }
+}}
+
+ {{#contexts}}
+
{{{contextname}}}
+ {{>qbank_managecategories/move_category_list}}
+ {{/contexts}}
+
diff --git a/question/bank/managecategories/templates/newchild.mustache b/question/bank/managecategories/templates/newchild.mustache
new file mode 100644
index 0000000000000..350cda59abb9c
--- /dev/null
+++ b/question/bank/managecategories/templates/newchild.mustache
@@ -0,0 +1,46 @@
+{{!
+ 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_managecategories/newchild
+
+ Drop zone for creating a new child category.
+
+ Context variables required for this template:
+ * categoryid - The id of the parent category.
+
+ Example context (json):
+ {
+ "categoryid": "1",
+ "newchildtooltip": "As new child of Example"
+ }
+}}
+
+ +
+
+
+{{#js}}
+ require(['qbank_managecategories/newchild'], function(component) {
+ component.init('.qbank_managecategories-newchild[data-parent="{{categoryid}}"]');
+ });
+{{/js}}
diff --git a/question/bank/managecategories/templates/showdescriptions.mustache b/question/bank/managecategories/templates/showdescriptions.mustache
new file mode 100644
index 0000000000000..b8310270c9a80
--- /dev/null
+++ b/question/bank/managecategories/templates/showdescriptions.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_managecategories/showdescriptions
+
+ Template for displaying the Show descriptions toggle.
+
+ Context variables required for this template:
+ * helpstringhead - String header accompanied with it help button.
+ * hascapability - Boolean, true if user has capability to add categories.
+ * checkbox - Checkbox displaying descriptions.
+ * contextid - Context id for js init.
+ * cmid - Course module id, if in a plugin.
+
+ Example context (json):
+ {
+ "id": "showdescriptions-toggle",
+ "label": "Show descriptions",
+ "checked": true
+ }
+}}
+
+ {{>core/toggle}}
+
+{{#js}}
+ require(['qbank_managecategories/showdescriptions'], function(component) {
+ component.init('#qbank_managecategories-showdescriptions');
+ });
+{{/js}}
diff --git a/question/bank/managecategories/tests/behat/move_question_categories.feature b/question/bank/managecategories/tests/behat/move_question_categories.feature
index 85953139c70d8..19b39b6fb63b5 100644
--- a/question/bank/managecategories/tests/behat/move_question_categories.feature
+++ b/question/bank/managecategories/tests/behat/move_question_categories.feature
@@ -1,43 +1,101 @@
-@core @core_question
-Feature: A teacher can move question categories in the question bank
- In order to organize my questions
+@qbank @qbank_managecategories @category_reorder @javascript
+Feature: A teacher can reorder question categories
+ In order to change question category order
As a teacher
- I create question categories and move them in the question bank
+ I need to reorder them
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
- | teacher1 | T1 | Teacher1 | teacher1@example.com |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
- | fullname | shortname | category |
- | Course 1 | C1 | 0 |
+ | fullname | shortname | format |
+ | Course 1 | C1 | weeks |
+ And the following "categories" exist:
+ | name | category | idnumber |
+ | Category 1 | 0 | CAT1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
- And the following "activities" exist:
- | activity | name | course | idnumber |
- | quiz | Test quiz | C1 | quiz1 |
-
- Scenario: A question category can be moved to another context
- Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
- And I select "Categories" from the "Question bank tertiary navigation" singleselect
- And I follow "Add category"
- And I set the following fields to these values:
- | Name | Test category |
- | Parent category | Top for Test quiz |
- And I press "submitbutton"
- And I click on "Share in context for Course: Course 1" "link" in the "Test category" "list_item"
- Then I should see "Test category" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' questioncategories ') and contains(concat(' ', normalize-space(@class), ' '), ' contextlevel50 ')]" "xpath_element"
-
- Scenario: A question category can be moved to top level
- Given I am on the "Test quiz" "mod_quiz > question bank" page logged in as "teacher1"
- And I select "Categories" from the "Question bank tertiary navigation" singleselect
- And I follow "Add category"
- And I set the following fields to these values:
- | Name | Test category |
- | Parent category | Default for Test quiz |
- | Category info | Created as a test |
- And I press "submitbutton"
- And I click on "Move to top level" "link" in the "Test category" "list_item"
- Then I should see "Test category" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' questioncategories ') and contains(concat(' ', normalize-space(@class), ' '), ' contextlevel70 ')]" "xpath_element"
- And "//div[contains(concat(' ', normalize-space(@class), ' '), ' questioncategories ') and contains(concat(' ', normalize-space(@class), ' '), ' contextlevel70 ')]//li//ul" "xpath_element" should not exist
+ And the following "system role assigns" exist:
+ | user | role | contextlevel |
+ | teacher1 | editingteacher | System |
+ And the following "question categories" exist:
+ | contextlevel | reference | name | idnumber |
+ | Course | C1 | Course category 1 | questioncat1 |
+ | Course | C1 | Course category 2 | questioncat2 |
+ | Course | C1 | Course category 3 | questioncat3 |
+ | Category | CAT1 | Default for Category 1 | |
+ | System | S1 | System category | |
+ And I am on the "Course 1" "core_question > course question categories" page logged in as "teacher1"
+
+ Scenario: Teacher cannot move or delete single category under context
+ When I open the action menu in "Default for Category 1" "list_item"
+ Then I should not see "Delete"
+
+ Scenario: Teacher can see complete edit menu if multiples categories exist under context
+ When I open the action menu in "Course category 1" "list_item"
+ Then I should see "Edit settings"
+ And I should see "Delete"
+ And I should see "Export as Moodle XML"
+
+ Scenario: Teacher can move one category after another
+ Given "Course category 1" "list_item" should appear before "Course category 2" "list_item"
+ And "Course category 2" "list_item" should appear before "Course category 3" "list_item"
+ When I open the action menu in "Course category 1" "list_item"
+ And I choose "Move" in the open action menu
+ And I click on "After Course category 3" "link" in the "Move Course category 1" "dialogue"
+ Then "Course category 2" "list_item" should appear before "Course category 3" "list_item"
+ And "Course category 3" "list_item" should appear before "Course category 1" "list_item"
+
+ Scenario: Teacher can move one category before another
+ Given "Course category 1" "list_item" should appear before "Course category 2" "list_item"
+ And "Course category 2" "list_item" should appear before "Course category 3" "list_item"
+ And I open the action menu in "Course category 3" "list_item"
+ And I choose "Move" in the open action menu
+ And I click on "Before Course category 1" "link" in the "Move Course category 3" "dialogue"
+ Given "Course category 3" "list_item" should appear before "Course category 1" "list_item"
+ And "Course category 1" "list_item" should appear before "Course category 2" "list_item"
+
+ Scenario: Teacher can make a category a child of an existing category
+ Given "Course category 1" "list_item" should appear before "Course category 2" "list_item"
+ And "Course category 2" "list_item" should appear before "Course category 3" "list_item"
+ And "Course category 3" "list_item" should not exist in the "Course category 1" "list_item"
+ When I open the action menu in "Course category 3" "list_item"
+ And I choose "Move" in the open action menu
+ And I click on "As new child of Course category 1" "link" in the "Move Course category 3" "dialogue"
+ And "Course category 3" "list_item" should appear before "Course category 2" "list_item"
+ And "Course category 3" "list_item" should exist in the "Course category 1" "list_item"
+
+ Scenario: Teacher can move a category between contexts
+ Given "Course: Course 1" "text" should appear before "Course category 1" "list_item"
+ And "Category: Category 1" "text" should appear after "Course category 1" "list_item"
+ And "Category: Category 1" "text" should appear before "Default for Category 1" "list_item"
+ And "Course category 2" "list_item" should appear before "Course category 3" "list_item"
+ When I open the action menu in "Course category 1" "list_item"
+ And I choose "Move" in the open action menu
+ And I click on "After Default for Category 1" "link" in the "Move Course category 1" "dialogue"
+ Then "Course: Course 1" "text" should appear before "Course category 1" "list_item"
+ And "Category: Category 1" "text" should appear before "Course category 1" "list_item"
+ And "Default for Category 1" "list_item" should appear before "Course category 1" "list_item"
+
+ Scenario: Teacher can display and hide category descriptions
+ When I click on "Show descriptions" "checkbox"
+ Then I should see "The default category for questions shared in context 'Category 1'."
+ And I click on "Show descriptions" "checkbox"
+ And I should not see "The default category for questions shared in context 'Category 1'."
+
+ Scenario: Teacher cannot create a duplicate idnumber within a context by moving a category
+ Given "Course category 1" "list_item" should appear before "System category" "list_item"
+ And I open the action menu in "Course category 1" "list_item"
+ And I choose "Move" in the open action menu
+ And I click on "After System category" "link" in the "Move Course category 1" "dialogue"
+ Then "Course category 1" "list_item" should appear after "System category" "list_item"
+ And I open the action menu in "Course category 2" "list_item"
+ And I choose "Edit settings" in the open action menu
+ And I set the field "ID number" to "questioncat1"
+ And I click on "Save changes" "button" in the "Edit category" "dialogue"
+ And I open the action menu in "Course category 2" "list_item"
+ And I choose "Move" in the open action menu
+ And I click on "After Course category 1" "link" in the "Move Course category 2" "dialogue"
+ And I should see "ID number already in use, please change it to move or update category"
diff --git a/question/bank/managecategories/tests/behat/question_categories.feature b/question/bank/managecategories/tests/behat/question_categories.feature
index 593323521ccac..62a79ca9e1bcd 100644
--- a/question/bank/managecategories/tests/behat/question_categories.feature
+++ b/question/bank/managecategories/tests/behat/question_categories.feature
@@ -1,4 +1,4 @@
-@qbank @qbank_managecategories @javascript
+@qbank @qbank_managecategories @question_categories @javascript
Feature: A teacher can put questions in categories in the question bank
In order to organize my questions
As a teacher
@@ -6,44 +6,44 @@ Feature: A teacher can put questions in categories in the question bank
Background:
Given the following "users" exist:
- | username | firstname | lastname | email |
- | teacher1 | Teacher | 1 | teacher1@example.com |
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
- | Course 1 | C1 | weeks |
+ | Course 1 | C1 | weeks |
And the following "course enrolments" exist:
- | user | course | role |
- | teacher1 | C1 | editingteacher |
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
And the following "question categories" exist:
- | contextlevel | reference | questioncategory | name |
- | Course | C1 | Top | top |
- | Course | C1 | top | Default for C1 |
- | Course | C1 | Default for C1 | Subcategory |
- | Course | C1 | Default for C1 | Another subcat |
- | Course | C1 | top | Used category |
- | Course | C1 | top | Default & testing |
+ | contextlevel | reference | questioncategory | name | sortorder | desciption |
+ | Course | C1 | Top | top | 0 | |
+ | Course | C1 | top | Default for C1 | 0 | Description for default for C1 |
+ | Course | C1 | Default for C1 | Subcategory | 0 | Description for Subcategory |
+ | Course | C1 | Default for C1 | Another subcat | 1 | Description for Another subcat |
+ | Course | C1 | top | Used category | 1 | |
+ | Course | C1 | top | Default & testing | 2 | |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Used category | essay | Test question to be moved | Write about whatever you want |
| Another subcat | essay | Question 1 | Write about whatever you want |
And I log in as "teacher1"
- And I am on "Course 1" course homepage
Scenario: A new question category can be created
When I am on the "Course 1" "core_question > course question categories" page
- And I follow "Add category"
+ And I press "Add category"
And I set the following fields to these values:
| Name | 'Test' & 'display' |
| Parent category | Default & testing |
| Category info | Created for testing category, HTML entity & its encoding |
| ID number | newcatidnumber |
- And I press "submitbutton"
+ And I click on "Add category" "button" in the "Add category" "dialogue"
Then I should see "Default & testing"
And I should see "ID number"
And I should see "newcatidnumber"
And I should see "(0)"
+ And I click on "Show descriptions" "checkbox"
And I should see "Created for testing category, HTML entity & its encoding" in the "'Test' & 'display'" "list_item"
- And I follow "Add category"
+ And I press "Add category"
And the "Parent category" select box should contain "'Test' & 'display' [newcatidnumber]"
Scenario: A question category can be edited
@@ -52,29 +52,35 @@ Feature: A teacher can put questions in categories in the question bank
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Subcategory | essay | Test question for renaming category | Write about whatever you want |
- And I click on "Edit this category" "link" in the "Subcategory" "list_item"
+ And I open the action menu in "Subcategory" "list_item"
+ And I choose "Edit settings" in the open action menu
And the field "parent" matches value " Default for C1"
And I set the following fields to these values:
| Name | New name |
| Category info | I was edited |
And I press "Save changes"
Then I should see "New name"
- And I should see "I was edited" in the "New name" "list_item"
+ And I click on "Show descriptions" "checkbox"
+ And I should see "I was edited"
Scenario: An empty question category can be deleted
When I am on the "Course 1" "core_question > course question categories" page
- And I click on "Delete" "link" in the "Subcategory" "list_item"
+ And I open the action menu in "Subcategory" "list_item"
+ And I choose "Delete" in the open action menu
+ And I click on "Delete" "button" in the "Delete" "dialogue"
Then I should not see "Subcategory"
Scenario: An non-empty question category can be deleted if you move the contents elsewhere
When 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 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 should see "The category 'Used category' contains 1 questions"
And I select "Default for C1" from the "Category" singleselect
And I press "Save in category"
Then I should not see "Used category"
- And I follow "Add category"
- And I should see "Default for C1 (1)"
+ And I should see "Default for C1"
+ And I should see "(1)"
@_file_upload
Scenario: Multi answer questions with their child questions can be moved to another category when the current category is deleted
@@ -84,12 +90,13 @@ Feature: A teacher can put questions in categories in the question bank
And I press "id_submitbutton"
And I press "Continue"
And I am on the "Course 1" "core_question > course question categories" page
- And I click on "Delete" "link" in the "Default for Test images in backup" "list_item"
+ And I open the action menu in "Default for Test images in backup" "list_item"
+ And I choose "Delete" in the open action menu
And I should see "The category 'Default for Test images in backup' contains 1 questions"
And I select "Used category" from the "Category" singleselect
And I press "Save in category"
Then I should not see "Default for Test images in backup"
- And I follow "Add category"
+ And I press "Add category"
And I should see "Used category (2)"
Scenario: Filter questions by category and subcategories
diff --git a/question/bank/managecategories/tests/behat/question_categories_idnumber.feature b/question/bank/managecategories/tests/behat/question_categories_idnumber.feature
index 0c23df2e4791f..2db6176742c0b 100644
--- a/question/bank/managecategories/tests/behat/question_categories_idnumber.feature
+++ b/question/bank/managecategories/tests/behat/question_categories_idnumber.feature
@@ -1,4 +1,4 @@
-@qbank @qbank_managecategories
+@qbank @qbank_managecategories @question_categories_idnumber @javascript
Feature: A teacher can put questions with idnumbers in categories with idnumbers in the question bank
In order to organize my questions
As a teacher
@@ -20,34 +20,36 @@ Feature: A teacher can put questions with idnumbers in categories with idnumbers
Scenario: A new question category can only be created with a unique idnumber for a context
# Note need to create the top category each time.
When the following "question categories" exist:
- | contextlevel | reference | questioncategory | name | idnumber |
- | Course | C1 | Top | top | |
- | Course | C1 | top | Used category | c1used |
+ | contextlevel | reference | questioncategory | name | idnumber |
+ | Course | C1 | Top | top | |
+ | Course | C1 | top | Used category | c1used |
And I am on the "Course 1" "core_question > course question categories" page
- And I follow "Add category"
+ And I press "Add category"
And I set the following fields to these values:
- | Name | Sub used category |
- | Parent category | Used category |
+ | Name | New cat |
+ | Parent category | Top for Course 1 |
| Category info | Created as a test |
| ID number | c1used |
- And I press "submitbutton"
+ And I click on "Add category" "button" in the "Add category" "dialogue"
# Standard warning.
Then I should see "This ID number is already in use"
# Correction to a unique idnumber for the context.
And I set the field "ID number" to "c1unused"
- And I press "submitbutton"
- Then I should see "Sub used category"
+ And I click on "Add category" "button" in the "Add category" "dialogue"
+ Then I should see "New cat"
And I should see "ID number"
And I should see "c1unused"
And I should see "(0)"
- And I should see "Created as a test" in the "Sub used category" "list_item"
+ And I click on "Show descriptions" "checkbox"
+ And I should see "Created as a test" in the "New cat" "list_item"
Scenario: A question category can be edited and saved without changing the idnumber
When the following "question categories" exist:
- | contextlevel | reference | questioncategory | name | idnumber |
- | Course | C1 | Top | top | |
- | Course | C1 | top | Used category | c1used |
+ | contextlevel | reference | questioncategory | name | idnumber |
+ | Course | C1 | Top | top | |
+ | Course | C1 | top | Used category | c1used |
And I am on the "Course 1" "core_question > course question categories" page
- And I click on "Edit this category" "link" in the "Used category" "list_item"
- And I press "Save changes"
+ Then I open the action menu in "Used category" "list_item"
+ And I choose "Edit settings" in the open action menu
+ And I click on "Save changes" "button" in the "Edit category" "dialogue"
Then I should not see "This ID number is already in use"
diff --git a/question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature b/question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature
index cf6f5ce93e1ed..457108cf7200b 100644
--- a/question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature
+++ b/question/bank/managecategories/tests/behat/view_manage_categories_plugin.feature
@@ -1,4 +1,4 @@
-@qbank @qbank_managecategories @javascript
+@qbank @qbank_managecategories @view_manage_categories_plugin @javascript
Feature: Use the qbank plugin manager page for managecategories
In order to check the plugin behaviour with enable and disable
@@ -7,8 +7,8 @@ Feature: Use the qbank plugin manager page for managecategories
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "activities" exist:
- | activity | name | course | idnumber |
- | quiz | Test quiz | C1 | quiz1 |
+ | activity | name | course | idnumber |
+ | quiz | Test quiz | C1 | quiz1 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
@@ -26,7 +26,7 @@ Feature: Use the qbank plugin manager page for managecategories
And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
And I click on "Enable" "link" in the "Manage categories" "table_row"
And I am on the "Test quiz" "mod_quiz > question bank" page
- And I should see "Categories" in the "//div[contains(@class, 'urlselect')]//option[contains(text(), 'Categories')]" "xpath_element"
+ And I should see "Categories" in the "Question bank tertiary navigation" "select"
Scenario: Enable/disable the tab New category when trying to add a random question to a quiz
Given I log in as "admin"
diff --git a/question/bank/managecategories/tests/external/move_category_test.php b/question/bank/managecategories/tests/external/move_category_test.php
new file mode 100644
index 0000000000000..afd9525d17303
--- /dev/null
+++ b/question/bank/managecategories/tests/external/move_category_test.php
@@ -0,0 +1,535 @@
+.
+
+namespace qbank_managecategories\external;
+
+use context_course;
+use qbank_managecategories\helper;
+use qbank_managecategories\question_categories;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/../manage_category_test_base.php');
+
+/**
+ * Unit tests for move_category
+ *
+ * @package qbank_managecategories
+ * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
+ * @author Mark Johnson
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @covers \qbank_managecategories\external\move_category
+ */
+final class move_category_test extends \qbank_managecategories\manage_category_test_base {
+
+ /**
+ * Return order of categories for a given context.
+ *
+ * @param int $contextid The context to get the category order for.
+ * @return array Nested array, keyed by category IDs.
+ */
+ private function get_current_order(int $contextid): array {
+ return $this->reduce_tree(question_categories::create_ordered_tree(helper::get_categories_for_contexts($contextid)));
+ }
+
+ /**
+ * Reduce the ordered tree of categories to a multi-dimensional array of IDs for easier comparison.
+ *
+ * @param array $tree Tree of categories from helper::create_ordered_tree
+ * @return array
+ */
+ private function reduce_tree(array $tree): array {
+ $result = [];
+ foreach ($tree as $category) {
+ $result[$category->id] = [];
+ if (isset($category->children) && !empty((array)$category->children)) {
+ $result[$category->id] = $this->reduce_tree($category->children);
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Move a category below another category within the same parent.
+ *
+ * @return void
+ */
+ public function test_move_category_down(): void {
+ $this->setAdminUser();
+ $this->resetAfterTest();
+
+ // Create context for question categories.
+ $course = $this->create_course();
+ $context = context_course::instance($course->id);
+ $this->create_course_category();
+
+ // Question categories.
+ $qcat1 = $this->create_question_category_for_a_course($course);
+ $qcat2 = $this->create_question_category_for_a_course($course);
+ $qcat3 = $this->create_question_category_for_a_course($course);
+
+ // Check current order.
+ $currentorder = $this->get_current_order($context->id);
+ $expectedorder = [
+ $qcat1->id => [],
+ $qcat2->id => [],
+ $qcat3->id => [],
+ ];
+ $this->assertEquals($expectedorder, $currentorder);
+
+ // Move category 1 after category 2.
+ $stateupdates = move_category::execute($context->id, $qcat1->id, $qcat1->parent, $qcat2->id);
+
+ $neworder = $this->get_current_order($context->id);
+ $newexpectedorder = [
+ $qcat2->id => [],
+ $qcat1->id => [],
+ $qcat3->id => [],
+ ];
+ $this->assertEquals($newexpectedorder, $neworder);
+
+ // We should have an update to the sortorder of the moved category and following categories.
+ $expectedstateupdates = [
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat1->id,
+ 'sortorder' => 3,
+ ],
+ ],
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat3->id,
+ 'sortorder' => 4,
+ ],
+ ],
+ ];
+ $this->assertEquals($stateupdates, $expectedstateupdates);
+ }
+
+ /**
+ * Move a category to the top of its parent.
+ *
+ * @return void
+ * @throws \moodle_exception
+ */
+ public function test_move_category_to_top(): void {
+ $this->setAdminUser();
+ $this->resetAfterTest();
+
+ // Create context for question categories.
+ $course = $this->create_course();
+ $context = context_course::instance($course->id);
+ $this->create_course_category();
+
+ // Question categories.
+ $qcat1 = $this->create_question_category_for_a_course($course);
+ $qcat2 = $this->create_question_category_for_a_course($course);
+ $qcat3 = $this->create_question_category_for_a_course($course);
+
+ // Check current order.
+ $currentorder = $this->get_current_order($context->id);
+ $expectedorder = [
+ $qcat1->id => [],
+ $qcat2->id => [],
+ $qcat3->id => [],
+ ];
+ $this->assertEquals($expectedorder, $currentorder);
+
+ // Move category 3 to the top.
+ $stateupdates = move_category::execute($context->id, $qcat3->id, $qcat3->parent);
+
+ $neworder = $this->get_current_order($context->id);
+ $newexpectedorder = [
+ $qcat3->id => [],
+ $qcat1->id => [],
+ $qcat2->id => [],
+ ];
+ $this->assertEquals($newexpectedorder, $neworder);
+
+ // Expecting all categories to have an updated sortorder.
+ $expectedstateupdates = [
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat3->id,
+ 'sortorder' => 1,
+ ],
+ ],
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat1->id,
+ 'sortorder' => 2,
+ ],
+ ],
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat2->id,
+ 'sortorder' => 3,
+ ],
+ ],
+ ];
+ $this->assertEquals($stateupdates, $expectedstateupdates);
+ }
+
+ /**
+ * Move a category to a new parent that doesn't currently have any children.
+ *
+ * @return void
+ */
+ public function test_move_category_as_new_child(): void {
+ $this->setAdminUser();
+ $this->resetAfterTest();
+
+ // Create context for question categories.
+ $course = $this->create_course();
+ $context = context_course::instance($course->id);
+ $this->create_course_category();
+
+ // Question categories.
+ $qcat1 = $this->create_question_category_for_a_course($course);
+ $qcat2 = $this->create_question_category_for_a_course($course);
+ $qcat3 = $this->create_question_category_for_a_course($course);
+
+ // Check current order.
+ $currentorder = $this->get_current_order($context->id);
+ $expectedorder = [
+ $qcat1->id => [],
+ $qcat2->id => [],
+ $qcat3->id => [],
+ ];
+ $this->assertEquals($expectedorder, $currentorder);
+
+ // Set Category 2 as the parent of Category 1.
+ $stateupdates = move_category::execute($context->id, $qcat1->id, $qcat2->id);
+
+ $neworder = $this->get_current_order($context->id);
+ $newexpectedorder = [
+ $qcat2->id => [
+ $qcat1->id => [],
+ ],
+ $qcat3->id => [],
+ ];
+ $this->assertEquals($newexpectedorder, $neworder);
+
+ // Expecting an update to the parent and sortorder of the moved category, and updated sortorders for the children
+ // of the original parent.
+ $expectedstateupdates = [
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat1->id,
+ 'sortorder' => 1,
+ 'parent' => $qcat2->id,
+ ],
+ ],
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat2->id,
+ 'sortorder' => 1,
+ ],
+ ],
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat3->id,
+ 'sortorder' => 2,
+ ],
+ ],
+ ];
+ $this->assertEquals($stateupdates, $expectedstateupdates);
+ }
+
+ /**
+ * Move a category from one parent to another that already has a child.
+ *
+ * @return void
+ * @throws \moodle_exception
+ */
+ public function test_move_category_between_parents(): void {
+ $this->setAdminUser();
+ $this->resetAfterTest();
+
+ // Create context for question categories.
+ $course = $this->create_course();
+ $context = context_course::instance($course->id);
+ $this->create_course_category();
+
+ // Question categories.
+ $qcat1 = $this->create_question_category_for_a_course($course);
+ $qcat2 = $this->create_question_category_for_a_course($course);
+ $qcat3 = $this->create_question_category_for_a_course($course, ['parent' => $qcat1->id]);
+ $qcat4 = $this->create_question_category_for_a_course($course, ['parent' => $qcat2->id]);
+
+ // Check current order.
+ $currentorder = $this->get_current_order($context->id);
+ $expectedorder = [
+ $qcat1->id => [
+ $qcat3->id => [],
+ ],
+ $qcat2->id => [
+ $qcat4->id => [],
+ ],
+ ];
+ $this->assertEquals($expectedorder, $currentorder);
+
+ // Set Category 2 as the parent of Category 1.
+ $stateupdates = move_category::execute($context->id, $qcat3->id, $qcat2->id, $qcat4->id);
+
+ $neworder = $this->get_current_order($context->id);
+ $newexpectedorder = [
+ $qcat1->id => [],
+ $qcat2->id => [
+ $qcat4->id => [],
+ $qcat3->id => [],
+ ],
+ ];
+ $this->assertEquals($newexpectedorder, $neworder);
+
+ // As there are no remaining children of the original parent, and this was moved to the bottom of the new parent,
+ // just the moved category is updated.
+ $expectedstateupdates = [
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat3->id,
+ 'sortorder' => 2,
+ 'parent' => $qcat2->id,
+ ],
+ ],
+ ];
+ $this->assertEquals($stateupdates, $expectedstateupdates);
+ }
+
+ /**
+ * Move a category from the top category of one context to another.
+ *
+ * @return void
+ */
+ public function test_move_category_between_contexts(): void {
+ $this->setAdminUser();
+ $this->resetAfterTest();
+
+ // Create context for question categories.
+ $course = $this->create_course();
+ $coursecontext = context_course::instance($course->id);
+ $coursecategory = $this->create_course_category();
+ $categorycontext = \context_coursecat::instance($coursecategory->id);
+
+ // Question categories.
+ $coursecatqcat = question_get_top_category($categorycontext->id, true);
+ $qcat1 = $this->create_question_category_for_a_course($course);
+ $qcat2 = $this->create_question_category_for_a_course($course);
+
+ $currentcourseorder = $this->get_current_order($coursecontext->id);
+ $expectedorder = [
+ $qcat1->id => [],
+ $qcat2->id => [],
+ ];
+ $this->assertEquals($expectedorder, $currentcourseorder);
+ $currentcoursecatorder = $this->get_current_order($categorycontext->id);
+ $expectedcoursecatorder = [
+ ];
+ $this->assertEquals($expectedcoursecatorder, $currentcoursecatorder);
+
+ $stateupdates = move_category::execute($coursecontext->id, $qcat1->id, $coursecatqcat->id);
+
+ $newcourseorder = $this->get_current_order($coursecontext->id);
+ $newexpectedorder = [
+ $qcat2->id => [],
+ ];
+ $this->assertEquals($newcourseorder, $newexpectedorder);
+ $newcoursecatorder = $this->get_current_order($categorycontext->id);
+ $newexpectedcoursecatorder = [
+ $qcat1->id => [],
+ ];
+ $this->assertEquals($newcoursecatorder, $newexpectedcoursecatorder);
+
+ // Expect an update to the sortorder, parent and context of the moved category.
+ // Since the original sibling is the only remaining child of a top-level category, it has its draghandle property
+ // updated as well as its sortorder.
+ $expectedstateupdates = [
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat1->id,
+ 'sortorder' => 1,
+ 'parent' => $coursecatqcat->id,
+ 'context' => $categorycontext->id,
+ ],
+ ],
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat2->id,
+ 'sortorder' => 1,
+ 'draghandle' => false,
+ ],
+ ],
+ ];
+ $this->assertEquals($stateupdates, $expectedstateupdates);
+ }
+
+ /**
+ * Move a category that has its own children.
+ *
+ * The children should move with the parent.
+ *
+ * @return void
+ */
+ public function test_move_category_with_children(): void {
+ $this->setAdminUser();
+ $this->resetAfterTest();
+
+ // Create context for question categories.
+ $course = $this->create_course();
+ $context = context_course::instance($course->id);
+ $this->create_course_category();
+
+ // Question categories.
+ $qcat1 = $this->create_question_category_for_a_course($course);
+ $qcat2 = $this->create_question_category_for_a_course($course);
+ $qcat3 = $this->create_question_category_for_a_course($course);
+ $qcat4 = $this->create_question_category_for_a_course($course, ['parent' => $qcat3->id]);
+ $qcat5 = $this->create_question_category_for_a_course($course, ['parent' => $qcat3->id]);
+
+ // Check current order.
+ $currentorder = $this->get_current_order($context->id);
+ $expectedorder = [
+ $qcat1->id => [],
+ $qcat2->id => [],
+ $qcat3->id => [
+ $qcat4->id => [],
+ $qcat5->id => [],
+ ],
+ ];
+ $this->assertEquals($expectedorder, $currentorder);
+
+ $stateupdates = move_category::execute($context->id, $qcat3->id, $qcat3->parent, $qcat1->id);
+
+ $neworder = $this->get_current_order($context->id);
+ $newexpectedorder = [
+ $qcat1->id => [],
+ $qcat3->id => [
+ $qcat4->id => [],
+ $qcat5->id => [],
+ ],
+ $qcat2->id => [],
+ ];
+ $this->assertEquals($neworder, $newexpectedorder);
+
+ // Update the sortorder of the moving category and the following sibling. No updates to the children are required.
+ $expectedstateupdates = [
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat3->id,
+ 'sortorder' => 2,
+ ],
+ ],
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat2->id,
+ 'sortorder' => 3,
+ ],
+ ],
+ ];
+ $this->assertEquals($stateupdates, $expectedstateupdates);
+ }
+
+ /**
+ * Move a category that has its own children to a new parent.
+ *
+ * The children should also move and become descendants of the new parent.
+ *
+ * @return void
+ */
+ public function test_change_parent_with_children(): void {
+ $this->setAdminUser();
+ $this->resetAfterTest();
+
+ // Create context for question categories.
+ $course = $this->create_course();
+ $context = context_course::instance($course->id);
+ $this->create_course_category();
+
+ // Question categories.
+ $qcat1 = $this->create_question_category_for_a_course($course);
+ $qcat2 = $this->create_question_category_for_a_course($course);
+ $qcat3 = $this->create_question_category_for_a_course($course);
+ $qcat4 = $this->create_question_category_for_a_course($course, ['parent' => $qcat3->id]);
+ $qcat5 = $this->create_question_category_for_a_course($course, ['parent' => $qcat4->id]);
+
+ // Check current order.
+ $currentorder = $this->get_current_order($context->id);
+ $expectedorder = [
+ $qcat1->id => [],
+ $qcat2->id => [],
+ $qcat3->id => [
+ $qcat4->id => [
+ $qcat5->id => [],
+ ],
+ ],
+ ];
+ $this->assertEquals($expectedorder, $currentorder);
+
+ $stateupdates = move_category::execute($context->id, $qcat4->id, $qcat2->id);
+
+ $neworder = $this->get_current_order($context->id);
+ $newexpectedorder = [
+ $qcat1->id => [],
+ $qcat2->id => [
+ $qcat4->id => [
+ $qcat5->id => [],
+ ],
+ ],
+ $qcat3->id => [],
+ ];
+ $this->assertEquals($newexpectedorder, $neworder);
+
+ // Expecting an update to the sortorder and parent of the moved category. No updates to the children are required.
+ $expectedstateupdates = [
+ (object)[
+ 'name' => 'categories',
+ 'action' => 'put',
+ 'fields' => (object)[
+ 'id' => $qcat4->id,
+ 'parent' => $qcat2->id,
+ 'sortorder' => 1,
+ ],
+ ],
+ ];
+ $this->assertEquals($expectedstateupdates, $stateupdates);
+ }
+}
diff --git a/question/bank/managecategories/tests/manage_category_test_base.php b/question/bank/managecategories/tests/manage_category_test_base.php
index d9e85c026053d..360dcfbd3ed7e 100644
--- a/question/bank/managecategories/tests/manage_category_test_base.php
+++ b/question/bank/managecategories/tests/manage_category_test_base.php
@@ -13,6 +13,7 @@
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
+
namespace qbank_managecategories;
use core_question\local\bank\question_edit_contexts;
diff --git a/question/bank/managecategories/tests/privacy/provider_test.php b/question/bank/managecategories/tests/privacy/provider_test.php
new file mode 100644
index 0000000000000..c7c5a2aab11b5
--- /dev/null
+++ b/question/bank/managecategories/tests/privacy/provider_test.php
@@ -0,0 +1,48 @@
+.
+
+namespace qbank_managecategories\privacy;
+
+use advanced_testcase;
+use core_privacy\local\request\writer;
+use qbank_managecategories\privacy\provider;
+
+/**
+ * Unit tests for qbank_managecategories privacy provider.
+ *
+ * @package qbank_managecategories
+ * @copyright 2021 Catalyst IT Australia Pty Ltd
+ * @author 2021, Ghaly Marc-Alexandre
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \qbank_managecategories\privacy\provider
+ */
+final class provider_test extends advanced_testcase {
+ /**
+ * Test to check export_user_preferences.
+ *
+ * @covers ::export_user_preferences
+ */
+ public function test_export_user_preferences(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ set_user_preference('qbank_managecategories_showdescriptions', 1, $user);
+ provider::export_user_preferences($user->id);
+ $writer = writer::with_context(\context_system::instance());
+ $prefs = $writer->get_user_preferences('qbank_managecategories');
+ $this->assertEquals(1, $prefs->showdescr->value);
+ $this->assertEquals(get_string('displaydescription', 'qbank_managecategories'), $prefs->showdescr->description);
+ }
+}
diff --git a/question/bank/managecategories/tests/question_categories_test.php b/question/bank/managecategories/tests/question_categories_test.php
index d0fb344af4e05..8af31f379dacc 100644
--- a/question/bank/managecategories/tests/question_categories_test.php
+++ b/question/bank/managecategories/tests/question_categories_test.php
@@ -13,6 +13,7 @@
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
+
namespace qbank_managecategories;
defined('MOODLE_INTERNAL') || die();
@@ -29,7 +30,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \qbank_managecategories\question_categories
*/
-class question_categories_test extends manage_category_test_base {
+final class question_categories_test extends manage_category_test_base {
/**
* Test get children.
*
diff --git a/question/bank/managecategories/version.php b/question/bank/managecategories/version.php
index 4ac7ad3771b68..71a116f2d0735 100644
--- a/question/bank/managecategories/version.php
+++ b/question/bank/managecategories/version.php
@@ -26,6 +26,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qbank_managecategories';
-$plugin->version = 2024042200;
+$plugin->version = 2024070400;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;