mirror of
https://github.com/moodle/moodle.git
synced 2025-03-23 17:10:20 +01:00
MDL-71378 qbank_bulkmove: refactor for moving to shared question banks
This commit is contained in:
parent
746efe94eb
commit
a6c4ddada5
2
lib/amd/build/form-autocomplete.min.js
vendored
2
lib/amd/build/form-autocomplete.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -490,6 +490,8 @@ define([
|
||||
|
||||
/**
|
||||
* Rebuild the list of suggestions based on the current values in the select list, and the query.
|
||||
* Any options in the original select with [data-enabled=disabled] will not be included
|
||||
* as a suggestion option in the enhanced field.
|
||||
*
|
||||
* @method updateSuggestions
|
||||
* @private
|
||||
@ -510,7 +512,7 @@ define([
|
||||
// Used to track if we found any visible suggestions.
|
||||
var matchingElements = false;
|
||||
// Options is used by the context when rendering the suggestions from a template.
|
||||
var suggestions = rebuildOptions(originalSelect.children('option:not(:selected)'), true);
|
||||
var suggestions = rebuildOptions(originalSelect.children('option:not(:selected, [data-enabled="disabled"])'), true);
|
||||
|
||||
// Re-render the list of suggestions.
|
||||
var searchquery = state.caseSensitive ? query : query.toLocaleLowerCase();
|
||||
|
@ -3270,6 +3270,12 @@ $functions = array(
|
||||
'type' => 'write',
|
||||
'ajax' => true,
|
||||
],
|
||||
'core_question_move_questions' => [
|
||||
'classname' => '\core_question\external\move_questions',
|
||||
'description' => 'Bulk move questions to a new category.',
|
||||
'type' => 'write',
|
||||
'ajax' => true,
|
||||
],
|
||||
);
|
||||
|
||||
$services = array(
|
||||
|
11
question/amd/build/repository.min.js
vendored
Normal file
11
question/amd/build/repository.min.js
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
define("core_question/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
|
||||
/**
|
||||
* A javascript module to handle core_question ajax actions.
|
||||
*
|
||||
* @module core_question/repository
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Simon Adams <simon.adams@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.moveQuestions=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.moveQuestions=function(newContextId,newCategoryId,questionIds){let returnUrl=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"";return _ajax.default.call([{methodname:"core_question_move_questions",args:{newcontextid:newContextId,newcategoryid:newCategoryId,questionids:questionIds,returnurl:returnUrl}}])[0]}}));
|
||||
|
||||
//# sourceMappingURL=repository.min.js.map
|
1
question/amd/build/repository.min.js.map
Normal file
1
question/amd/build/repository.min.js.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to handle core_question ajax actions.\n *\n * @module core_question/repository\n * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}\n * @author Simon Adams <simon.adams@catalyst-eu.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * @param {integer} newContextId target bank context id\n * @param {integer} newCategoryId target question category id\n * @param {string} questionIds questionIds comma separated list of question ids to move.\n * @param {string} returnUrl optional url to add/update the filter param with the new category id\n * @return {*}\n */\nexport const moveQuestions = (\n newContextId,\n newCategoryId,\n questionIds,\n returnUrl = '',\n) => Ajax.call([{\n methodname: 'core_question_move_questions',\n args: {\n newcontextid: newContextId,\n newcategoryid: newCategoryId,\n questionids: questionIds,\n returnurl: returnUrl,\n },\n}])[0];\n"],"names":["newContextId","newCategoryId","questionIds","returnUrl","Ajax","call","methodname","args","newcontextid","newcategoryid","questionids","returnurl"],"mappings":";;;;;;;;sKAiC6B,SACzBA,aACAC,cACAC,iBACAC,iEAAY,UACXC,cAAKC,KAAK,CAAC,CACZC,WAAY,+BACZC,KAAM,CACFC,aAAcR,aACdS,cAAeR,cACfS,YAAaR,YACbS,UAAWR,cAEf"}
|
47
question/amd/src/repository.js
Normal file
47
question/amd/src/repository.js
Normal file
@ -0,0 +1,47 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* A javascript module to handle core_question ajax actions.
|
||||
*
|
||||
* @module core_question/repository
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Simon Adams <simon.adams@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import Ajax from 'core/ajax';
|
||||
|
||||
/**
|
||||
* @param {integer} newContextId target bank context id
|
||||
* @param {integer} newCategoryId target question category id
|
||||
* @param {string} questionIds questionIds comma separated list of question ids to move.
|
||||
* @param {string} returnUrl optional url to add/update the filter param with the new category id
|
||||
* @return {*}
|
||||
*/
|
||||
export const moveQuestions = (
|
||||
newContextId,
|
||||
newCategoryId,
|
||||
questionIds,
|
||||
returnUrl = '',
|
||||
) => Ajax.call([{
|
||||
methodname: 'core_question_move_questions',
|
||||
args: {
|
||||
newcontextid: newContextId,
|
||||
newcategoryid: newCategoryId,
|
||||
questionids: questionIds,
|
||||
returnurl: returnUrl,
|
||||
},
|
||||
}])[0];
|
3
question/bank/bulkmove/amd/build/modal_question_bank_bulkmove.min.js
vendored
Normal file
3
question/bank/bulkmove/amd/build/modal_question_bank_bulkmove.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
333
question/bank/bulkmove/amd/src/modal_question_bank_bulkmove.js
Normal file
333
question/bank/bulkmove/amd/src/modal_question_bank_bulkmove.js
Normal file
@ -0,0 +1,333 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Contain the logic for the bulkmove questions modal.
|
||||
*
|
||||
* @module qbank_bulkmove/modal_question_bank_bulkmove
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Simon Adams <simon.adams@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import Modal from 'core/modal';
|
||||
import * as Fragment from 'core/fragment';
|
||||
import {getString} from 'core/str';
|
||||
import AutoComplete from 'core/form-autocomplete';
|
||||
import {moveQuestions} from 'core_question/repository';
|
||||
import Templates from 'core/templates';
|
||||
import Notification from 'core/notification';
|
||||
|
||||
|
||||
export default class ModalQuestionBankBulkmove extends Modal {
|
||||
static TYPE = 'qbank_bulkmove/bulkmove';
|
||||
|
||||
static SELECTORS = {
|
||||
SAVE_BUTTON: '[data-action="bulkmovesave"]',
|
||||
SELECTED_QUESTIONS: 'table#categoryquestions input[id^="checkq"]',
|
||||
SEARCH_BANK: '#searchbanks',
|
||||
SEARCH_CATEGORY: '#searchcategories',
|
||||
CATEGORY_OPTIONS: '#searchcategories option',
|
||||
BANK_OPTIONS: '#searchbanks option',
|
||||
CATEGORY_ENHANCED_INPUT: '.search-categories input',
|
||||
ORIGINAL_SELECTS: 'select.bulk-move',
|
||||
CATEGORY_WARNING: '#searchcatwarning',
|
||||
CATEGORY_SUGGESTION: '.search-categories span.form-autocomplete-downarrow',
|
||||
CONFIRM_BUTTON: '.bulk-move-footer button[data-action="save"]',
|
||||
CANCEL_BUTTON: '.bulk-move-footer button[data-action="cancel"]'
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {integer} contextId The current bank context id.
|
||||
* @param {integer} categoryId The current question category id.
|
||||
*/
|
||||
static init(contextId, categoryId) {
|
||||
document.addEventListener('click', (e) => {
|
||||
const trigger = e.target;
|
||||
if (trigger.className === 'dropdown-item' && trigger.getAttribute('name') === 'move') {
|
||||
e.preventDefault();
|
||||
ModalQuestionBankBulkmove.create({
|
||||
contextId,
|
||||
title: getString('bulkmoveheader', 'qbank_bulkmove'),
|
||||
show: true,
|
||||
categoryId: categoryId,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the initialised config on the class.
|
||||
*
|
||||
* @param {Object} modalConfig
|
||||
*/
|
||||
configure(modalConfig) {
|
||||
this.contextId = modalConfig.contextId;
|
||||
this.targetBankContextId = modalConfig.contextId;
|
||||
this.initSelectedCategoryId(modalConfig.categoryId);
|
||||
modalConfig.removeOnClose = true;
|
||||
super.configure(modalConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the category select based on the data passed to the JS or if a filter is applied in the url.
|
||||
* @param {integer} categoryId
|
||||
*/
|
||||
initSelectedCategoryId(categoryId) {
|
||||
const filter = new URLSearchParams(window.location.href).get('filter');
|
||||
if (filter) {
|
||||
const filteredCategoryId = JSON.parse(filter)?.category.values[0];
|
||||
this.currentCategoryId = filteredCategoryId > 0 ? filteredCategoryId : null;
|
||||
this.targetCategoryId = filteredCategoryId;
|
||||
return;
|
||||
}
|
||||
this.currentCategoryId = categoryId;
|
||||
this.targetCategoryId = categoryId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal contents.
|
||||
* @return {Promise}
|
||||
*/
|
||||
show() {
|
||||
void this.display(this.contextId, this.currentCategoryId);
|
||||
return super.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content to display and enhance the selects into auto complete fields.
|
||||
* @param {integer} currentBankContextId
|
||||
* @param {integer} currentCategoryId
|
||||
*/
|
||||
async display(currentBankContextId, currentCategoryId) {
|
||||
this.bodyPromise = await Fragment.loadFragment(
|
||||
'qbank_bulkmove',
|
||||
'bulk_move',
|
||||
currentBankContextId,
|
||||
{
|
||||
'categoryid': currentCategoryId,
|
||||
}
|
||||
);
|
||||
|
||||
await this.setBody(this.bodyPromise);
|
||||
await this.enhanceSelects(document.querySelectorAll(ModalQuestionBankBulkmove.SELECTORS.ORIGINAL_SELECTS));
|
||||
this.registerEnhancedEventListeners();
|
||||
this.mapCategoryContextIds();
|
||||
this.updateSaveButtonState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register event listeners on the enhanced selects. Must be done after they have been enhanced.
|
||||
*/
|
||||
registerEnhancedEventListeners() {
|
||||
document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_CATEGORY).addEventListener("change", (e) => {
|
||||
const targetCategoryId = e.currentTarget.value;
|
||||
this.targetCategoryId = targetCategoryId;
|
||||
this.rebuildOptions(this.targetBankContextId, targetCategoryId);
|
||||
this.updateSaveButtonState();
|
||||
});
|
||||
|
||||
document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SEARCH_BANK).addEventListener("change", (e) => {
|
||||
const selectedBankContextId = e.currentTarget.value;
|
||||
this.targetBankContextId = selectedBankContextId;
|
||||
this.rebuildOptions(selectedBankContextId, this.targetCategoryId);
|
||||
});
|
||||
|
||||
this.getModal().on("click", ModalQuestionBankBulkmove.SELECTORS.SAVE_BUTTON, (e) => {
|
||||
e.preventDefault();
|
||||
void this.displayConfirmMove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a map, so we can determine which bank belongs to which category.
|
||||
*/
|
||||
mapCategoryContextIds() {
|
||||
const customSelectCategoryOptions = document.querySelectorAll(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_OPTIONS);
|
||||
|
||||
if (customSelectCategoryOptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryContextIds = [];
|
||||
|
||||
customSelectCategoryOptions.forEach((option) => {
|
||||
categoryContextIds[option.value] = option.dataset.bankContextid;
|
||||
});
|
||||
|
||||
this.categoryContextIds = categoryContextIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the body with a confirmation prompt and set confirm cancel buttons in the footer.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async displayConfirmMove() {
|
||||
this.setTitle(getString('confirm', 'core'));
|
||||
this.setBody(getString('confirmmove', 'qbank_bulkmove'));
|
||||
if (!this.hasFooterContent()) {
|
||||
// We don't have the footer yet so go grab it and register event listeners on the buttons.
|
||||
this.setFooter(Templates.render('qbank_bulkmove/bulk_move_footer', {}));
|
||||
await this.getFooterPromise();
|
||||
|
||||
document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CONFIRM_BUTTON).addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.moveQuestionsAfterConfirm(this.targetBankContextId, this.targetCategoryId);
|
||||
});
|
||||
|
||||
document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CANCEL_BUTTON).addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.setTitle(getString('bulkmoveheader', 'qbank_bulkmove'));
|
||||
this.setBodyContent(Templates.renderForPromise('core/loading', {}));
|
||||
this.hideFooter();
|
||||
this.display(this.targetBankContextId, this.targetCategoryId);
|
||||
});
|
||||
} else {
|
||||
// We already have a footer so just show it.
|
||||
this.showFooter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically update all enhanced selects options based on what is selected.
|
||||
*
|
||||
* @param {integer} selectedBankContextId
|
||||
* @param {integer} selectedCategoryId
|
||||
*/
|
||||
rebuildOptions(selectedBankContextId, selectedCategoryId) {
|
||||
const categoryContextIds = this.categoryContextIds;
|
||||
const customSelectCategoryOptions = document.querySelectorAll(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_OPTIONS);
|
||||
|
||||
// Disable the category selector if no bank selected.
|
||||
if (!selectedBankContextId) {
|
||||
this.updateCategorySelectorState(false);
|
||||
} else {
|
||||
// Mark to be disabled all the categories not belonging to the selected bank.
|
||||
// This will then be handled by the enhanced selects event handlers.
|
||||
customSelectCategoryOptions.forEach((option) => {
|
||||
if (option.dataset.bankContextid != selectedBankContextId) {
|
||||
option.dataset.enabled = 'disabled';
|
||||
} else {
|
||||
option.dataset.enabled = 'enabled';
|
||||
}
|
||||
});
|
||||
this.updateCategorySelectorState(true);
|
||||
}
|
||||
|
||||
// De-select the selected category if it does not belong to the selected bank.
|
||||
if (selectedCategoryId && selectedBankContextId && categoryContextIds[selectedCategoryId] != selectedBankContextId) {
|
||||
const selectedCategoryElement = document.querySelector(
|
||||
'.search-categories span[role="option"][data-value="' + selectedCategoryId + '"]'
|
||||
);
|
||||
selectedCategoryElement.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable/enable the enhanced category selector field.
|
||||
* @param {boolean} toEnable True to enable, false to disable the field.
|
||||
*/
|
||||
updateCategorySelectorState(toEnable) {
|
||||
const warning = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_WARNING);
|
||||
const enhancedInput = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_ENHANCED_INPUT);
|
||||
const suggestionButton = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.CATEGORY_SUGGESTION);
|
||||
|
||||
if (toEnable) {
|
||||
warning.classList.add('d-none');
|
||||
enhancedInput.removeAttribute('disabled');
|
||||
suggestionButton.classList.remove('d-none');
|
||||
} else {
|
||||
warning.classList.remove('d-none');
|
||||
enhancedInput.setAttribute('disabled', 'disabled');
|
||||
suggestionButton.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the button if the selected category is the same as the one the questions already belong to. Enable it otherwise.
|
||||
*/
|
||||
updateSaveButtonState() {
|
||||
const saveButton = document.querySelector(ModalQuestionBankBulkmove.SELECTORS.SAVE_BUTTON);
|
||||
const targetCategoryId = this.targetCategoryId;
|
||||
|
||||
if (targetCategoryId && targetCategoryId != this.currentCategoryId) {
|
||||
saveButton.removeAttribute('disabled');
|
||||
} else {
|
||||
saveButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the selected questions to their new target category.
|
||||
* @param {integer} targetContextId the target bank context id.
|
||||
* @param {integer} targetCategoryId the target question category id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async moveQuestionsAfterConfirm(targetContextId, targetCategoryId) {
|
||||
await this.setBody(Templates.render('core/loading', {}));
|
||||
const qelements = document.querySelectorAll(ModalQuestionBankBulkmove.SELECTORS.SELECTED_QUESTIONS);
|
||||
const questionids = [];
|
||||
qelements.forEach((element) => {
|
||||
if (element.checked) {
|
||||
const name = element.getAttribute('name');
|
||||
questionids.push(name.substr(1, name.length));
|
||||
}
|
||||
});
|
||||
if (questionids.length === 0) {
|
||||
await Notification.exception('No questions selected');
|
||||
}
|
||||
|
||||
try {
|
||||
window.location.href = await moveQuestions(
|
||||
targetContextId,
|
||||
targetCategoryId,
|
||||
questionids.join(),
|
||||
window.location.href
|
||||
);
|
||||
} catch (error) {
|
||||
await Notification.exception(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take the provided select options and enhance them into auto-complete fields.
|
||||
* @param {NodeList} selects Custom select elements to enhance.
|
||||
* @return {Promise<Promise[]>}
|
||||
*/
|
||||
async enhanceSelects(selects) {
|
||||
const placeholder = await getString('searchbyname', 'mod_quiz');
|
||||
const enhanced = [];
|
||||
|
||||
if (selects.length > 0) {
|
||||
for (let i = 0; i < selects.length; i++) {
|
||||
enhanced.push(AutoComplete.enhance(
|
||||
selects.item(i),
|
||||
false,
|
||||
'',
|
||||
placeholder,
|
||||
false,
|
||||
true,
|
||||
'',
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(enhanced);
|
||||
}
|
||||
|
||||
return Promise.reject('No selects to enhance');
|
||||
}
|
||||
}
|
@ -16,6 +16,8 @@
|
||||
|
||||
namespace qbank_bulkmove;
|
||||
|
||||
use moodle_exception;
|
||||
|
||||
/**
|
||||
* Class bulk_move_action is the base class for moving questions.
|
||||
*
|
||||
@ -44,4 +46,31 @@ class bulk_move_action extends \core_question\local\bank\bulk_action_base {
|
||||
'moodle/question:add',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the modal js with the current bank context id and question category id.
|
||||
* @return void
|
||||
*/
|
||||
public function initialise_javascript(): void {
|
||||
global $PAGE;
|
||||
|
||||
$category = $this->qbank->get_pagevars('cat');
|
||||
|
||||
if (!empty($category)) {
|
||||
[$categoryid, $contextid] = explode(',', $category);
|
||||
} else {
|
||||
$defaultcategory = question_get_default_category($this->qbank->cm->context->id, true);
|
||||
$categoryid = $defaultcategory->id;
|
||||
$contextid = $defaultcategory->contextid;
|
||||
}
|
||||
|
||||
$PAGE->requires->js_call_amd(
|
||||
'qbank_bulkmove/modal_question_bank_bulkmove',
|
||||
'init',
|
||||
[
|
||||
'contextid' => $contextid,
|
||||
'categoryid' => $categoryid,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -33,9 +33,10 @@ class helper {
|
||||
* @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;
|
||||
global $DB, $CFG;
|
||||
require_once($CFG->libdir .'/questionlib.php');
|
||||
if ($questionids = explode(',', $movequestionselected)) {
|
||||
list($usql, $params) = $DB->get_in_or_equal($questionids);
|
||||
[$usql, $params] = $DB->get_in_or_equal($questionids);
|
||||
$sql = "SELECT q.*, c.contextid
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
@ -58,8 +59,17 @@ class helper {
|
||||
* @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.
|
||||
* @deprecated since Moodle 5.0.
|
||||
* @todo MDL-82413 Final deprecation in Moodle 6.0.
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
replacement: 'replaced by a modal and webservice.
|
||||
See qbank_bulkmove/modal_question_bank_bulkmove and core_question_external\move_questions',
|
||||
since: '5.0',
|
||||
mdl: 'MDL-71378'
|
||||
)]
|
||||
public static function get_displaydata(array $addcontexts, \moodle_url $moveurl, \moodle_url $returnurl): array {
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
$displaydata = [];
|
||||
$displaydata ['categorydropdown'] = \qbank_managecategories\helper::question_category_select_menu($addcontexts,
|
||||
false, 0, '', -1, true);
|
||||
|
141
question/bank/bulkmove/classes/output/bulk_move.php
Normal file
141
question/bank/bulkmove/classes/output/bulk_move.php
Normal file
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Output class file.
|
||||
*
|
||||
* @package qbank_bulkmove
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Simon Adams <simon.adams@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace qbank_bulkmove\output;
|
||||
|
||||
use cm_info;
|
||||
use core_question\local\bank\question_bank_helper;
|
||||
use moodle_url;
|
||||
use renderer_base;
|
||||
use single_button;
|
||||
|
||||
/**
|
||||
* Output class to create a modal template with selects for question banks, question categories, and a move button.
|
||||
*/
|
||||
class bulk_move implements \renderable, \templatable {
|
||||
|
||||
/** @var int The question bank id you are currently moving the question(s) from */
|
||||
protected int $currentbankid;
|
||||
|
||||
/** @var int The question category id you are moving the question(s) from */
|
||||
protected int $currentcategoryid;
|
||||
|
||||
/**
|
||||
* Instantiate the output class.
|
||||
*
|
||||
* @param int $currentbankid
|
||||
* @param int $currentcategoryid
|
||||
*/
|
||||
public function __construct(int $currentbankid, int $currentcategoryid) {
|
||||
$this->currentbankid = $currentbankid;
|
||||
$this->currentcategoryid = $currentcategoryid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export data for use by the template.
|
||||
*
|
||||
* @param renderer_base $output
|
||||
* @return array
|
||||
*/
|
||||
public function export_for_template(renderer_base $output) {
|
||||
|
||||
[, $cmrec] = get_module_from_cmid($this->currentbankid);
|
||||
$currentbank = cm_info::create($cmrec);
|
||||
|
||||
// Get all shared banks and categories and make the current bank/category pre-selected, i.e. ordered first in the list.
|
||||
$bankstorender = question_bank_helper::get_activity_instances_with_shareable_questions(
|
||||
[],
|
||||
[],
|
||||
['moodle/question:add'],
|
||||
true,
|
||||
$this->currentbankid
|
||||
);
|
||||
|
||||
$allcategories = array_map(function($bank) {
|
||||
if ($bank->modid == $this->currentbankid) {
|
||||
// If this is the current bank then sort the categories so that our current categoryid is first in the list.
|
||||
$this->sort_categories($bank->questioncategories, $this->currentcategoryid);
|
||||
}
|
||||
return $bank->questioncategories;
|
||||
}, $bankstorender);
|
||||
|
||||
// The current bank is not a shared bank, but grab the category records anyway so that we can at least allow them
|
||||
// to be moved to another local category in the bank.
|
||||
if (!plugin_supports('mod', $currentbank->modname, FEATURE_PUBLISHES_QUESTIONS, false)) {
|
||||
$currentbank = question_bank_helper::get_activity_instances_with_private_questions(
|
||||
incourseids: [$currentbank->course],
|
||||
getcategories: true,
|
||||
currentbankid: $this->currentbankid,
|
||||
)[0];
|
||||
$currentbankcats = $currentbank->questioncategories;
|
||||
// Move the current category to the top of the list.
|
||||
$this->sort_categories($currentbankcats, $this->currentcategoryid);
|
||||
// Add the current bank categories to the front of the categories list.
|
||||
array_unshift($allcategories, $currentbankcats);
|
||||
// Add the current bank to the front of the banks list.
|
||||
array_unshift($bankstorender, $currentbank);
|
||||
}
|
||||
|
||||
// Flatten all the categories into a 2D array.
|
||||
$allcategories = array_merge(...array_values($allcategories));
|
||||
|
||||
$savebutton = new single_button(
|
||||
new moodle_url('#'),
|
||||
get_string('movequestions', 'qbank_bulkmove'),
|
||||
'post',
|
||||
single_button::BUTTON_PRIMARY,
|
||||
[
|
||||
'data-action' => 'bulkmovesave',
|
||||
'disabled' => 'disabled',
|
||||
]
|
||||
);
|
||||
|
||||
return [
|
||||
'allsharedbanks' => $bankstorender,
|
||||
'allcategories' => $allcategories,
|
||||
'save' => $savebutton->export_for_template($output),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapped usort to move the currentcategoryid to the top of the list of question categories.
|
||||
*
|
||||
* @param array $categories categories to sort
|
||||
* @param int $currentcategoryid the category to be sorted to the top of the list
|
||||
* @return void
|
||||
*/
|
||||
protected function sort_categories(array &$categories, int $currentcategoryid): void {
|
||||
usort($categories, static function($categorya, $categoryb) use ($currentcategoryid) {
|
||||
if ($categorya->id != $currentcategoryid && $categoryb->id == $currentcategoryid) {
|
||||
return 1;
|
||||
}
|
||||
if ($categorya->id == $currentcategoryid && $categoryb->id != $currentcategoryid) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return $categoryb->id <=> $categorya->id;
|
||||
});
|
||||
}
|
||||
}
|
@ -31,8 +31,12 @@ class renderer extends \plugin_renderer_base {
|
||||
*
|
||||
* @param array $displaydata
|
||||
* @return string
|
||||
* @deprecated since Moodle 5.0.
|
||||
* @todo MDL-82413 Final deprecation in Moodle 6.0.
|
||||
*/
|
||||
#[\core\attribute\deprecated('qbank_bulkmove\output\bulk_move', since: '5.0', mdl: 'MDL-71378')]
|
||||
public function render_bulk_move_form($displaydata) {
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
return $this->render_from_template('qbank_bulkmove/bulk_move', $displaydata);
|
||||
}
|
||||
|
||||
|
@ -16,8 +16,8 @@
|
||||
|
||||
namespace qbank_bulkmove;
|
||||
|
||||
use core_question\local\bank\bulk_action_base;
|
||||
use core_question\local\bank\plugin_features_base;
|
||||
use core_question\local\bank\view;
|
||||
|
||||
/**
|
||||
* Class plugin_feature is the entrypoint for the features.
|
||||
@ -28,9 +28,15 @@ use core_question\local\bank\plugin_features_base;
|
||||
* @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(): array {
|
||||
|
||||
/**
|
||||
* Initialise the bulk action.
|
||||
* @param view $qbank
|
||||
* @return bulk_move_action[]
|
||||
*/
|
||||
public function get_bulk_actions(view $qbank): array {
|
||||
return [
|
||||
new bulk_move_action(),
|
||||
new bulk_move_action($qbank),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,12 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['bulkmoveheader'] = 'Move the selected questions';
|
||||
$string['bulkmoveheader'] = 'Move the selected questions to...';
|
||||
$string['close'] = 'Close';
|
||||
$string['confirmmove'] = 'Are you sure you want to move these questions?';
|
||||
$string['movequestions'] = 'Move questions';
|
||||
$string['movetobulkaction'] = 'Move to...';
|
||||
$string['pluginname'] = 'Bulk move questions';
|
||||
$string['privacy:metadata'] = 'The Bulk move questions question bank plugin does not store any personal data.';
|
||||
$string['questionsmoved'] = 'Questions successfully moved';
|
||||
$string['warning'] = 'You must select a question bank before you can select a category.';
|
||||
|
40
question/bank/bulkmove/lib.php
Normal file
40
question/bank/bulkmove/lib.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* qbank_bulkmove lib functions.
|
||||
*
|
||||
* @package qbank_bulkmove
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Simon Adams <simon.adams@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generates the bulkmove bank and category chooser for display in the modal.
|
||||
*
|
||||
* @param array $args
|
||||
* @return bool|string
|
||||
*/
|
||||
function qbank_bulkmove_output_fragment_bulk_move(array $args) {
|
||||
global $OUTPUT;
|
||||
|
||||
$currentbankid = clean_param($args['context']->instanceid, PARAM_INT);
|
||||
$currentcategoryid = clean_param($args['categoryid'], PARAM_INT);
|
||||
$qbankcatchooser = new \qbank_bulkmove\output\bulk_move($currentbankid, $currentcategoryid);
|
||||
|
||||
return $OUTPUT->render($qbankcatchooser);
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Move questions page.
|
||||
*
|
||||
* @package qbank_bulkmove
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @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 core_question\local\bank\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);
|
||||
$PAGE->activityheader->disable();
|
||||
$PAGE->set_secondary_active_tab("questionbank");
|
||||
|
||||
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);
|
||||
}
|
||||
$returnfilters = \core_question\local\bank\filter_condition_manager::update_filter_param_to_category(
|
||||
$returnurl->param('filter'),
|
||||
$tocategoryid,
|
||||
);
|
||||
redirect(new moodle_url($returnurl, ['filter' => $returnfilters]));
|
||||
}
|
||||
|
||||
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();
|
@ -17,27 +17,124 @@
|
||||
{{!
|
||||
@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
|
||||
Template for qbank_bulkmove/modal_question_bank_bulkmove
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"categorydropdown": "<select class='select custom-select custom-select'><optgroup label='Course: tes'><option value='2,13'>Default for test (5)</option></optgroup></select>",
|
||||
"moveurl": "/question/bank/bulkmove/move.php?courseid=2",
|
||||
"returnurl": "/question/edit.php?courseid=2"
|
||||
{
|
||||
"allsharedbanks": [
|
||||
{
|
||||
"name": "Quiz 1",
|
||||
"contextid": 1,
|
||||
"coursenamebankname": "c1 - Quiz 1"
|
||||
},
|
||||
{
|
||||
"name": "Question bank 1",
|
||||
"modid": "2",
|
||||
"contextid": 2,
|
||||
"coursenamebankname": "c1 - Question bank 1",
|
||||
"cminfo": {},
|
||||
"questioncategories": [
|
||||
{
|
||||
"id": "22",
|
||||
"name": "Default for Question bank 1",
|
||||
"contextid": "22",
|
||||
"enabled": "disabled"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Question bank 2",
|
||||
"modid": "3",
|
||||
"contextid": 33,
|
||||
"coursenamebankname": "c2 - Question bank 2",
|
||||
"cminfo": {},
|
||||
"questioncategories": [
|
||||
{
|
||||
"id": "23",
|
||||
"name": "Default for Question bank 2",
|
||||
"contextid": "23",
|
||||
"enabled": "disabled"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"allcategories": [
|
||||
{
|
||||
"id": "22",
|
||||
"name": "Default for Question bank 1",
|
||||
"contextid": "22"
|
||||
},
|
||||
{
|
||||
"id": "23",
|
||||
"name": "Default for Question bank 2",
|
||||
"contextid": "23",
|
||||
"enabled": "disabled"
|
||||
},
|
||||
{
|
||||
"id": "24",
|
||||
"name": "Default for Question bank 3",
|
||||
"contextid": "24",
|
||||
"enabled": "disabled"
|
||||
},
|
||||
{
|
||||
"id": "25",
|
||||
"name": "Default for Question bank 4",
|
||||
"contextid": "25",
|
||||
"enabled": "disabled"
|
||||
}
|
||||
],
|
||||
"save": {
|
||||
"id": "single_button669e368d496707",
|
||||
"formid": null,
|
||||
"method": "post",
|
||||
"url": "#",
|
||||
"label": "Move questions",
|
||||
"classes": "singlebutton",
|
||||
"disabled": false,
|
||||
"tooltip": null,
|
||||
"type": "primary",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "data-action",
|
||||
"value": "bulkmovesave"
|
||||
},
|
||||
{
|
||||
"name": "disabled",
|
||||
"value": "disabled"
|
||||
}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"name": "sesskey",
|
||||
"value": "abcde12345"
|
||||
}
|
||||
],
|
||||
"actions": [],
|
||||
"hasactions": false
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
<div class="bulkmovequestion-header">
|
||||
<h3>{{#str}} bulkmoveheader, qbank_bulkmove {{/str}}</h3>
|
||||
<div class="search-banks">
|
||||
<h5>{{#str}}questionbank, question{{/str}}</h5>
|
||||
<select class="custom-select bulk-move d-none" id="searchbanks">
|
||||
{{#allsharedbanks}}
|
||||
<option value="{{contextid}}">{{{coursenamebankname}}}</option>
|
||||
{{/allsharedbanks}}
|
||||
</select>
|
||||
</div>
|
||||
<form action="{{{moveurl}}}" method="post" id="bulkmovequestion">
|
||||
{{{categorydropdown}}}
|
||||
<input type="submit" value="{{#str}} moveto, question {{/str}}" class="btn btn-primary" name="move" data-action="toggle" data-togglegroup="qbank"
|
||||
data-toggle="action" form="bulkmovequestion">
|
||||
<a href="{{{returnurl}}}" class="btn btn-secondary">{{#str}} close, qbank_bulkmove {{/str}}</a>
|
||||
</form>
|
||||
<div class="search-categories mt-3">
|
||||
<h5>{{#str}}questioncategories, question{{/str}}</h5>
|
||||
<select class="custom-select bulk-move d-none" id="searchcategories">
|
||||
{{#allcategories}}
|
||||
<option value="{{id}}" data-bank-contextid="{{contextid}}" data-enabled="{{enabled}}">{{{name}}}</option>
|
||||
{{/allcategories}}
|
||||
</select>
|
||||
<div id="searchcatwarning" class="d-none">{{#str}}warning, qbank_bulkmove{{/str}}</div>
|
||||
</div>
|
||||
|
||||
{{#save}}
|
||||
<div class="move-questions mt-3">
|
||||
{{>core/single_button}}
|
||||
</div>
|
||||
{{/save}}
|
||||
|
31
question/bank/bulkmove/templates/bulk_move_footer.mustache
Normal file
31
question/bank/bulkmove/templates/bulk_move_footer.mustache
Normal file
@ -0,0 +1,31 @@
|
||||
{{!
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template qbank_bulkmove/bulk_move_footer
|
||||
|
||||
Template for qbank_bulkmove/modal_question_bank_bulkmove
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
|
||||
}
|
||||
}}
|
||||
|
||||
<div class="bulk-move-footer">
|
||||
<button class="btn btn-primary" data-action="save">{{#str}}confirm{{/str}}</button>
|
||||
<button class="btn btn-primary" data-action="cancel">{{#str}}cancel{{/str}}</button>
|
||||
</div>
|
@ -6,15 +6,37 @@ Feature: Use the qbank plugin manager page for bulkmove
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
| Course 2 | C2 | 0 |
|
||||
| Course 3 | C3 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| teacher1 | C2 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Test quiz | C1 | quiz1 |
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Test quiz | C1 | quiz1 |
|
||||
| qbank | Question bank 1 | C1 | qbank1 |
|
||||
| qbank | Question bank 2 | C2 | qbank2 |
|
||||
| qbank | Question bank 3 | C3 | qbank3 |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
| contextlevel | reference | name |
|
||||
| Activity module | quiz1 | Test questions 1 |
|
||||
| Activity module | qbank1 | Test questions 2 |
|
||||
| Activity module | qbank2 | Test questions 3 |
|
||||
| Activity module | qbank3 | Test questions 4 |
|
||||
| Activity module | qbank1 | Test questions 5 |
|
||||
| Activity module | quiz1 | Test questions 6 |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | First question | Answer the first question |
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions 1 | truefalse | First question | Answer the first question |
|
||||
| Test questions 2 | truefalse | Second question | Answer the second question |
|
||||
| Test questions 3 | truefalse | Third question | Answer the third question |
|
||||
| Test questions 4 | truefalse | Fourth question | Answer the fourth question |
|
||||
| Test questions 5 | truefalse | Fifth question | Answer the fifth question |
|
||||
| Test questions 6 | truefalse | Sixth question | Answer the sixth question |
|
||||
|
||||
@javascript
|
||||
Scenario: Enable/disable bulk move questions bulk action from the base view
|
||||
@ -32,3 +54,42 @@ Feature: Use the qbank plugin manager page for bulkmove
|
||||
And I click on "First question" "checkbox"
|
||||
And I click on "With selected" "button"
|
||||
And I should see question bulk action "move"
|
||||
|
||||
@javascript
|
||||
Scenario: Selecting a shared question bank limits the available categories to those belonging to the selected bank.
|
||||
Given I log in as "teacher1"
|
||||
And I am on the "Test quiz" "mod_quiz > question bank" page
|
||||
And I click on "First question" "checkbox"
|
||||
And I click on "With selected" "button"
|
||||
And I click on "move" "button"
|
||||
And I open the autocomplete suggestions list in the ".search-categories" "css_element"
|
||||
And "Test questions 1" "autocomplete_suggestions" should exist
|
||||
And "Test questions 2" "autocomplete_suggestions" should not exist
|
||||
And "Test questions 3" "autocomplete_suggestions" should not exist
|
||||
And "Test questions 4" "autocomplete_suggestions" should not exist
|
||||
And "Test questions 5" "autocomplete_suggestions" should not exist
|
||||
And "Test questions 6" "autocomplete_suggestions" should exist
|
||||
When I open the autocomplete suggestions list in the ".search-banks" "css_element"
|
||||
Then I should not see "C3 - Question bank 3" in the ".search-banks" "css_element"
|
||||
And I click on "C1 - Question bank 1" item in the autocomplete list
|
||||
Then I should not see "Test questions 1" in the ".search-categories" "css_element"
|
||||
And I open the autocomplete suggestions list in the ".search-categories" "css_element"
|
||||
And "Test questions 2" "autocomplete_suggestions" should exist
|
||||
And "Test questions 3" "autocomplete_suggestions" should not exist
|
||||
And "Test questions 4" "autocomplete_suggestions" should not exist
|
||||
And "Test questions 5" "autocomplete_suggestions" should exist
|
||||
|
||||
@javascript
|
||||
Scenario: Move a question from one bank category to another.
|
||||
Given I log in as "teacher1"
|
||||
And I am on the "Test quiz" "mod_quiz > question bank" page
|
||||
And I click on "First question" "checkbox"
|
||||
And I click on "With selected" "button"
|
||||
And I click on "move" "button"
|
||||
And I open the autocomplete suggestions list in the ".search-categories" "css_element"
|
||||
And I click on "Test questions 6" item in the autocomplete list
|
||||
And I click on "Move questions" "button"
|
||||
Then I should see "Are you sure you want to move these questions?"
|
||||
And I click on "Confirm" "button"
|
||||
And I wait until the page is ready
|
||||
Then I should see "Questions successfully moved"
|
||||
|
@ -216,6 +216,7 @@ class helper_test extends \advanced_testcase {
|
||||
$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->assertDebuggingCalled();
|
||||
$this->assertStringContainsString('Test question category 1', $displaydata['categorydropdown']);
|
||||
$this->assertStringContainsString('Default for QBANK 1', $displaydata['categorydropdown']);
|
||||
$this->assertEquals($url, $displaydata ['moveurl']);
|
||||
|
@ -26,6 +26,6 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'qbank_bulkmove';
|
||||
$plugin->version = 2024100700;
|
||||
$plugin->version = 2024100800;
|
||||
$plugin->requires = 2024100100;
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
|
@ -44,9 +44,14 @@ class plugin_feature extends plugin_features_base {
|
||||
];
|
||||
}
|
||||
|
||||
public function get_bulk_actions(): array {
|
||||
/**
|
||||
* Initialise the bulk action.
|
||||
* @param view $qbank
|
||||
* @return bulk_delete_action[]
|
||||
*/
|
||||
public function get_bulk_actions(view $qbank): array {
|
||||
return [
|
||||
new bulk_delete_action(),
|
||||
new bulk_delete_action($qbank),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -45,11 +45,12 @@ class question_history_view_test extends \advanced_testcase {
|
||||
|
||||
// Create a course.
|
||||
$course = $generator->create_course();
|
||||
$context = \context_course::instance($course->id);
|
||||
$qbank = $generator->create_module('qbank', ['course' => $course->id]);
|
||||
$context = \context_module::instance($qbank->cmid);
|
||||
|
||||
// Create a question in the default category.
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts($context);
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
|
||||
$questiondata1 = $questiongenerator->create_question('numerical', null,
|
||||
['name' => 'Example question', 'category' => $cat->id]);
|
||||
|
||||
|
@ -24,6 +24,8 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use core\context;
|
||||
use core\notification;
|
||||
use core_external\external_api;
|
||||
use core_external\external_description;
|
||||
use core_external\external_value;
|
||||
@ -31,6 +33,7 @@ use core_external\external_single_structure;
|
||||
use core_external\external_multiple_structure;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_warnings;
|
||||
use core_question\local\bank\filter_condition_manager;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
132
question/classes/external/move_questions.php
vendored
Normal file
132
question/classes/external/move_questions.php
vendored
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core_question\external;
|
||||
|
||||
use core\context;
|
||||
use core\exception\moodle_exception;
|
||||
use core\notification;
|
||||
use core_external\external_api;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_value;
|
||||
use core_external\restricted_context_exception;
|
||||
use core_question\local\bank\filter_condition_manager;
|
||||
use moodle_url;
|
||||
|
||||
/**
|
||||
* API for moving questions from one question bank category to another.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Simon Adams <simon.adams@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class move_questions extends external_api {
|
||||
|
||||
/**
|
||||
* Declare the method parameters.
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters(
|
||||
[
|
||||
'newcontextid' => new external_value(PARAM_INT, 'Contextid of the target question bank'),
|
||||
'newcategoryid' => new external_value(PARAM_INT, 'ID of the target question category'),
|
||||
'questionids' => new external_value(PARAM_SEQUENCE, 'Comma separated list of question ids to move'),
|
||||
'returnurl' => new external_value(PARAM_URL,
|
||||
desc: 'A URL to add/update the filter param with the new category',
|
||||
default: ''
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the webservice response.
|
||||
*
|
||||
* @return external_value
|
||||
*/
|
||||
public static function execute_returns(): external_value {
|
||||
return new external_value(PARAM_URL, 'Modified URL with filter param containing the new question category', VALUE_OPTIONAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move questions to a new question category.
|
||||
* Optionally provide a url to add/update it with the filter param containing the new category.
|
||||
*
|
||||
* @param int $newcontextid of the target question bank
|
||||
* @param int $newcategoryid of the target category
|
||||
* @param string $questionids comma separated list of question ids to move
|
||||
* @param string $returnurlstring optional, provide this to have the filter url param added/updated to reflect the new category
|
||||
* @return null|string if $returnurlstring was provided then an updated url which filters to the new category
|
||||
*/
|
||||
public static function execute(
|
||||
int $newcontextid,
|
||||
int $newcategoryid,
|
||||
string $questionids,
|
||||
string $returnurlstring = ''
|
||||
): ?string {
|
||||
global $DB;
|
||||
|
||||
[
|
||||
'newcontextid' => $newcontextid,
|
||||
'newcategoryid' => $newcategoryid,
|
||||
'questionids' => $questionids,
|
||||
'returnurl' => $returnurlstring,
|
||||
] = self::validate_parameters(self::execute_parameters(), [
|
||||
'newcontextid' => $newcontextid,
|
||||
'newcategoryid' => $newcategoryid,
|
||||
'questionids' => $questionids,
|
||||
'returnurl' => $returnurlstring,
|
||||
]);
|
||||
|
||||
$newcontext = context::instance_by_id($newcontextid);
|
||||
self::validate_context($newcontext);
|
||||
|
||||
\core_question\local\bank\helper::require_plugin_enabled('qbank_bulkmove');
|
||||
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts($newcontext);
|
||||
$contexts->require_cap('moodle/question:add');
|
||||
|
||||
if (!$targetcategory = $DB->get_record('question_categories', ['id' => $newcategoryid, 'contextid' => $newcontextid])) {
|
||||
throw new \moodle_exception('cannotfindcate', 'question');
|
||||
}
|
||||
|
||||
\qbank_bulkmove\helper::bulk_move_questions($questionids, $targetcategory);
|
||||
notification::success(get_string('questionsmoved', 'qbank_bulkmove'));
|
||||
|
||||
if ($returnurlstring) {
|
||||
$returnurl = new moodle_url($returnurlstring);
|
||||
$returnurl->param('cmid', $newcontext->instanceid);
|
||||
$filter = $returnurl->param('filter');
|
||||
if ($filter) {
|
||||
$returnfilters = filter_condition_manager::update_filter_param_to_category(
|
||||
$filter,
|
||||
$newcategoryid,
|
||||
);
|
||||
} else {
|
||||
$returnfilters = json_encode(
|
||||
filter_condition_manager::get_default_filter("{$newcategoryid},{$newcontextid}"),
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
$returnurl->param('filter', $returnfilters);
|
||||
return $returnurl->out(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ namespace core_question\local\bank;
|
||||
* @author Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class bulk_action_base {
|
||||
abstract class bulk_action_base extends view_component {
|
||||
|
||||
/**
|
||||
* Title of the bulk action.
|
||||
@ -69,6 +69,15 @@ abstract class bulk_action_base {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override if you want to load your own javascript.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function initialise_javascript(): void {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since Moodle 4.0
|
||||
*/
|
||||
|
@ -25,6 +25,10 @@
|
||||
|
||||
namespace core_question\local\bank;
|
||||
|
||||
use core\output\datafilter;
|
||||
use qbank_deletequestion\hidden_condition;
|
||||
use qbank_managecategories\category_condition;
|
||||
|
||||
/**
|
||||
* Static methods for parsing and formatting data related to filter conditions.
|
||||
*/
|
||||
@ -117,4 +121,29 @@ class filter_condition_manager {
|
||||
}
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a category-context string to get a default filter array for the category.
|
||||
*
|
||||
* @param string $catstring in format '1,2' or 'categoryid,contextid'
|
||||
* @return array
|
||||
*/
|
||||
public static function get_default_filter(string $catstring): array {
|
||||
$filter = [];
|
||||
[$validcatid, $contextid] = category_condition::validate_category_param($catstring);
|
||||
if (!is_null($validcatid)) {
|
||||
$category = category_condition::get_category_record($validcatid, $contextid);
|
||||
$filter['category'] = [
|
||||
'jointype' => condition::JOINTYPE_DEFAULT,
|
||||
'values' => [$category->id],
|
||||
'filteroptions' => ['includesubcategories' => false],
|
||||
];
|
||||
}
|
||||
$filter['hidden'] = [
|
||||
'jointype' => condition::JOINTYPE_DEFAULT,
|
||||
'values' => [0],
|
||||
];
|
||||
|
||||
return $filter;
|
||||
}
|
||||
}
|
||||
|
@ -74,9 +74,10 @@ class plugin_features_base {
|
||||
/**
|
||||
* This method will return the array objects for the bulk actions ui.
|
||||
*
|
||||
* @param view $qbank
|
||||
* @return bulk_action_base[]
|
||||
*/
|
||||
public function get_bulk_actions() {
|
||||
public function get_bulk_actions(view $qbank): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -179,7 +179,7 @@ class view {
|
||||
public $returnurl;
|
||||
|
||||
/**
|
||||
* @var array $bulkactions to identify the bulk actions for the api.
|
||||
* @var bulk_action_base[] $bulkactions bulk actions for the api.
|
||||
*/
|
||||
public $bulkactions = [];
|
||||
|
||||
@ -237,20 +237,7 @@ class view {
|
||||
|
||||
// Default filter condition.
|
||||
if (!isset($params['filter']) && isset($params['cat'])) {
|
||||
$params['filter'] = [];
|
||||
[$categoryid, $contextid] = category_condition::validate_category_param($params['cat']);
|
||||
if (!is_null($categoryid)) {
|
||||
$category = category_condition::get_category_record($categoryid, $contextid);
|
||||
$params['filter']['category'] = [
|
||||
'jointype' => category_condition::JOINTYPE_DEFAULT,
|
||||
'values' => [$category->id],
|
||||
'filteroptions' => ['includesubcategories' => false],
|
||||
];
|
||||
}
|
||||
$params['filter']['hidden'] = [
|
||||
'jointype' => hidden_condition::JOINTYPE_DEFAULT,
|
||||
'values' => [0],
|
||||
];
|
||||
$params['filter'] = filter_condition_manager::get_default_filter($params['cat']);
|
||||
$params['jointype'] = datafilter::JOINTYPE_ALL;
|
||||
}
|
||||
if (!empty($params['filter'])) {
|
||||
@ -321,7 +308,7 @@ class view {
|
||||
*/
|
||||
protected function init_bulk_actions(): void {
|
||||
foreach ($this->plugins as $componentname => $plugin) {
|
||||
$bulkactions = $plugin->get_bulk_actions();
|
||||
$bulkactions = $plugin->get_bulk_actions($this);
|
||||
if (!is_array($bulkactions)) {
|
||||
debugging("The method {$componentname}::get_bulk_actions() must return an " .
|
||||
"array of bulk actions instead of a single bulk action. " .
|
||||
@ -331,11 +318,7 @@ class view {
|
||||
}
|
||||
|
||||
foreach ($bulkactions as $bulkactionobject) {
|
||||
$this->bulkactions[$bulkactionobject->get_key()] = [
|
||||
'title' => $bulkactionobject->get_bulk_action_title(),
|
||||
'url' => $bulkactionobject->get_bulk_action_url(),
|
||||
'capabilities' => $bulkactionobject->get_bulk_action_capabilities()
|
||||
];
|
||||
$this->bulkactions[$bulkactionobject->get_key()] = $bulkactionobject;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1328,7 +1311,7 @@ class view {
|
||||
foreach ($this->bulkactions as $key => $action) {
|
||||
// Check capabilities.
|
||||
$capcount = 0;
|
||||
foreach ($action['capabilities'] as $capability) {
|
||||
foreach ($action->get_bulk_action_capabilities() as $capability) {
|
||||
if (has_capability($capability, $catcontext)) {
|
||||
$capcount ++;
|
||||
}
|
||||
@ -1339,9 +1322,9 @@ class view {
|
||||
continue;
|
||||
}
|
||||
$actiondata = new \stdClass();
|
||||
$actiondata->actionname = $action['title'];
|
||||
$actiondata->actionname = $action->get_bulk_action_title();
|
||||
$actiondata->actionkey = $key;
|
||||
$actiondata->actionurl = new \moodle_url($action['url'], $params);
|
||||
$actiondata->actionurl = new \moodle_url($action->get_bulk_action_url(), $params);
|
||||
$bulkactiondata[] = $actiondata;
|
||||
|
||||
$bulkactiondatas ['bulkactionitems'] = $bulkactiondata;
|
||||
@ -1353,6 +1336,16 @@ class view {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Give each bulk action a chance to load its own javascript module.
|
||||
* @return void
|
||||
*/
|
||||
public function init_bulk_actions_js(): void {
|
||||
foreach ($this->bulkactions as $action) {
|
||||
$action->initialise_javascript();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the questions.
|
||||
*
|
||||
|
@ -82,8 +82,10 @@ echo $renderer->render($qbankaction);
|
||||
// Print the question area.
|
||||
$questionbank->display();
|
||||
|
||||
[$categoryid, $contextid] = explode(',', $pagevars['cat']);
|
||||
$questionbank->init_bulk_actions_js();
|
||||
|
||||
// Log the view of this category.
|
||||
list($categoryid, $contextid) = explode(',', $pagevars['cat']);
|
||||
$category = new stdClass();
|
||||
$category->id = $categoryid;
|
||||
$catcontext = context::instance_by_id($contextid);
|
||||
|
Loading…
x
Reference in New Issue
Block a user