MDL-87264 qbank_managecategories: Move categories between banks

This restores the ability to move an entire category to a different
context,
which now means a different activity module.

The feature is accessed via the Move option in the category's action
menu,
which now has a "Switch bank" option like the "add from question bank"
UI
in the quiz.

When a category is moved to a different context, it will disappear from
the current page, and a link to the destination bank is displayed.
This commit is contained in:
Mark Johnson
2026-02-27 14:43:57 +00:00
parent 06a467e45c
commit 37b55c085c
14 changed files with 160 additions and 63 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
define("qbank_managecategories/categorymanager",["exports","core/reactive","core/str","qbank_managecategories/mutations","qbank_managecategories/events","core/ajax","core/notification","core_form/modalform"],(function(_exports,_reactive,_str,_mutations,_events,_ajax,_notification,_modalform){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.categorymanager=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_modalform=_interopRequireDefault(_modalform);const SELECTORS_CATEGORY_LIST=".qbank_managecategories-categorylist",SELECTORS_CONTEXT=".qbank_managecategories-categorylist[data-contextid]",SELECTORS_CATEGORY_ITEM=".qbank_managecategories-item[data-categoryid]",SELECTORS_CATEGORY_ROOT="#categoryroot",SELECTORS_SHOWDESCRIPTIONS_TOGGLE="#showdescriptions-toggle",SELECTORS_ADD_EDIT_BUTTON='[data-action="addeditcategory"]',CLASSES_DRAGHANDLE="draghandle",CLASSES_DANGER="alert-danger";class CategoryManager extends _reactive.Reactive{moveCategory(categoryId,targetParentId){let precedingSiblingId=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const call={methodname:"qbank_managecategories_move_category",args:{pagecontextid:this.state.page.contextid,categoryid:categoryId,targetparentid:targetParentId,precedingsiblingid:precedingSiblingId}};_ajax.default.call([call])[0].then((stateUpdates=>(this.stateManager.processUpdates(stateUpdates),stateUpdates))).catch((error=>{var _document$getElements;_notification.default.addNotification({message:error.message,type:"error"}),null===(_document$getElements=document.getElementsByClassName(CLASSES_DANGER)[0])||void 0===_document$getElements||_document$getElements.scrollIntoView()}))}getTitle(isEdit){return(0,_str.get_string)(isEdit?"editcategory":"addcategory","question")}getSave(isEdit){return isEdit?(0,_str.get_string)("savechanges","core"):(0,_str.get_string)("addcategory","question")}showEditModal(e){const addEditButton=e.target.closest(SELECTORS_ADD_EDIT_BUTTON);if(!addEditButton)return;if(!addEditButton.dataset.actiontype)return;e.preventDefault();const title=categorymanager.getTitle("edit"===addEditButton.dataset.actiontype),save=categorymanager.getSave("edit"===addEditButton.dataset.actiontype),cmid=addEditButton.dataset.cmid,courseid=addEditButton.dataset.courseid,questioncount=addEditButton.dataset.questioncount;let contextid=addEditButton.dataset.contextid,categoryid=null,sortorder=null,parent=null;const categoryItem=e.target.closest(SELECTORS_CATEGORY_ITEM);if(categoryItem){contextid=categoryItem.dataset.contextid,categoryid=categoryItem.dataset.categoryid,sortorder=categoryItem.dataset.sortorder;const parentContext=categoryItem.closest(SELECTORS_CONTEXT);parent=categoryItem.dataset.parent+","+parentContext.dataset.contextid}const modalForm=new _modalform.default({formClass:"qbank_managecategories\\form\\question_category_edit_form",args:{cmid:cmid,courseid:courseid,questioncount:questioncount,contextid:contextid,categoryid:categoryid,sortorder:sortorder,parent:parent},modalConfig:{title:title,large:!0},saveButtonText:save,returnFocus:addEditButton});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(response=>{categorymanager.stateManager.processUpdates(response.detail)})),modalForm.show()}updateCategoryName(categoryId,newName){categorymanager.stateManager.setReadOnly(!1),categorymanager.stateManager.processUpdate("categories","put",{id:categoryId,name:newName}),categorymanager.stateManager.setReadOnly(!0)}}const categorymanager=new CategoryManager({name:"qtype_managecategories_categorymanager",eventName:_events.eventTypes.qbankManagecategoriesStateUpdated,eventDispatch:_events.notifyQbankManagecategoriesStateUpdated,mutations:_mutations.mutations});_exports.categorymanager=categorymanager;_exports.init=()=>{(async reactive=>{const stateData={page:{contextid:document.querySelector(SELECTORS_CATEGORY_ROOT).dataset.contextid,showdescriptions:document.querySelector(SELECTORS_SHOWDESCRIPTIONS_TOGGLE).checked},categories:[],categoryLists:[]};document.querySelectorAll(SELECTORS_CATEGORY_ITEM).forEach((item=>{stateData.categories.push({id:item.dataset.categoryid,name:item.dataset.categoryname,parent:item.dataset.parent,contextid:item.dataset.contextid,sortorder:item.dataset.sortorder,draghandle:item.classList.contains(CLASSES_DRAGHANDLE)})})),document.querySelectorAll(SELECTORS_CATEGORY_LIST).forEach((categoryList=>{stateData.categoryLists.push({id:categoryList.dataset.categoryid,childCount:categoryList.querySelectorAll(SELECTORS_CATEGORY_ITEM).length})})),reactive.setInitialState(stateData)})(categorymanager)}}));
define("qbank_managecategories/categorymanager",["exports","core/reactive","core/str","qbank_managecategories/mutations","qbank_managecategories/events","core/ajax","core/notification","core_form/modalform"],(function(_exports,_reactive,_str,_mutations,_events,_ajax,_notification,_modalform){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.categorymanager=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_modalform=_interopRequireDefault(_modalform);const SELECTORS_CATEGORY_LIST=".qbank_managecategories-categorylist",SELECTORS_CONTEXT=".qbank_managecategories-categorylist[data-contextid]",SELECTORS_CATEGORY_ITEM=".qbank_managecategories-item[data-categoryid]",SELECTORS_CATEGORY_ROOT="#categoryroot",SELECTORS_SHOWDESCRIPTIONS_TOGGLE="#showdescriptions-toggle",SELECTORS_ADD_EDIT_BUTTON='[data-action="addeditcategory"]',CLASSES_DRAGHANDLE="draghandle",CLASSES_DANGER="alert-danger";class CategoryManager extends _reactive.Reactive{moveCategory(categoryId,targetParentId){let precedingSiblingId=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const call={methodname:"qbank_managecategories_move_category",args:{pagecontextid:this.state.page.contextid,categoryid:categoryId,targetparentid:targetParentId,precedingsiblingid:precedingSiblingId}};_ajax.default.call([call])[0].then((stateUpdates=>(this.stateManager.processUpdates(stateUpdates),stateUpdates))).catch((error=>{var _document$getElements;_notification.default.addNotification({message:error.message,type:"error"}),null===(_document$getElements=document.getElementsByClassName(CLASSES_DANGER)[0])||void 0===_document$getElements||_document$getElements.scrollIntoView()}))}getTitle(isEdit){return(0,_str.get_string)(isEdit?"editcategory":"addcategory","question")}getSave(isEdit){return isEdit?(0,_str.get_string)("savechanges","core"):(0,_str.get_string)("addcategory","question")}showEditModal(e){const addEditButton=e.target.closest(SELECTORS_ADD_EDIT_BUTTON);if(!addEditButton)return;if(!addEditButton.dataset.actiontype)return;e.preventDefault();const title=categorymanager.getTitle("edit"===addEditButton.dataset.actiontype),save=categorymanager.getSave("edit"===addEditButton.dataset.actiontype),cmid=addEditButton.dataset.cmid,courseid=addEditButton.dataset.courseid,questioncount=addEditButton.dataset.questioncount;let contextid=addEditButton.dataset.contextid,categoryid=null,sortorder=null,parent=null;const categoryItem=e.target.closest(SELECTORS_CATEGORY_ITEM);if(categoryItem){contextid=categoryItem.dataset.contextid,categoryid=categoryItem.dataset.categoryid,sortorder=categoryItem.dataset.sortorder;const parentContext=categoryItem.closest(SELECTORS_CONTEXT);parent=categoryItem.dataset.parent+","+parentContext.dataset.contextid}const modalForm=new _modalform.default({formClass:"qbank_managecategories\\form\\question_category_edit_form",args:{cmid:cmid,courseid:courseid,questioncount:questioncount,contextid:contextid,categoryid:categoryid,sortorder:sortorder,parent:parent},modalConfig:{title:title,large:!0},saveButtonText:save,returnFocus:addEditButton});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(response=>{categorymanager.stateManager.processUpdates(response.detail)})),modalForm.show()}updateCategoryName(categoryId,newName){categorymanager.stateManager.setReadOnly(!1),categorymanager.stateManager.processUpdate("categories","put",{id:categoryId,name:newName}),categorymanager.stateManager.setReadOnly(!0)}}const categorymanager=new CategoryManager({name:"qtype_managecategories_categorymanager",eventName:_events.eventTypes.qbankManagecategoriesStateUpdated,eventDispatch:_events.notifyQbankManagecategoriesStateUpdated,mutations:_mutations.mutations});_exports.categorymanager=categorymanager;_exports.init=()=>{(async reactive=>{const rootElement=document.querySelector(SELECTORS_CATEGORY_ROOT),stateData={page:{contextid:rootElement.dataset.contextid,courseid:rootElement.dataset.courseid,cmid:rootElement.dataset.cmid,showdescriptions:document.querySelector(SELECTORS_SHOWDESCRIPTIONS_TOGGLE).checked},categories:[],categoryLists:[]};document.querySelectorAll(SELECTORS_CATEGORY_ITEM).forEach((item=>{stateData.categories.push({id:item.dataset.categoryid,name:item.dataset.categoryname,parent:item.dataset.parent,contextid:item.dataset.contextid,sortorder:item.dataset.sortorder,draghandle:item.classList.contains(CLASSES_DRAGHANDLE)})})),document.querySelectorAll(SELECTORS_CATEGORY_LIST).forEach((categoryList=>{stateData.categoryLists.push({id:categoryList.dataset.categoryid,childCount:categoryList.querySelectorAll(SELECTORS_CATEGORY_ITEM).length})})),reactive.setInitialState(stateData)})(categorymanager)}}));
//# sourceMappingURL=categorymanager.min.js.map
File diff suppressed because one or more lines are too long
@@ -403,12 +403,24 @@ export default class extends BaseComponent {
return categories;
}
/**
* Recursively create a list of all valid destinations for a current category within a parent category.
*
* Each entry in the list represents moving the category "before" another category by default, but may also represent
* moving "after" another category, or "as a new child of" a parent category.
*
* @param {Object} categoryData A list of category data from getCategoryDataFromElements() or getCategoryDataFromRecords().
* @param {Number} movingCategoryId The ID of the category currently being moved.
* @return {Array<Object>}
* @return {Array<Object>} A list of objects representing valid move targets for the category. Each object has:
* movingcategoryid - The ID of the category we are moving.
* precedingsiblingid - The ID of the previous category under the same parent as this target. 0 if this is the first child.
* parent - The ID of the target category's parent category. 0 if this is the top category.
* categoryname - The name of the target category, to display as part of the destination.
* categories - An array of child category targets. If there are no children, this must be null
* to prevent infinite recursion in the template.
* newchild - If true, this destination is "as a new child of the parent".
* lastchild - If true, this destination is after the target category, rather than before.
*/
createMoveCategoryList(categoryData, movingCategoryId) {
const categories = [];
@@ -416,18 +428,16 @@ export default class extends BaseComponent {
let precedingSibling = null;
categoryData.forEach(category => {
// Don't create a target for the category that's moving.
if (category.categoryId === movingCategoryId) {
if (parseInt(category.categoryId) === movingCategoryId) {
return;
}
// Create a target to move before this child.
let child = {
categoryid: category.categoryId,
movingcategoryid: movingCategoryId,
precedingsiblingid: precedingSibling?.categoryId ?? 0,
parent: category.parentId,
categoryname: category.categoryName,
categories: null,
current: category.categoryId === movingCategoryId,
categories: null, // Prevent infinite recursion in the template.
};
if (category.children) {
// If the child has its own children, recursively make a list of those.
@@ -440,7 +450,7 @@ export default class extends BaseComponent {
precedingsiblingid: 0,
parent: category.categoryId,
categoryname: category.categoryName,
categories: null,
categories: null, // Prevent infinite recursion in the template.
newchild: true,
}
];
@@ -456,7 +466,7 @@ export default class extends BaseComponent {
precedingsiblingid: precedingSibling.categoryId,
parent: precedingSibling.parentId,
categoryname: precedingSibling.categoryName,
categories: null,
categories: null, // Prevent infinite recursion in the template.
lastchild: true,
});
}
@@ -55,6 +55,8 @@ const loadState = async(reactive) => {
const stateData = {
page: {
contextid: rootElement.dataset.contextid,
courseid: rootElement.dataset.courseid,
cmid: rootElement.dataset.cmid,
showdescriptions: document.querySelector(SELECTORS.SHOWDESCRIPTIONS_TOGGLE).checked,
},
categories: [],
@@ -72,7 +72,7 @@ class move_category extends external_api {
'id' => new external_value(PARAM_INT, 'The ID of the category that was updated.'),
'sortorder' => new external_value(PARAM_INT, 'The new sortorder', VALUE_OPTIONAL),
'parent' => new external_value(PARAM_INT, 'The ID of the new parent category.', VALUE_OPTIONAL),
'context' => new external_value(PARAM_INT, 'The ID of the new context.', VALUE_OPTIONAL),
'contextid' => new external_value(PARAM_INT, 'The ID of the new context.', VALUE_OPTIONAL),
'draghandle' => new external_value(
PARAM_BOOL,
'Should this category have a drag handle?',
@@ -151,7 +151,7 @@ class move_category extends external_api {
) {
$transaction->rollback(new moodle_exception('idnumberexists', 'qbank_managecategories'));
}
$originstateupdate->fields->context = $targetparent->contextid;
$originstateupdate->fields->contextid = $targetparent->contextid;
}
// Update sort order.
@@ -187,7 +187,10 @@ class move_category extends external_api {
$toupdatesortorder = $DB->get_records_select('question_categories', $select, $params, $sort);
foreach ($toupdatesortorder as $category) {
$DB->set_field('question_categories', 'sortorder', ++$sortorder, ['id' => $category->id]);
$stateupdates[] = self::make_state_update($category->id, sortorder: $sortorder);
if ($targetparent->contextid == $pagecontextid) {
// If we're updating other categories in the current context, return state updates for those too.
$stateupdates[] = self::make_state_update($category->id, sortorder: $sortorder);
}
}
if (isset($originstateupdate->fields->parent)) {
@@ -27,6 +27,7 @@ $string['after'] = 'After {$a}';
$string['before'] = 'Before {$a}';
$string['categoryincategory'] = 'Cannot move category in same category.';
$string['categorymoved'] = 'The category has been moved successfully.';
$string['categorymovedto'] = 'Category moved to <a href="{$a->url}">{$a->name}</a>.';
$string['confirmdelete'] = 'Delete category "{$a}"?. Any child categories will be moved to this category\'s parent.';
$string['descriptionnotdisplayed'] = 'Categories description not displayed';
$string['displaydescription'] = 'Categories description displayed';
@@ -21,6 +21,8 @@
Context variables required for this template:
* contextid - Context id for js init.
* courseid - The course the current module context sits within.
* cmid - The module the context belongs to.
* categoriesrendered - Array representing categories data rendered.
* items - Categories data.
* heading - Category's heading for each context (ie: Course or Quiz).
@@ -29,6 +31,8 @@
Example context (json):
{
"contextid": "18",
"courseid": "2",
"cmid": "10",
"categoriesrendered": [{
"items": [{
"categoryid": "1",
@@ -43,14 +47,16 @@
"iconright": "<i class=\"icon fa fa-arrow-right fa-fw \" title=\"Make child of 'category'\" role=\"img\" aria-label=\"Make child of 'category'\"></i>",
"children": ""
}],
"heading": "Question categories for 'Quiz: qz'",
"ctxlvl": "contextlevel70"
"heading": "Question categories for 'Quiz: qz'"
}]
}
}}
<div id="categoryroot"
class="editing col-11 col-md-12 mt-3 ps-0{{#showdescriptions}} showdescriptions{{/showdescriptions}}"
data-contextid="{{{contextid}}}">
data-contextid="{{{contextid}}}"
data-courseid="{{courseid}}"
data-cmid="{{cmid}}"
>
{{#categoriesrendered}}
<h3 class="context-heading">{{heading}}</h3>
<div class="box py-3 boxwidthwide boxaligncenter generalbox questioncategories">
@@ -24,56 +24,94 @@
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.
* categoryid - The ID of the category to move relative to.
* movingcategoryid - The ID of the category we are moving.
* categoryname - The name of the category to move relative to.
* lastchild - Is this the last child of its parent? If true, an "after X" option will be rendered.
* precedingsiblingid - The ID of the preceding category under the same parent.
* parent - The ID of the parent category.
* newchild - If true, render this as a "new child of X" option rather than "before X".
* categories - Array containing child categories of this category.
If there are none, this must be set null or [] to avoid infinite recursion.
If there are none, this must be to [] 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,
"categoryid": "154001",
"movingcategoryid": 154007,
"precedingsiblingid": 0,
"parent": "154000",
"categoryname": "Default for Qbank 1",
"categories": [
{
"categoryname": "Test category 4",
"categoryid": 7,
"firstchild": true,
"current": false,
"hascategories": false,
"categories": []
"movingcategoryid": 154007,
"precedingsiblingid": 0,
"parent": "154001",
"categoryname": "Default for Qbank 1",
"categories": [],
"newchild": true
}
]
},
{
"categoryid": "154004",
"movingcategoryid": 154007,
"precedingsiblingid": "154001",
"parent": "154000",
"categoryname": "Course category 1",
"categories": [
{
"categoryid": "154006",
"movingcategoryid": 154007,
"precedingsiblingid": 0,
"parent": "154004",
"categoryname": "Course category 3",
"categories": [
{
"movingcategoryid": 154007,
"precedingsiblingid": 0,
"parent": "154006",
"categoryname": "Course category 3",
"categories": [],
"newchild": true
}
]
},
{
"movingcategoryid": 154007,
"precedingsiblingid": "154006",
"parent": "154004",
"categoryname": "Course category 3",
"categories": [],
"lastchild": true
}
]
},
{
"categoryid": "154005",
"movingcategoryid": 154007,
"precedingsiblingid": "154004",
"parent": "154000",
"categoryname": "Course category 2",
"categories": [
{
"movingcategoryid": 154007,
"precedingsiblingid": 0,
"parent": "154005",
"categoryname": "Course category 2",
"categories": [],
"newchild": true
}
]
},
{
"movingcategoryid": 154007,
"precedingsiblingid": "154005",
"parent": "154000",
"categoryname": "Course category 2",
"categories": [],
"lastchild": true
}
]
}
@@ -27,6 +27,7 @@
Example context (json):
{
"contexid": 1,
"cmid": 1,
"contextname": "Question bank: Qbank 1",
"categories": [
{
@@ -50,7 +51,7 @@
}}
<div role="tree">
<div class="d-flex">
<h3>{{{contextname}}}</h3>
<h3 class="bank-header" data-cmid="{{cmid}}">{{{contextname}}}</h3>
<button data-action="switch-question-bank" type="button" class="btn btn-secondary ms-auto mb-2">
{{#str}}switchbank, core_question{{/str}}
</button>
@@ -104,3 +104,34 @@ Feature: A teacher can reorder question categories
And I should not see "+" in the "Course category 2" "list_item"
When I drag "Course category 3" "list_item" and I drop it in "Course category 1" "text"
Then I should see "+" in the "Course category 1" "list_item"
Scenario: Teacher can move a category to another bank
Given the following "question categories" exist:
| contextlevel | reference | name | idnumber | questioncategory |
| Activity module | qbank1 | Course category 4 | questioncat4 | Course category 1 |
And I reload the page
And "Default for Qbank 1" "list_item" should exist
And "Course category 1" "list_item" should exist
And "Course category 2" "list_item" should exist
And "Course category 3" "list_item" should exist
And "Course category 4" "list_item" should exist
And I open the action menu in "Course category 1" "list_item"
And I choose "Move" in the open action menu
And I should see "Question bank: Qbank 1" in the "Move Course category 1" "dialogue"
And I should see "Before Course category 3" in the "Move Course category 1" "dialogue"
And I press "Switch bank"
And I click on "Qbank 2" "link" in the "Select question bank" "dialogue"
And I should see "Question bank: Qbank 2" in the "Move Course category 1" "dialogue"
And I should not see "Before Course category 3" in the "Move Course category 1" "dialogue"
And I should see "As new child of Default for Qbank 2" in the "Move Course category 1" "dialogue"
When I follow "As new child of Default for Qbank 2"
Then "Default for Qbank 1" "list_item" should exist
And "Course category 1" "list_item" should not exist
And "Course category 2" "list_item" should exist
And "Course category 3" "list_item" should exist
And "Course category 4" "list_item" should not exist
And I should see "Category moved to Question bank: Qbank 2"
And I follow "Question bank: Qbank 2"
And "Default for Qbank 2" "list_item" should exist
And "Course category 1" "list_item" should exist
And "Course category 4" "list_item" should exist
@@ -18,6 +18,7 @@ namespace qbank_managecategories\external;
use context;
use context_module;
use core\context\module;
use moodle_url;
use qbank_managecategories\question_categories;
@@ -39,12 +40,12 @@ final class move_category_test extends \qbank_managecategories\manage_category_t
/**
* Return order of categories for a given context.
*
* @param context $context The context to get the category order for.
* @param module $context The context to get the category order for.
* @return array Nested array, keyed by category IDs.
*/
private function get_current_order(context $context): array {
$categories = new question_categories(new moodle_url('/'), [$context]);
return $this->reduce_tree($categories->editlists[$context->id]->items);
private function get_current_order(module $context): array {
$categories = new question_categories(new moodle_url('/'), cmid: $context->instanceid);
return $this->reduce_tree($categories->editlist->items);
}
/**
@@ -127,7 +128,7 @@ final class move_category_test extends \qbank_managecategories\manage_category_t
],
],
];
$this->assertEquals($stateupdates, $expectedstateupdates);
$this->assertEquals($expectedstateupdates, $stateupdates);
}
/**
@@ -25,14 +25,18 @@ use core\router\parameters\query_coursemodule;
use core\router\require_login;
use core\router\route;
use core\router\schema\example;
use core\router\schema\objects\array_of_strings;
use core\router\schema\objects\array_of_things;
use core\router\schema\objects\schema_object;
use core\router\schema\parameters\path_parameter;
use core\router\schema\parameters\query_parameter;
use core\router\schema\response\content\json_media_type;
use core\router\schema\response\payload_response;
use core\router\schema\response\response;
use core_question\local\bank\formatted_bank;
use core_question\local\bank\question_bank_helper;
use core_question\local\bank\question_edit_contexts;
use core_question\local\bank\question_version_status;
use core_question\output\question_category_selector;
use core_question\question_category;
use Psr\Http\Message\ResponseInterface;