diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 670dd34..f3b9890 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,7 +7,7 @@ jobs:
services:
postgres:
- image: postgres:14
+ image: postgres:15
env:
POSTGRES_USER: 'postgres'
POSTGRES_HOST_AUTH_METHOD: 'trust'
@@ -30,8 +30,8 @@ jobs:
fail-fast: false
matrix:
include:
- - { php: '8.2', moodle-branch: MOODLE_404_STABLE, database: mariadb }
- - { php: '8.3', moodle-branch: MOODLE_405_STABLE, database: pgsql }
+ - { php: '8.2', moodle-branch: MOODLE_500_STABLE, database: mariadb }
+ - { php: '8.3', moodle-branch: main, database: pgsql }
steps:
- name: Check out repository code
diff --git a/amd/build/modal_embedquestion_question_bank.min.js b/amd/build/modal_embedquestion_question_bank.min.js
new file mode 100644
index 0000000..7b39443
--- /dev/null
+++ b/amd/build/modal_embedquestion_question_bank.min.js
@@ -0,0 +1,3 @@
+define("filter_embedquestion/modal_embedquestion_question_bank",["exports","mod_quiz/add_question_modal","core/fragment","core/str","core/form-autocomplete"],(function(_exports,_add_question_modal,Fragment,_str,_formAutocomplete){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=_exports.ModalEmbedQuestionQuestionBank=void 0,_add_question_modal=_interopRequireDefault(_add_question_modal),Fragment=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Fragment),_formAutocomplete=_interopRequireDefault(_formAutocomplete);const SELECTORS_BANK_SEARCH="#searchbanks",SELECTORS_NEW_BANKMOD_ID="data-newmodid",SELECTORS_ANCHOR="a[href]",SELECTORS_GO_BACK_BUTTON='button[data-action="go-back"]';class ModalEmbedQuestionQuestionBank extends _add_question_modal.default{configure(modalConfig){modalConfig.large=!0,modalConfig.show=!0,modalConfig.removeOnClose=!0,this.setContextId(modalConfig.contextId),this.setAddOnPageId(modalConfig.addOnPage),this.courseId=modalConfig.courseId,this.bankCmId=modalConfig.bankCmId,this.originalTitle=modalConfig.title,this.currentEditor=modalConfig.editor,super.configure(modalConfig)}show(){return this.handleSwitchBankContentReload(SELECTORS_BANK_SEARCH),super.show(this)}fireQbankSelectedEvent(bankCmid){this.destroy();const event=new CustomEvent("filter_embedquestion:qbank_selected",{detail:{bankCmid:bankCmid,editor:this.currentEditor}});document.dispatchEvent(event)}registerEventListeners(){super.registerEventListeners(this),this.getModal().on("click",SELECTORS_ANCHOR,(e=>{const anchorElement=e.currentTarget;e.preventDefault(),this.fireQbankSelectedEvent(anchorElement.getAttribute(SELECTORS_NEW_BANKMOD_ID))})),this.getModal().on("click",SELECTORS_GO_BACK_BUTTON,(e=>{e.preventDefault(),this.fireQbankSelectedEvent(e.currentTarget.value)}))}async handleSwitchBankContentReload(Selector){var _document$querySelect;this.setTitle((0,_str.getString)("selectquestionbank","mod_quiz"));const el=document.createElement("button");el.classList.add("btn","btn-primary"),el.textContent=await(0,_str.getString)("gobacktoquiz","mod_quiz"),el.setAttribute("data-action","go-back"),el.setAttribute("value",this.bankCmId),this.setFooter(el),this.setBody(Fragment.loadFragment("filter_embedquestion","switch_question_bank",this.getContextId(),{courseid:this.courseId}));const placeholder=await(0,_str.getString)("searchbyname","mod_quiz");await this.getBodyPromise(),await _formAutocomplete.default.enhance(Selector,!1,"core_question/question_banks_datasource",placeholder,!1,!0,"",!0),null===(_document$querySelect=document.querySelector(".search-banks .form-autocomplete-selection"))||void 0===_document$querySelect||_document$querySelect.classList.add("d-none");const bankSearchEl=document.querySelector(Selector);return bankSearchEl&&bankSearchEl.addEventListener("change",(e=>{const selectedValue=e.target.value;selectedValue>0&&this.fireQbankSelectedEvent(selectedValue)})),this}}var obj,key,value;_exports.ModalEmbedQuestionQuestionBank=ModalEmbedQuestionQuestionBank,value="filter_embedquestion-question-bank",(key="TYPE")in(obj=ModalEmbedQuestionQuestionBank)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value;var _default={ModalEmbedQuestionQuestionBank:ModalEmbedQuestionQuestionBank};return _exports.default=_default,ModalEmbedQuestionQuestionBank.registerModalType(),_exports.default}));
+
+//# sourceMappingURL=modal_embedquestion_question_bank.min.js.map
\ No newline at end of file
diff --git a/amd/build/modal_embedquestion_question_bank.min.js.map b/amd/build/modal_embedquestion_question_bank.min.js.map
new file mode 100644
index 0000000..a011b90
--- /dev/null
+++ b/amd/build/modal_embedquestion_question_bank.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"modal_embedquestion_question_bank.min.js","sources":["../src/modal_embedquestion_question_bank.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 .\n\n/**\n * Contain the logic for the question bank modal.\n *\n * @module filter_embedquestion/modal_embedquestion_question_bank\n * @copyright 2025 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Modal from 'mod_quiz/add_question_modal';\nimport * as Fragment from 'core/fragment';\nimport {getString} from 'core/str';\nimport AutoComplete from 'core/form-autocomplete';\n\nconst SELECTORS = {\n SWITCH_TO_OTHER_BANK: 'button[data-action=\"switch-question-bank\"]',\n BANK_SEARCH: '#searchbanks',\n NEW_BANKMOD_ID: 'data-newmodid',\n ANCHOR: 'a[href]',\n SORTERS: '.sorters',\n GO_BACK_BUTTON: 'button[data-action=\"go-back\"]',\n};\n\n/**\n * Class representing a modal for selecting a question bank to embed questions from.\n */\nexport class ModalEmbedQuestionQuestionBank extends Modal {\n static TYPE = 'filter_embedquestion-question-bank';\n\n configure(modalConfig) {\n // Add question modals are always large.\n modalConfig.large = true;\n\n // Always show on creation.\n modalConfig.show = true;\n modalConfig.removeOnClose = true;\n\n // Apply question modal configuration.\n this.setContextId(modalConfig.contextId);\n this.setAddOnPageId(modalConfig.addOnPage);\n this.courseId = modalConfig.courseId;\n this.bankCmId = modalConfig.bankCmId;\n // Store the original title of the modal, so we can revert back to it once we have switched to another bank.\n this.originalTitle = modalConfig.title;\n this.currentEditor = modalConfig.editor;\n // Apply standard configuration.\n super.configure(modalConfig);\n }\n\n /**\n * Show the modal and load the content for switching question banks.\n *\n * @method show\n */\n show() {\n this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH);\n return super.show(this);\n }\n\n /**\n * Switch to the embed question modal for a specific question bank.\n * This will destroy the current modal and dispatch an event to switch to the new modal.\n *\n * @param {String} bankCmid - The course module ID of the question bank to switch to.\n * @method fireQbankSelectedEvent\n */\n fireQbankSelectedEvent(bankCmid) {\n this.destroy();\n const event = new CustomEvent('filter_embedquestion:qbank_selected', {\n detail: {bankCmid: bankCmid, editor: this.currentEditor},\n });\n document.dispatchEvent(event);\n }\n\n /**\n * Set up all the event handling for the modal.\n *\n * @method registerEventListeners\n */\n registerEventListeners() {\n // Apply parent event listeners.\n super.registerEventListeners(this);\n\n this.getModal().on('click', SELECTORS.ANCHOR, (e) => {\n const anchorElement = e.currentTarget;\n e.preventDefault();\n this.fireQbankSelectedEvent(anchorElement.getAttribute(SELECTORS.NEW_BANKMOD_ID));\n });\n\n this.getModal().on('click', SELECTORS.GO_BACK_BUTTON, (e) => {\n e.preventDefault();\n this.fireQbankSelectedEvent(e.currentTarget.value);\n });\n }\n\n /**\n * Update the modal with a list of banks to switch to and enhance the standard selects to Autocomplete fields.\n *\n * @param {String} Selector for the original select element.\n * @return {Promise} Modal.\n */\n async handleSwitchBankContentReload(Selector) {\n this.setTitle(getString('selectquestionbank', 'mod_quiz'));\n\n // Create a 'Go back' button and set it in the footer.\n const el = document.createElement('button');\n el.classList.add('btn', 'btn-primary');\n el.textContent = await getString('gobacktoquiz', 'mod_quiz');\n el.setAttribute('data-action', 'go-back');\n el.setAttribute('value', this.bankCmId);\n this.setFooter(el);\n\n this.setBody(\n Fragment.loadFragment(\n 'filter_embedquestion',\n 'switch_question_bank',\n this.getContextId(),\n {\n 'courseid': this.courseId,\n })\n );\n const placeholder = await getString('searchbyname', 'mod_quiz');\n await this.getBodyPromise();\n await AutoComplete.enhance(\n Selector,\n false,\n 'core_question/question_banks_datasource',\n placeholder,\n false,\n true,\n '',\n true\n );\n\n // Hide the selection element as we don't need it.\n document.querySelector('.search-banks .form-autocomplete-selection')?.classList.add('d-none');\n // Add a change listener to get the selected value.\n const bankSearchEl = document.querySelector(Selector);\n if (bankSearchEl) {\n bankSearchEl.addEventListener('change', (e) => {\n // This will be the chosen qbankCmid.\n const selectedValue = e.target.value;\n if (selectedValue > 0) {\n this.fireQbankSelectedEvent(selectedValue);\n }\n });\n }\n return this;\n }\n}\n\nexport default {\n ModalEmbedQuestionQuestionBank,\n};\nModalEmbedQuestionQuestionBank.registerModalType();"],"names":["SELECTORS","ModalEmbedQuestionQuestionBank","Modal","configure","modalConfig","large","show","removeOnClose","setContextId","contextId","setAddOnPageId","addOnPage","courseId","bankCmId","originalTitle","title","currentEditor","editor","handleSwitchBankContentReload","super","this","fireQbankSelectedEvent","bankCmid","destroy","event","CustomEvent","detail","document","dispatchEvent","registerEventListeners","getModal","on","e","anchorElement","currentTarget","preventDefault","getAttribute","value","Selector","setTitle","el","createElement","classList","add","textContent","setAttribute","setFooter","setBody","Fragment","loadFragment","getContextId","placeholder","getBodyPromise","AutoComplete","enhance","querySelector","bankSearchEl","addEventListener","selectedValue","target","registerModalType"],"mappings":"q+CA2BMA,sBAEW,eAFXA,yBAGc,gBAHdA,iBAIM,UAJNA,yBAMc,sCAMPC,uCAAuCC,4BAGhDC,UAAUC,aAENA,YAAYC,OAAQ,EAGpBD,YAAYE,MAAO,EACnBF,YAAYG,eAAgB,OAGvBC,aAAaJ,YAAYK,gBACzBC,eAAeN,YAAYO,gBAC3BC,SAAWR,YAAYQ,cACvBC,SAAWT,YAAYS,cAEvBC,cAAgBV,YAAYW,WAC5BC,cAAgBZ,YAAYa,aAE3Bd,UAAUC,aAQpBE,mBACSY,8BAA8BlB,uBAC5BmB,MAAMb,KAAKc,MAUtBC,uBAAuBC,eACdC,gBACCC,MAAQ,IAAIC,YAAY,sCAAuC,CACjEC,OAAQ,CAACJ,SAAUA,SAAUL,OAAQG,KAAKJ,iBAE9CW,SAASC,cAAcJ,OAQ3BK,+BAEUA,uBAAuBT,WAExBU,WAAWC,GAAG,QAAS/B,kBAAmBgC,UACrCC,cAAgBD,EAAEE,cACxBF,EAAEG,sBACGd,uBAAuBY,cAAcG,aAAapC,mCAGtD8B,WAAWC,GAAG,QAAS/B,0BAA2BgC,IACnDA,EAAEG,sBACGd,uBAAuBW,EAAEE,cAAcG,8CAUhBC,yCAC3BC,UAAS,kBAAU,qBAAsB,mBAGxCC,GAAKb,SAASc,cAAc,UAClCD,GAAGE,UAAUC,IAAI,MAAO,eACxBH,GAAGI,kBAAoB,kBAAU,eAAgB,YACjDJ,GAAGK,aAAa,cAAe,WAC/BL,GAAGK,aAAa,QAASzB,KAAKP,eACzBiC,UAAUN,SAEVO,QACDC,SAASC,aACL,uBACA,uBACA7B,KAAK8B,eACL,UACgB9B,KAAKR,kBAGvBuC,kBAAoB,kBAAU,eAAgB,kBAC9C/B,KAAKgC,uBACLC,0BAAaC,QACfhB,UACA,EACA,0CACAa,aACA,GACA,EACA,IACA,iCAIJxB,SAAS4B,cAAc,sGAA+Cb,UAAUC,IAAI,gBAE9Ea,aAAe7B,SAAS4B,cAAcjB,iBACxCkB,cACAA,aAAaC,iBAAiB,UAAWzB,UAE/B0B,cAAgB1B,EAAE2B,OAAOtB,MAC3BqB,cAAgB,QACXrC,uBAAuBqC,kBAIjCtC,qGAxHG,wDADLnB,mJA6HE,CACXA,+BAAAA,iEAEJA,+BAA+B2D"}
\ No newline at end of file
diff --git a/amd/build/questionid_choice_updater.min.js b/amd/build/questionid_choice_updater.min.js
index 74a8625..348dea8 100644
--- a/amd/build/questionid_choice_updater.min.js
+++ b/amd/build/questionid_choice_updater.min.js
@@ -6,6 +6,6 @@
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define("filter_embedquestion/questionid_choice_updater",["jquery","core/ajax"],(function($,Ajax){var t={init:function(){$("select#id_categoryidnumber").on("change",t.categoryChanged),t.lastCategory=null},lastCategory:null,categoryChanged:function(){$("select#id_categoryidnumber").val()!==t.lastCategory&&(M.util.js_pending("filter_embedquestion-get_questions"),t.lastCategory=$("select#id_categoryidnumber").val(),""===t.lastCategory?t.updateChoices([]):Ajax.call([{methodname:"filter_embedquestion_get_sharable_question_choices",args:{courseid:$("input[name=courseid]").val(),categoryidnumber:t.lastCategory}}])[0].done(t.updateChoices))},updateChoices:function(response){var select=$("select#id_questionidnumber");select.empty(),$(response).each((function(index,option){select.append('")})),M.util.js_complete("filter_embedquestion-get_questions")}};return t}));
+define("filter_embedquestion/questionid_choice_updater",["jquery","core/ajax","core/str","core/notification","core_user/repository"],(function($,Ajax,Str,Notification,UserRepository){var t={init:function(defaultQbankCmid){$("select#id_qbankcmid").on("change",t.qbankChanged),$("select#id_categoryidnumber").on("change",t.categoryChanged),t.lastQbank=$("select#id_qbankcmid").val(),t.lastCategory=$("select#id_categoryidnumber").val();var selectedText=$("#id_qbankcmid option:selected").text();Str.get_string("currentbank","mod_quiz",selectedText).then((function(string){$("#id_questionheadercontainer h5").text(string)})).catch(Notification.exception),defaultQbankCmid&&$("select#id_qbankcmid").val(defaultQbankCmid).trigger("change")},lastCategory:null,lastQbank:null,categoryChanged:function(){M.util.js_pending("filter_embedquestion-get_questions"),t.lastCategory=$("select#id_categoryidnumber").val(),""===t.lastCategory?t.updateChoices([]):(Ajax.call([{methodname:"filter_embedquestion_get_sharable_question_choices",args:{cmid:t.lastQbank,categoryidnumber:t.lastCategory}}])[0].then(t.updateChoices).catch(Notification.exception),$("select#id_questionidnumber").attr("disabled",!1))},qbankChanged:function(){if($("select#id_qbankcmid").val()!==t.lastQbank){M.util.js_pending("filter_embedquestion-get_categories"),t.lastQbank=$("select#id_qbankcmid").val();var selectedText=$("#id_qbankcmid option:selected").text();Str.get_string("currentbank","mod_quiz",selectedText).then((function(string){$("#id_questionheadercontainer h5").text(string)})).catch(Notification.exception);var prefKey="filter_embedquestion_userdefaultqbank",courseId=document.querySelector('input[name="courseid"]').value;document.querySelector('input[name="issamecourse"]').value&&UserRepository.getUserPreference(prefKey).then((current=>{let prefs=current?JSON.parse(current):{};return prefs[courseId]=t.lastQbank,UserRepository.setUserPreference(prefKey,JSON.stringify(prefs))})).catch(Notification.exception),""===$("select#id_qbankcmid").val()?(t.updateCategories([]),M.util.js_pending("filter_embedquestion-get_questions"),t.updateChoices([])):(Ajax.call([{methodname:"filter_embedquestion_get_sharable_category_choices",args:{cmid:t.lastQbank}}])[0].then(t.updateCategories).catch(Notification.exception),M.util.js_pending("filter_embedquestion-get_questions"),t.updateChoices([]))}},updateCategories:function(response){var select=$("select#id_categoryidnumber");select.empty(),$(response).each((function(index,option){select.append('")})),M.util.js_complete("filter_embedquestion-get_categories")},updateChoices:function(response){var select=$("select#id_questionidnumber");select.empty(),$(response).each((function(index,option){select.append('")})),M.util.js_complete("filter_embedquestion-get_questions")}};return t}));
//# sourceMappingURL=questionid_choice_updater.min.js.map
\ No newline at end of file
diff --git a/amd/build/questionid_choice_updater.min.js.map b/amd/build/questionid_choice_updater.min.js.map
index 596a63f..75d85c2 100644
--- a/amd/build/questionid_choice_updater.min.js.map
+++ b/amd/build/questionid_choice_updater.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"questionid_choice_updater.min.js","sources":["../src/questionid_choice_updater.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 .\n\n/*\n * The module provides autocomplete for the question idnumber form field.\n *\n * @module filter_embedquestion/questionid_choice_updater\n * @package filter_embedquestion\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax'], function($, Ajax) {\n var t = {\n /**\n * Initialise the handling.\n */\n init: function() {\n $('select#id_categoryidnumber').on('change', t.categoryChanged);\n t.lastCategory = null;\n },\n\n /**\n * Used to track when the category really changes.\n */\n lastCategory: null,\n\n /**\n * Source of data for Ajax element.\n */\n categoryChanged: function() {\n if ($('select#id_categoryidnumber').val() === t.lastCategory) {\n return;\n }\n\n M.util.js_pending('filter_embedquestion-get_questions');\n t.lastCategory = $('select#id_categoryidnumber').val();\n\n if (t.lastCategory === '') {\n t.updateChoices([]);\n } else {\n Ajax.call([{\n methodname: 'filter_embedquestion_get_sharable_question_choices',\n args: {courseid: $('input[name=courseid]').val(), categoryidnumber: t.lastCategory}\n }])[0].done(t.updateChoices);\n }\n },\n\n /**\n * Update the contents of the Question select with the results of the AJAX call.\n *\n * @param {Array} response - array of options, each has fields value and label.\n */\n updateChoices: function(response) {\n var select = $('select#id_questionidnumber');\n\n select.empty();\n $(response).each(function(index, option) {\n select.append('');\n });\n M.util.js_complete('filter_embedquestion-get_questions');\n }\n };\n return t;\n});\n"],"names":["define","$","Ajax","t","init","on","categoryChanged","lastCategory","val","M","util","js_pending","updateChoices","call","methodname","args","courseid","categoryidnumber","done","response","select","empty","each","index","option","append","value","label","js_complete"],"mappings":";;;;;;;;AAuBAA,wDAAO,CAAC,SAAU,cAAc,SAASC,EAAGC,UACpCC,EAAI,CAIJC,KAAM,WACFH,EAAE,8BAA8BI,GAAG,SAAUF,EAAEG,iBAC/CH,EAAEI,aAAe,MAMrBA,aAAc,KAKdD,gBAAiB,WACTL,EAAE,8BAA8BO,QAAUL,EAAEI,eAIhDE,EAAEC,KAAKC,WAAW,sCAClBR,EAAEI,aAAeN,EAAE,8BAA8BO,MAE1B,KAAnBL,EAAEI,aACFJ,EAAES,cAAc,IAEhBV,KAAKW,KAAK,CAAC,CACPC,WAAY,qDACZC,KAAM,CAACC,SAAUf,EAAE,wBAAwBO,MAAOS,iBAAkBd,EAAEI,iBACtE,GAAGW,KAAKf,EAAES,iBAStBA,cAAe,SAASO,cAChBC,OAASnB,EAAE,8BAEfmB,OAAOC,QACPpB,EAAEkB,UAAUG,MAAK,SAASC,MAAOC,QAC7BJ,OAAOK,OAAO,kBAAoBD,OAAOE,MAAQ,KAAOF,OAAOG,MAAQ,gBAE3ElB,EAAEC,KAAKkB,YAAY,+CAGpBzB"}
\ No newline at end of file
+{"version":3,"file":"questionid_choice_updater.min.js","sources":["../src/questionid_choice_updater.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 .\n\n/*\n * The module provides autocomplete for the question idnumber form field.\n *\n * @module filter_embedquestion/questionid_choice_updater\n * @package filter_embedquestion\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core_user/repository'],\n function($, Ajax, Str, Notification, UserRepository) {\n var t = {\n /**\n * Initialise the handling.\n *\n * @param {string} defaultQbankCmid - The default question bank to select, if any.\n */\n init: function(defaultQbankCmid) {\n $('select#id_qbankcmid').on('change', t.qbankChanged);\n $('select#id_categoryidnumber').on('change', t.categoryChanged);\n\n t.lastQbank = $('select#id_qbankcmid').val();\n t.lastCategory = $('select#id_categoryidnumber').val();\n var selectedText = $('#id_qbankcmid option:selected').text();\n Str.get_string('currentbank', 'mod_quiz', selectedText)\n .then(function(string) {\n $('#id_questionheadercontainer h5').text(string);\n return;\n }).catch(Notification.exception);\n if (defaultQbankCmid) {\n // If a default question bank is set, we need to trigger the change event to load the categories.\n $('select#id_qbankcmid').val(defaultQbankCmid).trigger('change');\n }\n },\n\n /**\n * Used to track when the category really changes.\n */\n lastCategory: null,\n /**\n * Used to track when the question bank really changes.\n */\n lastQbank: null,\n\n /**\n * Source of data for Ajax element.\n */\n categoryChanged: function() {\n M.util.js_pending('filter_embedquestion-get_questions');\n t.lastCategory = $('select#id_categoryidnumber').val();\n if (t.lastCategory === '') {\n t.updateChoices([]);\n } else {\n Ajax.call([{\n methodname: 'filter_embedquestion_get_sharable_question_choices',\n args: {cmid: t.lastQbank, categoryidnumber: t.lastCategory},\n }])[0].then(t.updateChoices).catch(Notification.exception);\n $('select#id_questionidnumber').attr('disabled', false);\n }\n },\n\n /**\n * Source of data for Ajax element.\n */\n qbankChanged: function() {\n if ($('select#id_qbankcmid').val() === t.lastQbank) {\n return;\n }\n M.util.js_pending('filter_embedquestion-get_categories');\n t.lastQbank = $('select#id_qbankcmid').val();\n // Update the heading immediately when selection changes.\n var selectedText = $('#id_qbankcmid option:selected').text();\n Str.get_string('currentbank', 'mod_quiz', selectedText)\n .then(function(string) {\n $('#id_questionheadercontainer h5').text(string);\n return;\n }).catch(Notification.exception);\n var prefKey = 'filter_embedquestion_userdefaultqbank';\n var courseId = document.querySelector('input[name=\"courseid\"]').value;\n var isSameCourse = document.querySelector('input[name=\"issamecourse\"]').value;\n if (isSameCourse) {\n UserRepository.getUserPreference(prefKey).then(current => {\n let prefs = current ? JSON.parse(current) : {};\n prefs[courseId] = t.lastQbank;\n return UserRepository.setUserPreference(prefKey, JSON.stringify(prefs));\n }).catch(Notification.exception);\n }\n if ($('select#id_qbankcmid').val() === '') {\n t.updateCategories([]);\n M.util.js_pending('filter_embedquestion-get_questions');\n t.updateChoices([]);\n } else {\n Ajax.call([{\n methodname: 'filter_embedquestion_get_sharable_category_choices',\n args: {cmid: t.lastQbank}\n }])[0].then(t.updateCategories).catch(Notification.exception);\n M.util.js_pending('filter_embedquestion-get_questions');\n t.updateChoices([]);\n }\n },\n\n /**\n * Update the contents of the Question select with the results of the AJAX call.\n *\n * @param {Array} response - array of options, each has fields value and label.\n */\n updateCategories: function(response) {\n var select = $('select#id_categoryidnumber');\n\n select.empty();\n $(response).each(function(index, option) {\n select.append('');\n });\n M.util.js_complete('filter_embedquestion-get_categories');\n },\n\n /**\n * Update the contents of the Question select with the results of the AJAX call.\n *\n * @param {Array} response - array of options, each has fields value and label.\n */\n updateChoices: function(response) {\n var select = $('select#id_questionidnumber');\n\n select.empty();\n $(response).each(function(index, option) {\n select.append('');\n });\n M.util.js_complete('filter_embedquestion-get_questions');\n }\n };\n return t;\n});\n"],"names":["define","$","Ajax","Str","Notification","UserRepository","t","init","defaultQbankCmid","on","qbankChanged","categoryChanged","lastQbank","val","lastCategory","selectedText","text","get_string","then","string","catch","exception","trigger","M","util","js_pending","updateChoices","call","methodname","args","cmid","categoryidnumber","attr","prefKey","courseId","document","querySelector","value","getUserPreference","current","prefs","JSON","parse","setUserPreference","stringify","updateCategories","response","select","empty","each","index","option","append","label","js_complete"],"mappings":";;;;;;;;AAwBAA,wDAAO,CAAC,SAAU,YAAa,WAAY,oBAAqB,yBACxD,SAASC,EAAGC,KAAMC,IAAKC,aAAcC,oBACrCC,EAAI,CAMJC,KAAM,SAASC,kBACXP,EAAE,uBAAuBQ,GAAG,SAAUH,EAAEI,cACxCT,EAAE,8BAA8BQ,GAAG,SAAUH,EAAEK,iBAE/CL,EAAEM,UAAYX,EAAE,uBAAuBY,MACvCP,EAAEQ,aAAeb,EAAE,8BAA8BY,UAC7CE,aAAed,EAAE,iCAAiCe,OACtDb,IAAIc,WAAW,cAAe,WAAYF,cACrCG,MAAK,SAASC,QACXlB,EAAE,kCAAkCe,KAAKG,WAE1CC,MAAMhB,aAAaiB,WACtBb,kBAEAP,EAAE,uBAAuBY,IAAIL,kBAAkBc,QAAQ,WAO/DR,aAAc,KAIdF,UAAW,KAKXD,gBAAiB,WACbY,EAAEC,KAAKC,WAAW,sCAClBnB,EAAEQ,aAAeb,EAAE,8BAA8BY,MAC1B,KAAnBP,EAAEQ,aACFR,EAAEoB,cAAc,KAEhBxB,KAAKyB,KAAK,CAAC,CACPC,WAAY,qDACZC,KAAM,CAACC,KAAMxB,EAAEM,UAAWmB,iBAAkBzB,EAAEQ,iBAC9C,GAAGI,KAAKZ,EAAEoB,eAAeN,MAAMhB,aAAaiB,WAChDpB,EAAE,8BAA8B+B,KAAK,YAAY,KAOzDtB,aAAc,cACNT,EAAE,uBAAuBY,QAAUP,EAAEM,WAGzCW,EAAEC,KAAKC,WAAW,uCAClBnB,EAAEM,UAAYX,EAAE,uBAAuBY,UAEnCE,aAAed,EAAE,iCAAiCe,OACtDb,IAAIc,WAAW,cAAe,WAAYF,cACrCG,MAAK,SAASC,QACXlB,EAAE,kCAAkCe,KAAKG,WAE1CC,MAAMhB,aAAaiB,eACtBY,QAAU,wCACVC,SAAWC,SAASC,cAAc,0BAA0BC,MAC7CF,SAASC,cAAc,8BAA8BC,OAEpEhC,eAAeiC,kBAAkBL,SAASf,MAAKqB,cACvCC,MAAQD,QAAUE,KAAKC,MAAMH,SAAW,UAC5CC,MAAMN,UAAY5B,EAAEM,UACbP,eAAesC,kBAAkBV,QAASQ,KAAKG,UAAUJ,WACjEpB,MAAMhB,aAAaiB,WAEa,KAAnCpB,EAAE,uBAAuBY,OACzBP,EAAEuC,iBAAiB,IACnBtB,EAAEC,KAAKC,WAAW,sCAClBnB,EAAEoB,cAAc,MAEhBxB,KAAKyB,KAAK,CAAC,CACPC,WAAY,qDACZC,KAAM,CAACC,KAAMxB,EAAEM,cACf,GAAGM,KAAKZ,EAAEuC,kBAAkBzB,MAAMhB,aAAaiB,WACnDE,EAAEC,KAAKC,WAAW,sCAClBnB,EAAEoB,cAAc,OASxBmB,iBAAkB,SAASC,cACnBC,OAAS9C,EAAE,8BAEf8C,OAAOC,QACP/C,EAAE6C,UAAUG,MAAK,SAASC,MAAOC,QAC7BJ,OAAOK,OAAO,kBAAoBD,OAAOd,MAAQ,KAAOc,OAAOE,MAAQ,gBAE3E9B,EAAEC,KAAK8B,YAAY,wCAQvB5B,cAAe,SAASoB,cAChBC,OAAS9C,EAAE,8BAEf8C,OAAOC,QACP/C,EAAE6C,UAAUG,MAAK,SAASC,MAAOC,QAC7BJ,OAAOK,OAAO,kBAAoBD,OAAOd,MAAQ,KAAOc,OAAOE,MAAQ,gBAE3E9B,EAAEC,KAAK8B,YAAY,+CAGpBhD"}
\ No newline at end of file
diff --git a/amd/src/modal_embedquestion_question_bank.js b/amd/src/modal_embedquestion_question_bank.js
new file mode 100644
index 0000000..97353a6
--- /dev/null
+++ b/amd/src/modal_embedquestion_question_bank.js
@@ -0,0 +1,168 @@
+// 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 .
+
+/**
+ * Contain the logic for the question bank modal.
+ *
+ * @module filter_embedquestion/modal_embedquestion_question_bank
+ * @copyright 2025 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+import Modal from 'mod_quiz/add_question_modal';
+import * as Fragment from 'core/fragment';
+import {getString} from 'core/str';
+import AutoComplete from 'core/form-autocomplete';
+
+const SELECTORS = {
+ SWITCH_TO_OTHER_BANK: 'button[data-action="switch-question-bank"]',
+ BANK_SEARCH: '#searchbanks',
+ NEW_BANKMOD_ID: 'data-newmodid',
+ ANCHOR: 'a[href]',
+ SORTERS: '.sorters',
+ GO_BACK_BUTTON: 'button[data-action="go-back"]',
+};
+
+/**
+ * Class representing a modal for selecting a question bank to embed questions from.
+ */
+export class ModalEmbedQuestionQuestionBank extends Modal {
+ static TYPE = 'filter_embedquestion-question-bank';
+
+ configure(modalConfig) {
+ // Add question modals are always large.
+ modalConfig.large = true;
+
+ // Always show on creation.
+ modalConfig.show = true;
+ modalConfig.removeOnClose = true;
+
+ // Apply question modal configuration.
+ this.setContextId(modalConfig.contextId);
+ this.setAddOnPageId(modalConfig.addOnPage);
+ this.courseId = modalConfig.courseId;
+ this.bankCmId = modalConfig.bankCmId;
+ // Store the original title of the modal, so we can revert back to it once we have switched to another bank.
+ this.originalTitle = modalConfig.title;
+ this.currentEditor = modalConfig.editor;
+ // Apply standard configuration.
+ super.configure(modalConfig);
+ }
+
+ /**
+ * Show the modal and load the content for switching question banks.
+ *
+ * @method show
+ */
+ show() {
+ this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH);
+ return super.show(this);
+ }
+
+ /**
+ * Switch to the embed question modal for a specific question bank.
+ * This will destroy the current modal and dispatch an event to switch to the new modal.
+ *
+ * @param {String} bankCmid - The course module ID of the question bank to switch to.
+ * @method fireQbankSelectedEvent
+ */
+ fireQbankSelectedEvent(bankCmid) {
+ this.destroy();
+ const event = new CustomEvent('filter_embedquestion:qbank_selected', {
+ detail: {bankCmid: bankCmid, editor: this.currentEditor},
+ });
+ document.dispatchEvent(event);
+ }
+
+ /**
+ * Set up all the event handling for the modal.
+ *
+ * @method registerEventListeners
+ */
+ registerEventListeners() {
+ // Apply parent event listeners.
+ super.registerEventListeners(this);
+
+ this.getModal().on('click', SELECTORS.ANCHOR, (e) => {
+ const anchorElement = e.currentTarget;
+ e.preventDefault();
+ this.fireQbankSelectedEvent(anchorElement.getAttribute(SELECTORS.NEW_BANKMOD_ID));
+ });
+
+ this.getModal().on('click', SELECTORS.GO_BACK_BUTTON, (e) => {
+ e.preventDefault();
+ this.fireQbankSelectedEvent(e.currentTarget.value);
+ });
+ }
+
+ /**
+ * Update the modal with a list of banks to switch to and enhance the standard selects to Autocomplete fields.
+ *
+ * @param {String} Selector for the original select element.
+ * @return {Promise} Modal.
+ */
+ async handleSwitchBankContentReload(Selector) {
+ this.setTitle(getString('selectquestionbank', 'mod_quiz'));
+
+ // Create a 'Go back' button and set it in the footer.
+ const el = document.createElement('button');
+ el.classList.add('btn', 'btn-primary');
+ el.textContent = await getString('gobacktoquiz', 'mod_quiz');
+ el.setAttribute('data-action', 'go-back');
+ el.setAttribute('value', this.bankCmId);
+ this.setFooter(el);
+
+ this.setBody(
+ Fragment.loadFragment(
+ 'filter_embedquestion',
+ 'switch_question_bank',
+ this.getContextId(),
+ {
+ 'courseid': this.courseId,
+ })
+ );
+ const placeholder = await getString('searchbyname', 'mod_quiz');
+ await this.getBodyPromise();
+ await AutoComplete.enhance(
+ Selector,
+ false,
+ 'core_question/question_banks_datasource',
+ placeholder,
+ false,
+ true,
+ '',
+ true
+ );
+
+ // Hide the selection element as we don't need it.
+ document.querySelector('.search-banks .form-autocomplete-selection')?.classList.add('d-none');
+ // Add a change listener to get the selected value.
+ const bankSearchEl = document.querySelector(Selector);
+ if (bankSearchEl) {
+ bankSearchEl.addEventListener('change', (e) => {
+ // This will be the chosen qbankCmid.
+ const selectedValue = e.target.value;
+ if (selectedValue > 0) {
+ this.fireQbankSelectedEvent(selectedValue);
+ }
+ });
+ }
+ return this;
+ }
+}
+
+export default {
+ ModalEmbedQuestionQuestionBank,
+};
+ModalEmbedQuestionQuestionBank.registerModalType();
\ No newline at end of file
diff --git a/amd/src/questionid_choice_updater.js b/amd/src/questionid_choice_updater.js
index 8fd38e6..30ec68f 100644
--- a/amd/src/questionid_choice_updater.js
+++ b/amd/src/questionid_choice_updater.js
@@ -21,40 +21,112 @@
* @copyright 2018 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define(['jquery', 'core/ajax'], function($, Ajax) {
+
+define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core_user/repository'],
+ function($, Ajax, Str, Notification, UserRepository) {
var t = {
/**
* Initialise the handling.
+ *
+ * @param {string} defaultQbankCmid - The default question bank to select, if any.
*/
- init: function() {
+ init: function(defaultQbankCmid) {
+ $('select#id_qbankcmid').on('change', t.qbankChanged);
$('select#id_categoryidnumber').on('change', t.categoryChanged);
- t.lastCategory = null;
+
+ t.lastQbank = $('select#id_qbankcmid').val();
+ t.lastCategory = $('select#id_categoryidnumber').val();
+ var selectedText = $('#id_qbankcmid option:selected').text();
+ Str.get_string('currentbank', 'mod_quiz', selectedText)
+ .then(function(string) {
+ $('#id_questionheadercontainer h5').text(string);
+ return;
+ }).catch(Notification.exception);
+ if (defaultQbankCmid) {
+ // If a default question bank is set, we need to trigger the change event to load the categories.
+ $('select#id_qbankcmid').val(defaultQbankCmid).trigger('change');
+ }
},
/**
* Used to track when the category really changes.
*/
lastCategory: null,
+ /**
+ * Used to track when the question bank really changes.
+ */
+ lastQbank: null,
/**
* Source of data for Ajax element.
*/
categoryChanged: function() {
- if ($('select#id_categoryidnumber').val() === t.lastCategory) {
- return;
- }
-
M.util.js_pending('filter_embedquestion-get_questions');
t.lastCategory = $('select#id_categoryidnumber').val();
-
if (t.lastCategory === '') {
t.updateChoices([]);
} else {
Ajax.call([{
methodname: 'filter_embedquestion_get_sharable_question_choices',
- args: {courseid: $('input[name=courseid]').val(), categoryidnumber: t.lastCategory}
- }])[0].done(t.updateChoices);
+ args: {cmid: t.lastQbank, categoryidnumber: t.lastCategory},
+ }])[0].then(t.updateChoices).catch(Notification.exception);
+ $('select#id_questionidnumber').attr('disabled', false);
+ }
+ },
+
+ /**
+ * Source of data for Ajax element.
+ */
+ qbankChanged: function() {
+ if ($('select#id_qbankcmid').val() === t.lastQbank) {
+ return;
}
+ M.util.js_pending('filter_embedquestion-get_categories');
+ t.lastQbank = $('select#id_qbankcmid').val();
+ // Update the heading immediately when selection changes.
+ var selectedText = $('#id_qbankcmid option:selected').text();
+ Str.get_string('currentbank', 'mod_quiz', selectedText)
+ .then(function(string) {
+ $('#id_questionheadercontainer h5').text(string);
+ return;
+ }).catch(Notification.exception);
+ var prefKey = 'filter_embedquestion_userdefaultqbank';
+ var courseId = document.querySelector('input[name="courseid"]').value;
+ var isSameCourse = document.querySelector('input[name="issamecourse"]').value;
+ if (isSameCourse) {
+ UserRepository.getUserPreference(prefKey).then(current => {
+ let prefs = current ? JSON.parse(current) : {};
+ prefs[courseId] = t.lastQbank;
+ return UserRepository.setUserPreference(prefKey, JSON.stringify(prefs));
+ }).catch(Notification.exception);
+ }
+ if ($('select#id_qbankcmid').val() === '') {
+ t.updateCategories([]);
+ M.util.js_pending('filter_embedquestion-get_questions');
+ t.updateChoices([]);
+ } else {
+ Ajax.call([{
+ methodname: 'filter_embedquestion_get_sharable_category_choices',
+ args: {cmid: t.lastQbank}
+ }])[0].then(t.updateCategories).catch(Notification.exception);
+ M.util.js_pending('filter_embedquestion-get_questions');
+ t.updateChoices([]);
+ }
+ },
+
+ /**
+ * Update the contents of the Question select with the results of the AJAX call.
+ *
+ * @param {Array} response - array of options, each has fields value and label.
+ */
+ updateCategories: function(response) {
+ var select = $('select#id_categoryidnumber');
+
+ select.empty();
+ $(response).each(function(index, option) {
+ select.append('');
+ });
+ M.util.js_complete('filter_embedquestion-get_categories');
},
/**
diff --git a/changes.md b/changes.md
index fae6f79..c1bffea 100644
--- a/changes.md
+++ b/changes.md
@@ -1,5 +1,14 @@
# Change log for the embed questions filter
+## Changes in 2.4
+
+* This version works with Moodle 5.0+
+* Add switch question bank dialog to the embedded question UI, so that teachers can
+ select question bank from any course they have access to.
+* Add user preference to control whether the embedded question UI to select default question bank
+ from current course
+
+
## Changes in 2.3
* This version works with Moodle 4.5.
diff --git a/classes/attempt.php b/classes/attempt.php
index 037d7af..9210f37 100644
--- a/classes/attempt.php
+++ b/classes/attempt.php
@@ -40,6 +40,11 @@ class attempt {
*/
protected $user;
+ /**
+ * @var \context the context in which the question is belong to.
+ */
+ protected $context;
+
/**
* @var \stdClass the question category we are in.
*/
@@ -88,18 +93,49 @@ public function __construct(embed_id $embedid, embed_location $embedlocation,
$this->embedlocation = $embedlocation;
$this->user = $user;
$this->options = $options;
- $this->category = $this->find_category($embedid->categoryidnumber);
+
+ $courseid = utils::get_relevant_courseid($this->embedlocation->context);
+ if ($embedid->courseshortname) {
+ $courseid = utils::get_courseid_by_course_shortname($embedid->courseshortname);
+ }
+ $this->category = $this->find_category(
+ $embedid->categoryidnumber,
+ $courseid,
+ $embedid->questionbankidnumber
+ );
}
/**
* Find the category for a category idnumber, if it exists.
*
* @param string $categoryidnumber idnumber of the category to use.
+ * @param int $courseid the id of the course question banks are being shared from.
+ * @param string|null $qbankidnumber the idnumber of the question bank,
* @return \stdClass if the category was OK. If not null and problem and problemdetails are set.
*/
- private function find_category(string $categoryidnumber): ?\stdClass {
- $coursecontext = \context_course::instance(utils::get_relevant_courseid($this->embedlocation->context));
- $category = utils::get_category_by_idnumber($coursecontext, $categoryidnumber);
+ private function find_category(string $categoryidnumber, int $courseid,
+ ?string $qbankidnumber = null): ?\stdClass {
+ $cmid = utils::get_qbank_by_idnumber($courseid, $qbankidnumber);
+ if (!$cmid || $cmid === -1) {
+ if ($cmid === -1) {
+ $this->problem = 'invalidquestionbank';
+ return null;
+ } else {
+ if ($qbankidnumber) {
+ $this->problem = 'invalidqbankidnumber';
+ $this->problemdetails = [
+ 'qbankidnumber' => $qbankidnumber,
+ 'contextname' => $this->embedlocation->context_name_for_errors(),
+ ];
+ } else {
+ $this->problem = 'invalidquestionbank';
+ }
+ return null;
+ }
+ }
+ $context = \context_module::instance($cmid);
+ $this->context = $context;
+ $category = utils::get_category_by_idnumber($context, $categoryidnumber);
if (!$category) {
$this->problem = 'invalidcategory';
$this->problemdetails = [
@@ -443,7 +479,7 @@ public function render_question(\filter_embedquestion\output\renderer $renderer)
$relevantcourseid = utils::get_relevant_courseid($this->embedlocation->context);
if (question_has_capability_on($this->current_question(), 'edit')) {
$this->options->editquestionparams =
- ['returnurl' => $this->embedlocation->pageurl, 'courseid' => $relevantcourseid];
+ ['returnurl' => $this->embedlocation->pageurl, 'cmid' => $this->context->instanceid];
}
// Show an 'Question bank' action to those with permissions.
diff --git a/classes/embed_id.php b/classes/embed_id.php
index 731a4f6..3a55c98 100644
--- a/classes/embed_id.php
+++ b/classes/embed_id.php
@@ -40,15 +40,29 @@ class embed_id {
*/
public $questionidnumber;
+ /**
+ * @var string the course shortname.
+ */
+ public $courseshortname;
+ /**
+ * @var string the question bank idnumber.
+ */
+ public $questionbankidnumber;
+
/**
* Simple embed_id constructor.
*
* @param string $categoryidnumber the category idnumber.
* @param string $questionidnumber the question idnumber.
+ * @param null|string $questionbankidnumber the question bank idnumber, optional.
+ * @param null|string $courseshortname the course shortname, optional.
*/
- public function __construct(string $categoryidnumber, string $questionidnumber) {
+ public function __construct(string $categoryidnumber, string $questionidnumber,
+ ?string $questionbankidnumber = null, ?string $courseshortname = null) {
$this->categoryidnumber = $categoryidnumber;
$this->questionidnumber = $questionidnumber;
+ $this->questionbankidnumber = $questionbankidnumber;
+ $this->courseshortname = $courseshortname;
}
/**
@@ -61,10 +75,15 @@ public static function create_from_string(string $questioninfo): ?embed_id {
if (strpos($questioninfo, '/') === false) {
return null;
}
-
- list($categoryidnumber, $questionidnumber) = explode('/', $questioninfo, 2);
+ $parts = explode('/', $questioninfo);
+ // Ensure 4 parts, right-aligned.
+ $parts = array_pad($parts, -4, '');
+ // Assign in order: courseshortname, qbankid, categoryid, questionid.
+ [$courseshortname, $questionbankidnumber, $categoryidnumber, $questionidnumber] = $parts;
return new embed_id(str_replace(self::ESCAPED, self::TO_ESCAPE, $categoryidnumber),
- str_replace(self::ESCAPED, self::TO_ESCAPE, $questionidnumber));
+ str_replace(self::ESCAPED, self::TO_ESCAPE, $questionidnumber),
+ str_replace(self::ESCAPED, self::TO_ESCAPE, $questionbankidnumber),
+ str_replace(self::ESCAPED, self::TO_ESCAPE, $courseshortname));
}
/**
@@ -73,7 +92,13 @@ public static function create_from_string(string $questioninfo): ?embed_id {
* @return string categoryidnumber/questionidnumber.
*/
public function __toString(): string {
- return str_replace(self::TO_ESCAPE, self::ESCAPED, $this->categoryidnumber) . '/' .
+ $optional = !empty($this->courseshortname) ?
+ str_replace(self::TO_ESCAPE, self::ESCAPED, $this->courseshortname) . '/' : '';
+ $optional .= !empty($this->questionbankidnumber) ?
+ str_replace(self::TO_ESCAPE, self::ESCAPED, $this->questionbankidnumber) . '/' : '';
+
+ return $optional .
+ str_replace(self::TO_ESCAPE, self::ESCAPED, $this->categoryidnumber) . '/' .
str_replace(self::TO_ESCAPE, self::ESCAPED, $this->questionidnumber);
}
@@ -84,7 +109,11 @@ public function __toString(): string {
* that are safe in HTML id attributes.
*/
public function to_html_id(): string {
- return clean_param($this->categoryidnumber, PARAM_ALPHANUMEXT) . '/' .
+ $optional = !empty($this->courseshortname) ? clean_param($this->courseshortname, PARAM_ALPHANUMEXT) . '/' : '';
+ $optional .= !empty($this->questionbankidnumber) ? clean_param($this->questionbankidnumber, PARAM_ALPHANUMEXT) . '/' : '';
+
+ return $optional .
+ clean_param($this->categoryidnumber, PARAM_ALPHANUMEXT) . '/' .
clean_param($this->questionidnumber, PARAM_ALPHANUMEXT);
}
@@ -94,6 +123,8 @@ public function to_html_id(): string {
* @param \moodle_url $url the URL to add to.
*/
public function add_params_to_url(\moodle_url $url): void {
+ $url->param('courseshortname', $this->courseshortname);
+ $url->param('questionbankidnumber', $this->questionbankidnumber);
$url->param('catid', $this->categoryidnumber);
$url->param('qid', $this->questionidnumber);
}
diff --git a/classes/external.php b/classes/external.php
index 5e8233a..1c50202 100644
--- a/classes/external.php
+++ b/classes/external.php
@@ -16,6 +16,8 @@
namespace filter_embedquestion;
+use core\exception\moodle_exception;
+
defined('MOODLE_INTERNAL') || die();
global $CFG;
@@ -36,7 +38,7 @@ class external extends \external_api {
*/
public static function get_sharable_question_choices_parameters(): \external_function_parameters {
return new \external_function_parameters([
- 'courseid' => new \external_value(PARAM_INT, 'Course id.'),
+ 'cmid' => new \external_value(PARAM_INT, 'Course module ID'),
'categoryidnumber' => new \external_value(PARAM_RAW, 'Idnumber of the question category.'),
]);
}
@@ -66,18 +68,18 @@ public static function get_sharable_question_choices_is_allowed_from_ajax(): boo
/**
* Get the list of sharable questions in a category.
*
- * @param int $courseid the course whose question bank we are sharing from.
+ * @param int $cmid the course module id.
* @param string $categoryidnumber the idnumber of the question category.
*
* @return array of arrays with two elements, keys value and label.
*/
- public static function get_sharable_question_choices(int $courseid, string $categoryidnumber): array {
+ public static function get_sharable_question_choices(int $cmid, string $categoryidnumber): array {
global $USER;
self::validate_parameters(self::get_sharable_question_choices_parameters(),
- ['courseid' => $courseid, 'categoryidnumber' => $categoryidnumber]);
+ ['cmid' => $cmid, 'categoryidnumber' => $categoryidnumber]);
- $context = \context_course::instance($courseid);
+ $context = \context_module::instance($cmid);
self::validate_context($context);
if (has_capability('moodle/question:useall', $context)) {
@@ -111,8 +113,8 @@ public static function get_embed_code_parameters(): \external_function_parameter
// We can't use things like PARAM_INT for things like variant, because it is
// and int of '' for not set.
return new \external_function_parameters([
- 'courseid' => new \external_value(PARAM_INT,
- 'Course id.'),
+ 'cmid' => new \external_value(PARAM_INT,
+ 'Course module id of the question bank.'),
'categoryidnumber' => new \external_value(PARAM_RAW,
'Id number of the question category.'),
'questionidnumber' => new \external_value(PARAM_RAW,
@@ -166,7 +168,7 @@ public static function get_embed_code_is_allowed_from_ajax(): bool {
* Given the course id, category and question idnumbers, and any display options,
* return the {Q{...}Q} code needed to embed this question.
*
- * @param int $courseid the id of the course we are embedding questions from.
+ * @param int $cmid the course module id of the question bank.
* @param string $categoryidnumber the idnumber of the question category.
* @param string $questionidnumber the idnumber of the question to be embedded, or '*' to mean a question picked at random.
* @param string $iframedescription the iframe description.
@@ -184,7 +186,7 @@ public static function get_embed_code_is_allowed_from_ajax(): bool {
*
* @return string the embed code.
*/
- public static function get_embed_code(int $courseid, string $categoryidnumber, string $questionidnumber,
+ public static function get_embed_code(int $cmid, string $categoryidnumber, string $questionidnumber,
string $iframedescription, string $behaviour, string $maxmark, string $variant, string $correctness,
string $marks, string $markdp, string $feedback, string $generalfeedback, string $rightanswer, string $history,
string $forcedlanguage): string {
@@ -193,7 +195,7 @@ public static function get_embed_code(int $courseid, string $categoryidnumber, s
self::validate_parameters(
self::get_embed_code_parameters(),
[
- 'courseid' => $courseid,
+ 'cmid' => $cmid,
'categoryidnumber' => $categoryidnumber,
'questionidnumber' => $questionidnumber,
'iframedescription' => $iframedescription,
@@ -210,23 +212,31 @@ public static function get_embed_code(int $courseid, string $categoryidnumber, s
'forcedlanguage' => $forcedlanguage,
]
);
-
- $context = \context_course::instance($courseid);
- self::validate_context($context);
-
+ $context = \context_module::instance($cmid);
// Check permissions.
+ self::validate_context($context);
+ if (!utils::has_permission($context) || !get_coursemodule_from_id('qbank', $cmid)) {
+ throw new moodle_exception('errornopermissions', 'filter_embedquestion');
+ }
require_once($CFG->libdir . '/questionlib.php');
$category = utils::get_category_by_idnumber($context, $categoryidnumber);
if ($questionidnumber === '*') {
- $context = \context_course::instance($courseid);
require_capability('moodle/question:useall', $context);
} else {
$questiondata = utils::get_question_by_idnumber($category->id, $questionidnumber);
$question = \question_bank::load_question($questiondata->id);
question_require_capability_on($question, 'use');
}
+ // When we get the question bank created by system, usually they don't have idnumber
+ // So we need to add '*' to questionbankidnumber to make sure the question bank can be found.
+ [$embedcourse, $cm] = get_course_and_cm_from_cmid($cmid, 'qbank');
+ $questionbankidnumber = $cm->idnumber;
+ if (empty($questionbankidnumber)) {
+ $questionbankidnumber = '*';
+ }
$fromform = new \stdClass();
- $fromform->courseid = $courseid;
+ $fromform->questionbankidnumber = $questionbankidnumber;
+ $fromform->courseshortname = $embedcourse->shortname;
$fromform->categoryidnumber = $categoryidnumber;
$fromform->questionidnumber = $questionidnumber;
$fromform->iframedescription = $iframedescription;
diff --git a/classes/external/get_sharable_category_choices.php b/classes/external/get_sharable_category_choices.php
new file mode 100644
index 0000000..1bd3f3c
--- /dev/null
+++ b/classes/external/get_sharable_category_choices.php
@@ -0,0 +1,95 @@
+.
+
+namespace filter_embedquestion\external;
+
+use core_external\external_api;
+use core_external\external_function_parameters;
+use core_external\external_value;
+use core_external\external_multiple_structure;
+use core_external\external_single_structure;
+use core_external\external_description;
+use filter_embedquestion\utils;
+
+/**
+ * External API to get the list of sharable question categories.
+ *
+ * @package filter_embedquestion
+ * @copyright 2025 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class get_sharable_category_choices extends external_api {
+ /**
+ * Returns parameter types for get_sharable_category_choices function.
+ *
+ * @return external_function_parameters Parameters
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters([
+ 'cmid' => new external_value(PARAM_INT, 'Course module ID'),
+ ]);
+ }
+
+ /**
+ * Returns result type for get_sharable_category_choices function.
+ *
+ * @return external_description Result type
+ */
+ public static function execute_returns(): external_description {
+ return new external_multiple_structure(
+ new external_single_structure([
+ 'value' => new external_value(PARAM_RAW, 'Choice value to return from the form.'),
+ 'label' => new external_value(PARAM_RAW, 'Choice name, to display to users.'),
+ ]));
+ }
+
+ /**
+ * Get the list of sharable categories.
+ *
+ * @param int $cmid the course module ID of the question bank.
+ *
+ * @return array of arrays with two elements, keys value and label.
+ */
+ public static function execute(int $cmid): array {
+ global $USER;
+
+ self::validate_parameters(self::execute_parameters(),
+ ['cmid' => $cmid]);
+
+ $context = \context_module::instance($cmid);
+ self::validate_context($context);
+
+ if (has_capability('moodle/question:useall', $context)) {
+ $userlimit = null;
+
+ } else if (has_capability('moodle/question:usemine', $context)) {
+ $userlimit = $USER->id;
+ } else {
+ throw new \coding_exception('This user is not allowed to embed questions.');
+ }
+
+ $categories = utils::get_categories_with_sharable_question_choices($context, $userlimit);
+ if (!$categories) {
+ throw new \coding_exception('Unknown question category.');
+ }
+
+ $out = [];
+ foreach ($categories as $value => $label) {
+ $out[] = ['value' => $value, 'label' => $label];
+ }
+ return $out;
+ }
+}
diff --git a/classes/form/embed_options_form.php b/classes/form/embed_options_form.php
index e72ee36..839fb46 100644
--- a/classes/form/embed_options_form.php
+++ b/classes/form/embed_options_form.php
@@ -27,9 +27,11 @@
global $CFG;
require_once($CFG->libdir . '/formslib.php');
+require_once($CFG->libdir . '/modinfolib.php');
+
use filter_embedquestion\utils;
use filter_embedquestion\question_options;
-
+use cm_info;
/**
* Form to let users edit all the options for embedding a question.
@@ -38,36 +40,73 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class embed_options_form extends \moodleform {
-
#[\Override]
public function definition() {
- global $PAGE;
+ global $PAGE, $OUTPUT;
$mform = $this->_form;
+ /** @var \context $context */
+ $context = $this->_customdata['context'];
+ $courseshortname = $this->_customdata['courseshortname'] ?? null;
+ $defaultqbankcmid = $this->_customdata['qbankcmid'] ?? null;
+ $embedcode = $this->_customdata['embedcode'] ?? null;
+
// The default form id ('mform1') is also highly likely to be the same as the
// id of the form in the background when we are shown in an atto editor pop-up.
// Therefore, set something different.
$mform->updateAttributes(['id' => 'embedqform']);
- /** @var \context $context */
- $context = $this->_customdata['context'];
-
$defaultoptions = new question_options();
-
+ $mform->addElement('hidden', 'contextid', $context->id);
+ $mform->setType('contextid', PARAM_INT);
$mform->addElement('hidden', 'courseid', $context->instanceid);
$mform->setType('courseid', PARAM_INT);
-
+ $mform->addElement('hidden', 'issamecourse', 1);
+ $mform->setType('issamecourse', PARAM_INT);
$mform->addElement('header', 'questionheader', get_string('whichquestion', 'filter_embedquestion'));
+ $prefs = [];
+
+ // Only load user preference if we do not have a default question bank cmid or embed code.
+ if (!$defaultqbankcmid && !$embedcode) {
+ // Retrieve existing preference (empty array if none).
+ $prefs = json_decode(get_user_preferences('filter_embedquestion_userdefaultqbank', '{}'));
+ }
+ $cmid = !empty($defaultqbankcmid) ? $defaultqbankcmid : ($prefs->{$context->instanceid} ?? null);
+ // If we have default question bank cmid, we will use it to get the course id.
+ if ($cmid) {
+ [, $cm] = get_course_and_cm_from_cmid($cmid);
+ $cminfo = cm_info::create($cm);
+ $courseid = $cminfo->get_course()->id;
+ } else if ($courseshortname) {
+ $courseid = utils::get_courseid_by_course_shortname($courseshortname);
+ if ($courseid != $context->instanceid) {
+ $mform->setDefault('issamecourse', 0);
+ }
+ } else {
+ $courseid = $context->instanceid;
+ }
+ $qbanks = utils::get_shareable_question_banks($courseid, $this->get_user_retriction());
+ $qbanksselectoptions = utils::create_select_qbank_choices($qbanks);
+ // If we have a default question bank cmid, we will use it to set the default value.
+ // If the default question bank cmid is not in the list of question banks, we will add it.
+ if ($cmid && empty($qbanksselectoptions[$cmid])) {
+ $qbanksselectoptions[$cmid] = format_string($cminfo->name);
+ }
+ $mform->addElement('html', $OUTPUT->render_from_template('mod_quiz/switch_bank_header',
+ ['currentbank' => reset($qbanksselectoptions)]));
+ $mform->addElement('select', 'qbankcmid', get_string('questionbank', 'question'),
+ $qbanksselectoptions);
+ $mform->addRule('qbankcmid', null, 'required', null, 'client');
+
$mform->addElement('select', 'categoryidnumber', get_string('questioncategory', 'question'),
- utils::get_categories_with_sharable_question_choices(
- $context, $this->get_user_retriction()));
+ []);
$mform->addRule('categoryidnumber', null, 'required', null, 'client');
-
+ $mform->disabledIf('questionidnumber', 'qbankcmid', 'eq', '');
$mform->addElement('select', 'questionidnumber', get_string('question'), []);
$mform->addRule('questionidnumber', null, 'required', null, 'client');
$mform->disabledIf('questionidnumber', 'categoryidnumber', 'eq', '');
- $PAGE->requires->js_call_amd('filter_embedquestion/questionid_choice_updater', 'init');
+ $PAGE->requires->js_call_amd('filter_embedquestion/questionid_choice_updater', 'init', [$cmid]);
$mform->addElement('text', 'iframedescription', get_string('iframedescription', 'filter_embedquestion'),
['size' => 100]);
@@ -164,17 +203,38 @@ protected function get_marks_options(int $default): array {
public function definition_after_data() {
parent::definition_after_data();
$mform = $this->_form;
+ $qbankcmid = $mform->getElementValue('qbankcmid');
+ if (is_null($qbankcmid)) {
+ return;
+ }
$categoryidnumbers = $mform->getElementValue('categoryidnumber');
if (is_null($categoryidnumbers)) {
return;
}
+ $qbankcmid = $qbankcmid[0];
+ if (!$qbankcmid || $qbankcmid == -1) {
+ return;
+ }
$categoryidnumber = $categoryidnumbers[0];
if ($categoryidnumber === '' || $categoryidnumber === null) {
return;
}
- $category = utils::get_category_by_idnumber($this->_customdata['context'], $categoryidnumber);
+ [$course,] = get_course_and_cm_from_cmid($qbankcmid);
+ $qbanks = utils::get_shareable_question_banks($course->id, $this->get_user_retriction());
+ $qbanksselectoptions = utils::create_select_qbank_choices($qbanks);
+ $element = $mform->getElement('qbankcmid');
+ // Clear the existing options, so that we can load the new ones.
+ $element->_options = [];
+ $mform->getElement('qbankcmid')->loadArray($qbanksselectoptions);
+ $context = \context_module::instance($qbankcmid);
+ $mform->setDefault('qbankcmid', $qbankcmid);
+
+ $categories = utils::get_categories_with_sharable_question_choices($context,
+ $this->get_user_retriction());
+ $mform->getElement('categoryidnumber')->loadArray($categories);
+ $category = utils::get_category_by_idnumber($context, $categoryidnumber);
if ($category) {
$choices = utils::get_sharable_question_choices($category->id, $this->get_user_retriction());
$mform->getElement('questionidnumber')->loadArray($choices);
@@ -205,10 +265,17 @@ protected function get_user_retriction(): ?int {
#[\Override]
public function validation($data, $files) {
$errors = parent::validation($data, $files);
- $context = $this->_customdata['context'];
-
- $category = utils::get_category_by_idnumber($context, $data['categoryidnumber']);
+ $qbankcontext = \context_module::instance($data['qbankcmid']);
+ if (!$qbankcontext) {
+ $errors['qbankcmid'] = get_string('errorquestionbanknotfound', 'filter_embedquestion');
+ return $errors;
+ }
+ if (!utils::has_permission($qbankcontext) || !get_coursemodule_from_id('qbank', $data['qbankcmid'])) {
+ $errors['qbankcmid'] = get_string('errornopermissions', 'filter_embedquestion');
+ return $errors;
+ }
+ $category = utils::get_category_by_idnumber($qbankcontext, $data['categoryidnumber']);
$questiondata = false;
if (isset($data['questionidnumber'])) {
$questiondata = utils::get_question_by_idnumber($category->id, $data['questionidnumber']);
diff --git a/classes/output/embed_iframe.php b/classes/output/embed_iframe.php
index 0b79d74..c927c9d 100644
--- a/classes/output/embed_iframe.php
+++ b/classes/output/embed_iframe.php
@@ -51,7 +51,9 @@ public function export_for_template(renderer_base $output): array {
'name' => null,
'iframedescription' => format_string($this->iframedescription),
'embedid' => (new embed_id($this->showquestionurl->param('catid'),
- $this->showquestionurl->param('qid')))->to_html_id(),
+ $this->showquestionurl->param('qid'),
+ $this->showquestionurl->param('questionbankidnumber'),
+ $this->showquestionurl->param('courseshortname')))->to_html_id(),
];
if (defined('BEHAT_SITE_RUNNING')) {
$data['name'] = 'filter_embedquestion-iframe';
diff --git a/classes/output/switch_question_bank.php b/classes/output/switch_question_bank.php
new file mode 100644
index 0000000..827712c
--- /dev/null
+++ b/classes/output/switch_question_bank.php
@@ -0,0 +1,72 @@
+.
+
+/**
+ * Switch question bank output class for the embed question filter.
+ *
+ * @package filter_embedquestion
+ * @copyright 2025 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace filter_embedquestion\output;
+
+use core_question\local\bank\question_bank_helper;
+use renderer_base;
+
+/**
+ * Get the switch question bank rendered content. Displays lists of shared banks the viewing user has access to.
+ */
+class switch_question_bank implements \renderable, \templatable {
+
+ /**
+ * Instantiate the output class.
+ *
+ * @param int $courseid of the current course.
+ * @param int $userid of the user viewing the page.
+ */
+ public function __construct(
+ /** @var int id of the current course */
+ private readonly int $courseid,
+ /** @var int id of the user viewing the page */
+ private readonly int $userid
+ ) {
+ }
+
+ /**
+ * Create a list of question banks the user has access to for the template.
+ *
+ * @param renderer_base $output
+ * @return array
+ */
+ public function export_for_template(renderer_base $output) {
+ $capabilities = ['moodle/question:useall', 'moodle/question:usemine'];
+ $contextcourse = \context_course::instance($this->courseid);
+ $coursesharedbanks = question_bank_helper::get_activity_instances_with_shareable_questions(
+ incourseids: [$this->courseid],
+ havingcap: $capabilities,
+ );
+ $recentlyviewedbanks = question_bank_helper::get_recently_used_open_banks($this->userid, havingcap: $capabilities);
+
+ return [
+ 'hascoursesharedbanks' => !empty($coursesharedbanks),
+ 'coursesharedbanks' => $coursesharedbanks,
+ 'hasrecentlyviewedbanks' => !empty($recentlyviewedbanks),
+ 'recentlyviewedbanks' => $recentlyviewedbanks,
+ 'contextid' => $contextcourse->id,
+ ];
+ }
+}
diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php
index 6816c99..8a8d6f6 100644
--- a/classes/privacy/provider.php
+++ b/classes/privacy/provider.php
@@ -16,6 +16,8 @@
namespace filter_embedquestion\privacy;
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\writer;
/**
* Privacy Subsystem for filter_embedquestion implementing null_provider.
*
@@ -23,15 +25,24 @@
* @copyright 2018 The Open Univesity
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class provider implements \core_privacy\local\metadata\null_provider {
+class provider implements \core_privacy\local\metadata\provider,
+ \core_privacy\local\request\user_preference_provider {
+ #[\Override]
+ public static function get_metadata(collection $collection): collection {
+ $collection->add_user_preference('filter_embedquestion_userdefaultqbank', 'privacy:preference:defaultqbank');
+ return $collection;
+ }
- /**
- * Get the language string identifier with the component's language
- * file to explain why this plugin stores no data.
- *
- * @return string The language string identifier with the component's language.
- */
- public static function get_reason(): string {
- return 'privacy:metadata';
+ #[\Override]
+ public static function export_user_preferences(int $userid): void {
+ $defaultqbanks = get_user_preferences('filter_embedquestion_userdefaultqbank', '{}', $userid);
+ if ($defaultqbanks !== '{}') {
+ writer::export_user_preference(
+ 'filter_embedquestion',
+ 'userdefaultqbank',
+ $defaultqbanks,
+ get_string('defaultqbank', 'filter_embedquestion')
+ );
+ }
}
}
diff --git a/classes/question_options.php b/classes/question_options.php
index 9c7b812..b691cba 100644
--- a/classes/question_options.php
+++ b/classes/question_options.php
@@ -176,7 +176,8 @@ public function add_params_to_url(\moodle_url $url): void {
*/
public static function get_embed_from_form_options(\stdClass $fromform): string {
- $embedid = new embed_id($fromform->categoryidnumber, $fromform->questionidnumber);
+ $embedid = new embed_id($fromform->categoryidnumber, $fromform->questionidnumber,
+ $fromform->questionbankidnumber ?? '', $fromform->courseshortname ?? '');
$parts = [(string) $embedid];
foreach (self::get_field_types() as $field => $type) {
if (!isset($fromform->$field) || $fromform->$field === '') {
diff --git a/classes/utils.php b/classes/utils.php
index 05e4439..03c7029 100644
--- a/classes/utils.php
+++ b/classes/utils.php
@@ -137,6 +137,49 @@ public static function get_show_url(embed_id $embedid, embed_location $embedloca
return $url;
}
+ /**
+ * Find a question bank with a given idnumber in a given course.
+ *
+ * @param int $courseid the id of the course to look in.
+ * @param string|null $qbankidnumber the idnumber of the question bank to look for.
+ * @param int|null $userid if set, only count question banks created by this user.
+ * @return int|null cmid or null if not found.
+ * If there are multiple question banks in the course, and no idnumber is given, return -1 only if there is no
+ * question bank with no idnumber created by system.
+ */
+ public static function get_qbank_by_idnumber(int $courseid, ?string $qbankidnumber = null, ?int $userid = null): ?int {
+ $qbanks = self::get_shareable_question_banks($courseid, $userid, $qbankidnumber);
+ if (empty($qbanks)) {
+ return null;
+ } else if (count($qbanks) === 1) {
+ $cmid = reset($qbanks)->cmid;
+ } else {
+ if (!$qbankidnumber || $qbankidnumber === '*') {
+ // Multiple qbanks in this course.
+ $qbankswithoutidnumber = array_filter($qbanks, function($qbank) {
+ return empty($qbank->qbankidnumber);
+ });
+ if (count($qbankswithoutidnumber) === 1) {
+ $cmid = reset($qbankswithoutidnumber)->cmid;
+ } else {
+ // There are multiple question banks without id number and we can't determine which one to use.
+ return -1;
+ }
+ } else {
+ // We have a qbankidnumber, so we can filter the list.
+ $match = array_filter($qbanks, fn($q) => $q->qbankidnumber === $qbankidnumber);
+ if (count($match) === 1) {
+ $cmid = reset($match)->cmid;
+ } else {
+ // There are multiple question banks with id number and we can't determine which one to use.
+ return -1;
+ }
+ }
+ }
+
+ return $cmid;
+ }
+
/**
* Find a category with a given idnumber in a given context.
*
@@ -156,6 +199,89 @@ public static function get_category_by_idnumber(\context $context, string $idnum
return $category;
}
+ /**
+ * Get a list of the question banks that have sharable questions in the specific course.
+ *
+ * The list is returned in a form suitable for using in a select menu.
+ *
+ * @param int $courseid the id of the course to look in.
+ * @param int|null $userid if set, only count question created by this user.
+ * @param string|null $qbankidnumber if set, only count question banks with this idnumber.
+ * @return array course module id => object with fields cmid, qbankidnumber, courseid, qbankid.
+ */
+ public static function get_shareable_question_banks(int $courseid,
+ ?int $userid = null, ?string $qbankidnumber = null): array {
+ global $DB;
+ $params = [
+ 'modulename' => 'qbank',
+ 'courseid' => $courseid,
+ 'contextlevel' => CONTEXT_MODULE,
+ 'ready' => question_version_status::QUESTION_STATUS_READY,
+ ];
+
+ $creatortest = '';
+ if ($userid) {
+ $creatortest = 'AND qbe.ownerid = :userid';
+ $params['userid'] = $userid;
+ }
+
+ $idnumber = '';
+ if ($qbankidnumber && $qbankidnumber !== '*') {
+ $idnumber = 'AND cm.idnumber = :qbankidnumber';
+ $params['qbankidnumber'] = $qbankidnumber;
+ }
+
+ $sql = "SELECT cm.id AS cmid,
+ cm.idnumber AS qbankidnumber,
+ qbank.id AS qbankid,
+ qbank.name,
+ qbank.type
+ FROM {course} c
+ JOIN {course_modules} cm ON cm.course = c.id
+ JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
+ JOIN {qbank} qbank ON qbank.id = cm.instance
+ JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel
+ JOIN {question_categories} qc ON qc.contextid = ctx.id
+ JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc.id
+ AND qbe.idnumber IS NOT NULL
+ $creatortest
+ JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
+ AND qv.version = (SELECT MAX(qv2.version)
+ FROM {question_versions} qv2
+ WHERE qv2.questionbankentryid = qbe.id
+ AND qv2.status = :ready
+ )
+ JOIN {question} q ON q.id = qv.questionid
+ WHERE c.id = :courseid
+ AND qc.idnumber IS NOT NULL
+ AND qc.idnumber <> ''
+ $idnumber
+ GROUP BY cm.id, cm.idnumber, qbank.id, qbank.name
+ HAVING COUNT(q.id) > 0
+ ORDER BY cm.id";
+ $qbanks = $DB->get_records_sql($sql, $params);
+ return $qbanks;
+ }
+
+ /**
+ * Create a list of question banks in a form suitable for using in a select menu.
+ *
+ * @param array $qbanks the question banks, as returned by {@see get_shareable_question_banks()}.
+ * @return array course module id => question bank name (and idnumber if set).
+ */
+ public static function create_select_qbank_choices(array $qbanks): array {
+ $choices = ['' => get_string('choosedots')];
+ foreach ($qbanks as $cmid => $qbank) {
+ if ($qbank->qbankidnumber) {
+ $choices[$cmid] = get_string('nameandidnumber', 'filter_embedquestion',
+ ['name' => format_string($qbank->name), 'idnumber' => s($qbank->qbankidnumber)]);
+ } else {
+ $choices[$cmid] = format_string($qbank->name);
+ }
+ }
+ return $choices;
+ }
+
/**
* Find a question with a given idnumber in a given context.
*
@@ -166,29 +292,22 @@ public static function get_category_by_idnumber(\context $context, string $idnum
public static function get_question_by_idnumber(int $categoryid, string $idnumber): ?\stdClass {
global $DB;
- if (self::has_question_versionning()) {
- $question = $DB->get_record_sql('
- SELECT q.*, qbe.idnumber, qbe.questioncategoryid AS category,
- qv.id AS versionid, qv.version, qv.questionbankentryid
- FROM {question_bank_entries} qbe
- JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = (
- SELECT MAX(version)
- FROM {question_versions}
- WHERE questionbankentryid = qbe.id AND status = :ready
- )
- JOIN {question} q ON q.id = qv.questionid
- WHERE qbe.questioncategoryid = :category AND qbe.idnumber = :idnumber',
- [
- 'ready' => question_version_status::QUESTION_STATUS_READY,
- 'category' => $categoryid, 'idnumber' => $idnumber,
- ],
- );
- } else {
- $question = $DB->get_record_select('question',
- "category = ? AND idnumber = ? AND hidden = 0 AND parent = 0",
- [$categoryid, $idnumber]);
-
- }
+ $question = $DB->get_record_sql('
+ SELECT q.*, qbe.idnumber, qbe.questioncategoryid AS category,
+ qv.id AS versionid, qv.version, qv.questionbankentryid
+ FROM {question_bank_entries} qbe
+ JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = (
+ SELECT MAX(version)
+ FROM {question_versions}
+ WHERE questionbankentryid = qbe.id AND status = :ready
+ )
+ JOIN {question} q ON q.id = qv.questionid
+ WHERE qbe.questioncategoryid = :category AND qbe.idnumber = :idnumber',
+ [
+ 'ready' => question_version_status::QUESTION_STATUS_READY,
+ 'category' => $categoryid, 'idnumber' => $idnumber,
+ ],
+ );
if (!$question) {
return null;
}
@@ -235,65 +354,36 @@ public static function is_latest_version(\question_definition $question): bool {
public static function get_categories_with_sharable_question_choices(\context $context,
int|null $userid = null): array {
global $DB;
+ $params = [];
+ $creatortest = '';
+ if ($userid) {
+ $creatortest = 'AND qbe.ownerid = :userid';
+ $params['userid'] = $userid;
+ }
+ $params['status'] = question_version_status::QUESTION_STATUS_READY;
+ $params['modulename'] = 'qbank';
+ $params['contextid'] = $context->id;
- if (self::has_question_versionning()) {
- $params = [];
- $creatortest = '';
- if ($userid) {
- $creatortest = 'AND qbe.ownerid = ?';
- $params[] = $userid;
- }
- $params[] = question_version_status::QUESTION_STATUS_READY;
- $params[] = $context->id;
-
- $categories = $DB->get_records_sql("
- SELECT qc.id, qc.name, qc.idnumber, COUNT(q.id) AS count
-
- FROM {question_categories} qc
- JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc.id
- AND qbe.idnumber IS NOT NULL $creatortest
- JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = (
- SELECT MAX(version)
- FROM {question_versions}
- WHERE questionbankentryid = qbe.id AND status = ?
- )
- JOIN {question} q ON q.id = qv.questionid
-
- WHERE qc.contextid = ?
- AND qc.idnumber IS NOT NULL
-
- GROUP BY qc.id, qc.name, qc.idnumber
- HAVING COUNT(q.id) > 0
- ORDER BY qc.name
- ", $params);
-
- } else {
- $params = [];
- $creatortest = '';
- if ($userid) {
- $creatortest = 'AND q.createdby = ?';
- $params[] = $userid;
- }
- $params[] = $context->id;
-
- $categories = $DB->get_records_sql("
- SELECT qc.id, qc.name, qc.idnumber, COUNT(q.id) AS count
+ $categories = $DB->get_records_sql("
+ SELECT qc.id, qc.name, qc.idnumber, COUNT(q.id) AS count
- FROM {question_categories} qc
- JOIN {question} q ON q.category = qc.id
- AND q.idnumber IS NOT NULL
- $creatortest
- AND q.hidden = 0
- AND q.parent = 0
+ FROM {question_categories} qc
+ JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc.id
+ AND qbe.idnumber IS NOT NULL $creatortest
+ JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = (
+ SELECT MAX(version)
+ FROM {question_versions}
+ WHERE questionbankentryid = qbe.id AND status = :status
+ )
+ JOIN {question} q ON q.id = qv.questionid
- WHERE qc.contextid = ?
+ WHERE qc.contextid = :contextid
AND qc.idnumber IS NOT NULL
-
- GROUP BY qc.id, qc.name, qc.idnumber
- HAVING COUNT(q.id) > 0
- ORDER BY qc.name
- ", $params);
- }
+ AND qc.idnumber <> ''
+ GROUP BY qc.id, qc.name, qc.idnumber
+ HAVING COUNT(q.id) > 0
+ ORDER BY qc.name
+ ", $params);
$choices = ['' => get_string('choosedots')];
foreach ($categories as $category) {
@@ -316,57 +406,32 @@ public static function get_categories_with_sharable_question_choices(\context $c
public static function get_sharable_question_ids(int $categoryid, int|null $userid = null): array {
global $DB;
- if (self::has_question_versionning()) {
- $params = [];
- $params[] = question_version_status::QUESTION_STATUS_READY;
- $params[] = $categoryid;
- $creatortest = '';
- if ($userid) {
- $creatortest = 'AND qbe.ownerid = ?';
- $params[] = $userid;
- }
-
- return $DB->get_records_sql("
- SELECT q.id, q.name, qbe.idnumber
-
- FROM {question_bank_entries} qbe
- JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = (
- SELECT MAX(version)
- FROM {question_versions}
- WHERE questionbankentryid = qbe.id AND status = ?
- )
- JOIN {question} q ON q.id = qv.questionid
-
- WHERE qbe.questioncategoryid = ?
- AND qbe.idnumber IS NOT NULL
- $creatortest
-
- ORDER BY q.name
- ", $params);
-
- } else {
- $params = [];
- $params[] = $categoryid;
- $creatortest = '';
- if ($userid) {
- $creatortest = 'AND q.createdby = ?';
- $params[] = $userid;
- }
+ $params = [];
+ $params[] = question_version_status::QUESTION_STATUS_READY;
+ $params[] = $categoryid;
+ $creatortest = '';
+ if ($userid) {
+ $creatortest = 'AND qbe.ownerid = ?';
+ $params[] = $userid;
+ }
- return $DB->get_records_sql("
- SELECT q.id, q.name, q.idnumber
+ return $DB->get_records_sql("
+ SELECT q.id, q.name, qbe.idnumber
- FROM {question} q
+ FROM {question_bank_entries} qbe
+ JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = (
+ SELECT MAX(version)
+ FROM {question_versions}
+ WHERE questionbankentryid = qbe.id AND status = ?
+ )
+ JOIN {question} q ON q.id = qv.questionid
- WHERE q.category = ?
- AND q.idnumber IS NOT NULL
- $creatortest
- AND q.hidden = 0
- AND q.parent = 0
+ WHERE qbe.questioncategoryid = ?
+ AND qbe.idnumber IS NOT NULL
+ $creatortest
- ORDER BY q.name
- ", $params);
- }
+ ORDER BY q.name
+ ", $params);
}
/**
@@ -456,9 +521,6 @@ public static function get_question_bank_url(\question_definition $question): \m
require_once($CFG->dirroot . '/question/editlib.php');
$context = \context::instance_by_id($question->contextid);
- if ($context->contextlevel != CONTEXT_COURSE) {
- throw new \coding_exception('Unexpected. Only questions from the course question bank should be embedded.');
- }
$latestquestionid = $DB->get_field_sql("
SELECT qv.questionid
@@ -472,7 +534,7 @@ public static function get_question_bank_url(\question_definition $question): \m
", [$question->questionbankentryid, $question->questionbankentryid]);
return new \moodle_url('/question/edit.php', [
- 'courseid' => $context->instanceid,
+ 'cmid' => $context->instanceid,
'cat' => $question->category . ',' . $question->contextid,
'qperpage' => MAXIMUM_QUESTIONS_PER_PAGE,
'lastchanged' => $latestquestionid,
@@ -554,4 +616,31 @@ public static function moodle_version_is(string $operator, string $version): boo
return false;
}
+
+ /**
+ * Get course id by course shortname.
+ *
+ * @param string $courseshortname
+ * @return int
+ */
+ public static function get_courseid_by_course_shortname(string $courseshortname): int {
+ global $DB;
+ return $DB->get_field('course', 'id', ['shortname' => $courseshortname]);
+ }
+
+ /**
+ * Check if the current user has permission to embed questions in this context.
+ *
+ * @param \context $context the context to check.
+ * @return bool true if the user has permission, false if not.
+ */
+ public static function has_permission(\context $context): bool {
+ if (has_capability('moodle/question:useall', $context)) {
+ return true;
+ } else if (has_capability('moodle/question:usemine', $context)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
}
diff --git a/db/services.php b/db/services.php
index c15d3c4..43b685b 100644
--- a/db/services.php
+++ b/db/services.php
@@ -34,11 +34,18 @@
'ajax' => true,
],
+ 'filter_embedquestion_get_sharable_category_choices' => [
+ 'classname' => 'filter_embedquestion\external\get_sharable_category_choices',
+ 'description' => 'Use by form autocomplete for selecting a sharable qbank.',
+ 'type' => 'read',
+ 'ajax' => true,
+ ],
+
'filter_embedquestion_get_embed_code' => [
'classname' => 'filter_embedquestion\external',
'methodname' => 'get_embed_code',
'classpath' => '',
- 'description' => 'Use by atto-editer embedquestion button.',
+ 'description' => 'Use by tiny/atto embedquestion button.',
'type' => 'read',
'ajax' => true,
],
diff --git a/lang/en/filter_embedquestion.php b/lang/en/filter_embedquestion.php
index 5c6765a..6698925 100644
--- a/lang/en/filter_embedquestion.php
+++ b/lang/en/filter_embedquestion.php
@@ -31,6 +31,7 @@
$string['chooserandomly'] = 'Choose an embeddable question from this category randomly';
$string['corruptattempt'] = 'Your previous attempt at a question here has stopped working. If you click continue, it will be removed and a new attempt created.';
$string['corruptattemptwithreason'] = 'Your previous attempt at a question here has stopped working. ({$a}) If you click continue, it will be removed and a new attempt created.';
+$string['defaultqbank'] = 'Default question bank for each course';
$string['defaultsheading'] = 'Default options for embedding questions';
$string['defaultsheading_desc'] = 'These are the defaults for the options that control how embedded questions display and function. These are the values that will be used if a particular option is not set when the question is embedded.';
$string['defaultx'] = 'Default ({$a})';
@@ -38,6 +39,7 @@
$string['embedquestion'] = 'Embed question';
$string['errormaxmarknumber'] = 'The maximum mark must be a number.';
$string['errornopermissions'] = 'You do not have permission to embed this question.';
+$string['errorquestionbanknotfound'] = 'The question bank does not exist';
$string['errorunknownquestion'] = 'Unknown, or unsharable question.';
$string['errorvariantformat'] = 'Variant number must be a positive integer.';
$string['errorvariantoutofrange'] = 'Variant number must be a positive integer at most {$a}.';
@@ -53,9 +55,12 @@
$string['iframedescriptionminlengthwarning'] = 'A description must have at least three characters.';
$string['iframetitle'] = 'Embedded question';
$string['iframetitleauto'] = 'Embedded question {$a}';
+$string['invalidcantfindquestionbank'] = 'We have multiple question banks in this context "{$a->contextname}", but the one you are trying to use does not exist.';
$string['invalidcategory'] = 'The category with idnumber "{$a->catid}" does not exist in "{$a->contextname}".';
$string['invalidemptycategory'] = 'The category "{$a->catname}" in "{$a->contextname}" does not contain any embeddable questions.';
+$string['invalidqbankidnumber'] = 'The question bank with idnumber "{$a->qbankidnumber}" does not exist in "{$a->contextname}".';
$string['invalidquestion'] = 'The question with idnumber "{$a->qid}" does not exist in category "{$a->catname} [{$a->catidnumber}]".';
+$string['invalidquestionbank'] = 'The question bank does not exist.';
$string['invalidrandomquestion'] = 'Cannot generate a random question from the question category "{$a}".';
$string['invalidtoken'] = 'This embedded question is incorrectly configured.';
$string['markdp_desc'] = 'The default number of digits that should be shown after the decimal point when displaying grades in embedded questions.';
@@ -68,7 +73,7 @@
$string['notyourattempt'] = 'This is not your attempt.';
$string['pluginname'] = 'Embed questions';
$string['previousattempts'] = 'Previous attempts';
-$string['privacy:metadata'] = 'The Embed questions filter does not store any personal data.';
+$string['privacy:preference:defaultqbank'] = 'Stores default qbank mapping from course ID to selected question bank (JSON-encoded).';
$string['questionbank'] = 'Question bank';
$string['questionidnumber'] = 'Question id number';
$string['questionidnumberchanged'] = 'The question being attempted here no longer has idnumber {$a}.';
diff --git a/lib.php b/lib.php
index bd5a83a..8df4c60 100644
--- a/lib.php
+++ b/lib.php
@@ -73,3 +73,34 @@ function filter_embedquestion_question_pluginfile($givencourse, $context, $compo
send_stored_file($file, 0, 0, $forcedownload, $fileoptions);
}
+
+/**
+ * Build and return the output for the question bank and category chooser.
+ *
+ * @param array $args provided by the AJAX request.
+ * @return string html to render to the modal.
+ */
+function filter_embedquestion_output_fragment_switch_question_bank(array $args): string {
+ global $USER, $OUTPUT;
+
+ $courseid = clean_param($args['courseid'], PARAM_INT);
+ $switchbankwidget = new filter_embedquestion\output\switch_question_bank($courseid, $USER->id);
+
+ return $OUTPUT->render($switchbankwidget);
+}
+
+/**
+ * Allow update of user preferences via AJAX.
+ *
+ * @return array[]
+ */
+function filter_embedquestion_user_preferences(): array {
+ return [
+ 'filter_embedquestion_userdefaultqbank' => [
+ 'type' => PARAM_RAW,
+ 'null' => NULL_NOT_ALLOWED,
+ 'default' => '{}',
+ 'permissioncallback' => [core_user::class, 'is_current_user'],
+ ],
+ ];
+}
diff --git a/showquestion.php b/showquestion.php
index 69bec35..c246218 100644
--- a/showquestion.php
+++ b/showquestion.php
@@ -56,7 +56,9 @@
// Process other parameters.
$categoryidnumber = required_param('catid', PARAM_RAW);
$questionidnumber = required_param('qid', PARAM_RAW);
-$embedid = new embed_id($categoryidnumber, $questionidnumber);
+$questionbankidnumber = optional_param('questionbankidnumber', '', PARAM_RAW);
+$courseshortname = optional_param('courseshortname', '', PARAM_RAW);
+$embedid = new embed_id($categoryidnumber, $questionidnumber, $questionbankidnumber, $courseshortname);
$embedlocation = embed_location::make_from_url_params();
diff --git a/templates/switch_question_bank.mustache b/templates/switch_question_bank.mustache
new file mode 100644
index 0000000..ae458f9
--- /dev/null
+++ b/templates/switch_question_bank.mustache
@@ -0,0 +1,131 @@
+{{!
+ 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 filter_embedquestion/switch_question_bank
+
+ Example context (json):
+{
+ "contextid": "2",
+ "hascoursesharedbanks": true,
+ "coursesharedbanks": [
+ {
+ "name": "Question bank 1",
+ "modid": "2",
+ "contextid": 2,
+ "coursenamebankname": "c1 - Question bank 1",
+ "cminfo": {},
+ "questioncategories": []
+ },
+ {
+ "name": "Question bank 2",
+ "modid": "3",
+ "contextid": 3,
+ "coursenamebankname": "c1 - Question bank 2",
+ "cminfo": {},
+ "questioncategories": []
+ }
+ ],
+ "hasrecentlyviewedbanks": true,
+ "recentlyviewedbanks": [
+ {
+ "name": "Question bank 3",
+ "modid": "4",
+ "contextid": 4,
+ "coursenamebankname": "c2 - Question bank 4",
+ "cminfo": {},
+ "questioncategories": []
+ },
+ {
+ "name": "Question bank 4",
+ "modid": "6",
+ "contextid": 6,
+ "coursenamebankname": "c3 - Question bank 5",
+ "cminfo": {},
+ "questioncategories": []
+ }
+ ],
+ "hassharedbanks": true,
+ "sharedbanks": [
+ {
+ "name": "Question bank 1",
+ "modid": "2",
+ "contextid": 2,
+ "coursenamebankname": "c1 - Question bank 1",
+ "cminfo": {},
+ "questioncategories": []
+ },
+ {
+ "name": "Question bank 2",
+ "modid": "3",
+ "contextid": 3,
+ "coursenamebankname": "c1 - Question bank 2",
+ "cminfo": {},
+ "questioncategories": []
+ },
+ {
+ "name": "Question bank 3",
+ "modid": "4",
+ "contextid": 4,
+ "coursenamebankname": "c2 - Question bank 4",
+ "cminfo": {},
+ "questioncategories": []
+ },
+ {
+ "name": "Question bank 4",
+ "modid": "6",
+ "contextid": 6,
+ "coursenamebankname": "c3 - Question bank 5",
+ "cminfo": {},
+ "questioncategories": []
+ }
+ ]
+}
+}}
+{{#hascoursesharedbanks}}
+
';
}
- $icon = ']*>Edit question';
- if (utils::moodle_version_is("<=", "44")) {
- $icon = ']*>Edit question';
- }
+ $icon = ']*>\s*Edit question\s*\s*';
// Verify that the edit question, question bank link and fill with correct links are present.
$expectedregex = '~
Question [^<]+' .
@@ -294,8 +291,8 @@ public function test_question_rendering(): void {
'Question bank
' .
'
' .
'
~';
+ '' .
+ 'Fill with correct~s';
$this->assertMatchesRegularExpression($expectedregex, $html);
// Create an authenticated user.
$user = $this->getDataGenerator()->create_user();
diff --git a/tests/behat/filter_embedquestion.feature b/tests/behat/filter_embedquestion.feature
index f29c12d..16b2fd9 100644
--- a/tests/behat/filter_embedquestion.feature
+++ b/tests/behat/filter_embedquestion.feature
@@ -16,9 +16,12 @@ Feature: Add an activity and embed a question inside that activity
| user | course | role |
| teacher | C1 | editingteacher |
| student | C1 | student |
+ And the following "activities" exist:
+ | activity | name | intro | course | idnumber |
+ | qbank | Qbank 1 | Question bank 1 | C1 | qbank1 |
And the following "question categories" exist:
- | contextlevel | reference | name | idnumber |
- | Course | C1 | Test questions| embed |
+ | contextlevel | reference | name | idnumber|
+ | Activity module | qbank1 | Test questions | embed |
And the "embedquestion" filter is "on"
@javascript
@@ -27,6 +30,7 @@ Feature: Add an activity and embed a question inside that activity
| questioncategory | qtype | name | idnumber |
| Test questions | truefalse | First question | test1 |
When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher
+ And I set the field "Question bank" to "Qbank 1 [qbank1]"
And I set the field "Question category" to "Test questions [embed] (1)"
And I set the field "id_questionidnumber" to "First question"
And I press "Embed question"
@@ -54,6 +58,7 @@ Feature: Add an activity and embed a question inside that activity
| Test questions | truefalse | Q3 | test3 |
| Test questions | truefalse | Q4 | test4 |
When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher
+ And I set the field "Question bank" to "Qbank 1 [qbank1]"
And I set the field "Question category" to "Test questions [embed] (4)"
And I set the field "id_questionidnumber" to "Choose an embeddable question from this category randomly"
And I set the field "Iframe description" to "Embed question for behat testing"
@@ -73,6 +78,7 @@ Feature: Add an activity and embed a question inside that activity
| questioncategory | qtype | name | idnumber |
| Test questions | truefalse | First question | test1 |
When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher
+ And I set the field "Question bank" to "Qbank 1 [qbank1]"
And I set the field "Question category" to "Test questions [embed] (1)"
And I set the field "id_questionidnumber" to "First question"
And I press "Embed question"
@@ -83,6 +89,7 @@ Feature: Add an activity and embed a question inside that activity
And I press "id_submitbutton"
# Because of the way the test page works, we need to re-select the question.
Then I should see "Generate the code to embed a question"
+ And I set the field "Question bank" to "Qbank 1 [qbank1]"
And I set the field "Question category" to "Test questions [embed] (1)"
And I set the field "id_questionidnumber" to "First question"
And I press "Embed question"
@@ -95,6 +102,7 @@ Feature: Add an activity and embed a question inside that activity
| questioncategory | qtype | name | idnumber |
| Test questions | truefalse | First question | test1 |
When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher
+ And I set the field "Question bank" to "Qbank 1 [qbank1]"
And I set the field "Question category" to "Test questions [embed] (1)"
And I set the field "id_questionidnumber" to "First question"
And I press "Embed question"
@@ -104,6 +112,7 @@ Feature: Add an activity and embed a question inside that activity
And I press "Cancel"
# Because of the way the test page works, we need to re-select the question.
Then I should see "Generate the code to embed a question"
+ And I set the field "Question bank" to "Qbank 1 [qbank1]"
And I set the field "Question category" to "Test questions [embed] (1)"
And I set the field "id_questionidnumber" to "First question"
And I press "Embed question"
@@ -118,6 +127,7 @@ Feature: Add an activity and embed a question inside that activity
| Test questions | recordrtc | Record AV question | test1 | audio |
And I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher
And I expand all fieldsets
+ And I set the field "Question bank" to "Qbank 1 [qbank1]"
And I set the field "Question category" to "Test questions [embed] (1)"
And I set the field "id_questionidnumber" to "Record AV question"
And I set the field "How the question behaves" to "Immediate feedback"
diff --git a/tests/behat/filter_embedquestion_fillwithcorrect.feature b/tests/behat/filter_embedquestion_fillwithcorrect.feature
index 94d0436..cd07eb3 100644
--- a/tests/behat/filter_embedquestion_fillwithcorrect.feature
+++ b/tests/behat/filter_embedquestion_fillwithcorrect.feature
@@ -14,9 +14,12 @@ Feature: Fill with correct feature for staff
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
+ And the following "activities" exist:
+ | activity | name | intro | course | idnumber |
+ | qbank | Qbank 1 | Question bank 1 | C1 | qbank1 |
And the following "question categories" exist:
- | contextlevel | reference | name | idnumber |
- | Course | C1 | Test questions| embed |
+ | contextlevel | reference | name | idnumber|
+ | Activity module | qbank1 | Test questions | embed |
And the following "questions" exist:
| questioncategory | qtype | name | idnumber |
| Test questions | truefalse | First question | test1 |
@@ -26,6 +29,7 @@ Feature: Fill with correct feature for staff
@javascript
Scenario: Teacher can see and use the Fill with correct link
When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher
+ And I set the field "Question bank" to "Qbank 1 [qbank1]"
And I set the field "Question category" to "Test questions [embed] (2)"
And I set the field "id_questionidnumber" to "First question"
And I press "Embed question"
@@ -43,6 +47,7 @@ Feature: Fill with correct feature for staff
@javascript
Scenario: Teacher can not see the Fill with correct link for open question
When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher
+ And I set the field "Question bank" to "Qbank 1 [qbank1]"
And I set the field "Question category" to "Test questions [embed] (2)"
And I set the field "id_questionidnumber" to "Second question"
And I press "Embed question"
@@ -52,6 +57,7 @@ Feature: Fill with correct feature for staff
@javascript
Scenario: Teacher can see and use the Question bank link.
When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher
+ And I set the field "Question bank" to "Qbank 1 [qbank1]"
And I set the field "Question category" to "Test questions [embed] (2)"
And I set the field "id_questionidnumber" to "First question"
And I press "Embed question"
diff --git a/tests/external_test.php b/tests/external_test.php
index 3ecfed5..dd0fe65 100644
--- a/tests/external_test.php
+++ b/tests/external_test.php
@@ -42,11 +42,12 @@ public function test_get_sharable_question_choices_working(): void {
$this->setAdminUser();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
+ $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id, 'idnumber' => 'abc123']);
/** @var \core_question_generator $questiongenerator */
$questiongenerator = $generator->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category([
'name' => 'Category with idnumber',
- 'contextid' => \context_course::instance($course->id)->id,
+ 'contextid' => \context_module::instance($qbank->cmid)->id,
'idnumber' => 'abc123',
]);
@@ -63,15 +64,26 @@ public function test_get_sharable_question_choices_working(): void {
['value' => 'toad', 'label' => 'Question 2 [toad]'],
['value' => '*', 'label' => get_string('chooserandomly', 'filter_embedquestion')],
],
- external::get_sharable_question_choices($course->id, 'abc123'));
+ external::get_sharable_question_choices($qbank->cmid, 'abc123'));
}
public function test_get_sharable_question_choices_no_permissions(): void {
+ global $DB;
$this->resetAfterTest();
- $this->setGuestUser();
$this->expectException('coding_exception');
$this->expectExceptionMessage('This user is not allowed to embed questions.');
- external::get_sharable_question_choices(SITEID, 'abc123');
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $user = $generator->create_user();
+ role_change_permission($DB->get_field('role', 'id', ['shortname' => 'editingteacher']),
+ \context_system::instance(), 'moodle/question:useall', CAP_PREVENT);
+ role_change_permission($DB->get_field('role', 'id', ['shortname' => 'editingteacher']),
+ \context_system::instance(), 'moodle/question:usemine', CAP_PREVENT);
+ $generator->enrol_user($user->id, $course->id, 'editingteacher');
+ $this->setUser($user);
+ $qbank1 = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id]);
+
+ external::get_sharable_question_choices($qbank1->cmid, 'abc123');
}
public function test_get_sharable_question_choices_only_user(): void {
@@ -88,10 +100,11 @@ public function test_get_sharable_question_choices_only_user(): void {
/** @var \core_question_generator $questiongenerator */
$questiongenerator = $generator->get_plugin_generator('core_question');
+ $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id, 'idnumber' => 'abc123']);
$category = $questiongenerator->create_question_category([
'name' => 'Category with idnumber',
'idnumber' => 'abc123',
- 'contextid' => \context_course::instance($course->id)->id,
+ 'contextid' => \context_module::instance($qbank->cmid)->id,
]);
$this->setAdminUser();
@@ -107,7 +120,7 @@ public function test_get_sharable_question_choices_only_user(): void {
['value' => '', 'label' => 'Choose...'],
['value' => 'frog', 'label' => 'Question 1 [frog]'],
],
- external::get_sharable_question_choices($course->id, 'abc123'));
+ external::get_sharable_question_choices($qbank->cmid, 'abc123'));
}
/**
@@ -115,8 +128,14 @@ public function test_get_sharable_question_choices_only_user(): void {
*/
public static function get_embed_code_cases(): array {
return [
- ['abc123', 'toad', 'abc123/toad'],
- ['A/V questions', '|---> 100%', 'A%2FV questions/%7C---> 100%25'],
+ ['abc123', 'toad', '', '', '*/abc123/toad'],
+ ['abc123', 'toad', '', 'id1', 'id1/abc123/toad'],
+ ['abc123', 'toad', 'c1', 'id1', 'c1/id1/abc123/toad'],
+ ['abc123', 'toad', 'c1', '', 'c1/*/abc123/toad'],
+ ['A/V questions', '|---> 100%', '', '', '*/A%2FV questions/%7C---> 100%25'],
+ ['A/V questions', '|---> 100%', '', 'id1', 'id1/A%2FV questions/%7C---> 100%25'],
+ ['A/V questions', '|---> 100%', 'c1', 'id1', 'c1/id1/A%2FV questions/%7C---> 100%25'],
+ ['A/V questions', '|---> 100%', 'c1', '', 'c1/*/A%2FV questions/%7C---> 100%25'],
];
}
@@ -125,25 +144,30 @@ public static function get_embed_code_cases(): array {
*
* @param string $catid idnumber to use for the category.
* @param string $questionid idnumber to use for the question.
+ * @param string $courseshortname the course shortname.
+ * @param string $qbankidnumber the question bank idnumber.
* @param string $expectedembedid what the embed id in the output should be.
* @dataProvider get_embed_code_cases
*/
- public function test_get_embed_code_working(string $catid, string $questionid, string $expectedembedid): void {
-
+ public function test_get_embed_code_working(string $catid, string $questionid,
+ string $courseshortname, string $qbankidnumber, string $expectedembedid): void {
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
- $course = $generator->create_course();
+ $course = $generator->create_course(['shortname' => $courseshortname]);
/** @var \core_question_generator $questiongenerator */
$questiongenerator = $generator->get_plugin_generator('core_question');
+ $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => $qbankidnumber]);
$category = $questiongenerator->create_question_category(
- ['name' => 'Category', 'idnumber' => $catid, 'contextid' => \context_course::instance($course->id)->id]);
+ ['name' => 'Category', 'idnumber' => $catid, 'contextid' => \context_module::instance($qbank->cmid)->id]);
$questiongenerator->create_question('shortanswer', null,
['category' => $category->id, 'name' => 'Question', 'idnumber' => $questionid]);
-
- $embedid = new embed_id($catid, $questionid);
+ if (!$qbankidnumber) {
+ $qbankidnumber = '*';
+ }
+ $embedid = new embed_id($catid, $questionid, $qbankidnumber, $courseshortname);
$iframedescription = '';
$behaviour = '';
$maxmark = '';
@@ -158,7 +182,7 @@ public function test_get_embed_code_working(string $catid, string $questionid, s
$token = token::make_secret_token($embedid);
$expected = '{Q{' . $expectedembedid . '|' . $token . '}Q}';
- $actual = external::get_embed_code($course->id, $embedid->categoryidnumber,
+ $actual = external::get_embed_code($qbank->cmid, $embedid->categoryidnumber,
$embedid->questionidnumber, $iframedescription, $behaviour,
$maxmark, $variant, $correctness, $marks, $markdp, $feedback,
$generalfeedback, $rightanswer, $history, '');
@@ -167,7 +191,7 @@ public function test_get_embed_code_working(string $catid, string $questionid, s
$behaviour = 'immediatefeedback';
$expected = '{Q{' . $expectedembedid . '|behaviour=' . $behaviour . '|' . $token . '}Q}';
- $actual = external::get_embed_code($course->id, $embedid->categoryidnumber,
+ $actual = external::get_embed_code($qbank->cmid, $embedid->categoryidnumber,
$embedid->questionidnumber, $iframedescription, $behaviour,
$maxmark, $variant, $correctness, $marks, $markdp, $feedback, $generalfeedback,
$rightanswer, $history, '');
@@ -182,17 +206,19 @@ public function test_get_embed_code_working_with_random_questions(): void {
$this->setAdminUser();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
+ $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id, 'idnumber' => '']);
+
/** @var \core_question_generator $questiongenerator */
$questiongenerator = $generator->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category(
- ['name' => 'Category', 'idnumber' => 'abc123', 'contextid' => \context_course::instance($course->id)->id]);
+ ['name' => 'Category', 'idnumber' => 'abc123', 'contextid' => \context_module::instance($qbank->cmid)->id]);
$questiongenerator->create_question('shortanswer', null,
['category' => $category->id, 'name' => 'Question1', 'idnumber' => 'toad']);
$questiongenerator->create_question('shortanswer', null,
['category' => $category->id, 'name' => 'Question2', 'idnumber' => 'frog']);
- $embedid = new embed_id('abc123', 'toad');
+ $embedid = new embed_id('abc123', 'toad', '*', $course->shortname);
$iframedescription = 'Embedded random question';
$behaviour = '';
$maxmark = '';
@@ -209,26 +235,26 @@ public function test_get_embed_code_working_with_random_questions(): void {
$token = token::make_secret_token($embedid);
$expected = '{Q{' . $embedid . $titlebit . '|' . $token . '}Q}';
- $actual = external::get_embed_code($course->id, $embedid->categoryidnumber,
+ $actual = external::get_embed_code($qbank->cmid, $embedid->categoryidnumber,
$embedid->questionidnumber, $iframedescription, $behaviour,
$maxmark, $variant, $correctness, $marks, $markdp, $feedback, $generalfeedback,
$rightanswer, $history, '');
$this->assertEquals($expected, $actual);
- $embedid = new embed_id('abc123', 'frog');
+ $embedid = new embed_id('abc123', 'frog', '*', $course->shortname);
$token = token::make_secret_token($embedid);
$expected = '{Q{' . $embedid . $titlebit . '|' . $token . '}Q}';
- $actual = external::get_embed_code($course->id, $embedid->categoryidnumber,
+ $actual = external::get_embed_code($qbank->cmid, $embedid->categoryidnumber,
$embedid->questionidnumber, $iframedescription, $behaviour,
$maxmark, $variant, $correctness, $marks, $markdp, $feedback, $generalfeedback,
$rightanswer, $history, '');
$this->assertEquals($expected, $actual);
// Accept '*' for $questionidnumber to indicate a random question.
- $embedid = new embed_id('abc123', '*');
+ $embedid = new embed_id('abc123', '*', '*', $course->shortname);
$token = token::make_secret_token($embedid);
$expected = '{Q{' . $embedid . $titlebit . '|' . $token . '}Q}';
- $actual = external::get_embed_code($course->id, $embedid->categoryidnumber,
+ $actual = external::get_embed_code($qbank->cmid, $embedid->categoryidnumber,
$embedid->questionidnumber, $iframedescription, $behaviour,
$maxmark, $variant, $correctness, $marks, $markdp, $feedback, $generalfeedback,
$rightanswer, $history, '');
@@ -236,7 +262,7 @@ public function test_get_embed_code_working_with_random_questions(): void {
$behaviour = 'immediatefeedback';
$expected = '{Q{' . $embedid . $titlebit . '|behaviour=' . $behaviour . '|' . $token . '}Q}';
- $actual = external::get_embed_code($course->id, $embedid->categoryidnumber,
+ $actual = external::get_embed_code($qbank->cmid, $embedid->categoryidnumber,
$embedid->questionidnumber, $iframedescription, $behaviour,
$maxmark, $variant, $correctness, $marks, $markdp, $feedback, $generalfeedback,
$rightanswer, $history, '');
@@ -263,4 +289,5 @@ public function test_is_authorized_secret_token(string $catid, string $questioni
$this->assertEquals(true, token::is_authorized_secret_token($token, $embedid));
}
+
}
diff --git a/tests/filter_test.php b/tests/filter_test.php
index e2e27ed..d68ff1b 100644
--- a/tests/filter_test.php
+++ b/tests/filter_test.php
@@ -55,6 +55,8 @@ public static function get_cases_for_test_filter(): array {
$expectedurl = new \moodle_url(
'/filter/embedquestion/showquestion.php',
[
+ 'courseshortname' => '',
+ 'questionbankidnumber' => '',
'catid' => 'cat',
'qid' => 'q',
'contextid' => '1',
@@ -84,6 +86,8 @@ class="filter_embedquestion-iframe" allowfullscreen loading="lazy"
$expectedurl = new \moodle_url(
'/filter/embedquestion/showquestion.php',
[
+ 'courseshortname' => '',
+ 'questionbankidnumber' => '',
'catid' => 'A/V questions',
'qid' => '|<--- 100%',
'contextid' => '1',
diff --git a/tests/generator/lib.php b/tests/generator/lib.php
index e6e0a88..6e84b35 100644
--- a/tests/generator/lib.php
+++ b/tests/generator/lib.php
@@ -73,11 +73,9 @@ public function create_embeddable_question(string $qtype, string|null $which = n
$categoryrecord['idnumber'] = 'embeddablecat' . (self::$uniqueid++);
}
if (isset($categoryrecord['contextid'])) {
- if (context::instance_by_id($categoryrecord['contextid'])->contextlevel !== CONTEXT_COURSE) {
- throw new coding_exception('Categorycontextid must refer to a course context.');
+ if (context::instance_by_id($categoryrecord['contextid'])->contextlevel !== CONTEXT_MODULE) {
+ throw new coding_exception('Categorycontextid must refer to a module context.');
}
- } else {
- $categoryrecord['contextid'] = context_course::instance(SITEID)->id;
}
$category = $this->questiongenerator->create_question_category($categoryrecord);
$overrides['category'] = $category->id;
@@ -113,8 +111,9 @@ public function get_embed_id_and_context(stdClass $question): array {
}
$context = context::instance_by_id($category->contextid);
- if ($context->contextlevel !== CONTEXT_COURSE) {
- throw new coding_exception('Categorycontextid must refer to a course context.');
+
+ if ($context->contextlevel !== CONTEXT_MODULE) {
+ throw new coding_exception('Categorycontextid must refer to a module context.');
}
return [new embed_id($category->idnumber, $question->idnumber), $context];
@@ -145,6 +144,8 @@ public function get_embed_code(stdClass $question) {
$fakeformdata = (object) [
'categoryidnumber' => $embedid->categoryidnumber,
'questionidnumber' => $embedid->questionidnumber,
+ 'questionbankidnumber' => $embedid->questionbankidnumber,
+ 'courseshortname' => $embedid->courseshortname,
];
return question_options::get_embed_from_form_options($fakeformdata);
}
@@ -167,17 +168,12 @@ public function create_attempt_at_embedded_question(stdClass $question,
$isfinish = true): attempt {
global $USER, $CFG;
- [$embedid, $coursecontext] = $this->get_embed_id_and_context($question);
+ [$embedid, $qbankcontext] = $this->get_embed_id_and_context($question);
if ($attemptcontext) {
- if ($attemptcontext->id !== $coursecontext->id &&
- $attemptcontext->get_parent_context()->id !== $coursecontext->id) {
- throw new coding_exception('The attempt context must either be the course ' .
- 'context where the question is, or one of the activities in that course.');
- }
$context = $attemptcontext;
} else {
- $context = $coursecontext;
+ $context = $qbankcontext;
}
if ($pagename) {
$pn = explode(':', $pagename);
diff --git a/tests/provider_test.php b/tests/provider_test.php
new file mode 100644
index 0000000..4a9e7b3
--- /dev/null
+++ b/tests/provider_test.php
@@ -0,0 +1,54 @@
+.
+
+namespace filter_embedquestion;
+
+use advanced_testcase;
+use core_privacy\local\request\writer;
+use filter_embedquestion\privacy\provider;
+
+/**
+ * Unit tests for filter_embedquestion privacy provider.
+ *
+ * @package filter_embedquestion
+ * @copyright 2025 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \filter_embedquestion\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();
+ $this->setUser($user);
+
+ $generator = $this->getDataGenerator();
+ $course = $generator->create_course();
+ $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id, 'idnumber' => 'abc123']);
+ // Simulate saved user preference JSON.
+ $example = [$course->id => $qbank->id];
+ set_user_preference('filter_embedquestion_userdefaultqbank', json_encode($example), $user->id);
+ provider::export_user_preferences($user->id);
+ $writer = writer::with_context(\context_system::instance());
+ $prefs = $writer->get_user_preferences('filter_embedquestion');
+ $this->assertEquals(get_string('defaultqbank', 'filter_embedquestion'), $prefs->userdefaultqbank->description);
+ $this->assertEquals(json_encode($example), $prefs->userdefaultqbank->value);
+ }
+}
diff --git a/tests/utils_test.php b/tests/utils_test.php
index aeaa07f..c80ce96 100644
--- a/tests/utils_test.php
+++ b/tests/utils_test.php
@@ -41,10 +41,11 @@ public function test_get_category_by_idnumber(): void {
$catwithidnumber = $questiongenerator->create_question_category(
['name' => 'Category with idnumber', 'idnumber' => 'abc123']);
$questiongenerator->create_question_category();
+ $context = \context::instance_by_id($catwithidnumber->contextid);
$this->assertEquals($catwithidnumber->id,
utils::get_category_by_idnumber(
- \context_system::instance(), 'abc123')->id);
+ $context, 'abc123')->id);
}
public function test_get_category_by_idnumber_not_existing(): void {
@@ -97,10 +98,11 @@ public function test_get_categories_with_sharable_question_choices(): void {
$questiongenerator->create_question('shortanswer', null,
['category' => $catwithid2->id, 'name' => 'Question', 'idnumber' => 'frog']);
+ $context = \context::instance_by_id($catwithid2->contextid);
$this->assertEquals(
['' => 'Choose...', 'pqr789' => 'Second category [pqr789] (1)'],
- utils::get_categories_with_sharable_question_choices(\context_system::instance()));
+ utils::get_categories_with_sharable_question_choices($context));
}
public function test_get_categories_with_sharable_question_choices_only_user(): void {
@@ -122,11 +124,11 @@ public function test_get_categories_with_sharable_question_choices_only_user():
$questiongenerator->create_question('shortanswer', null,
['category' => $catwithid2->id, 'name' => 'Question', 'idnumber' => 'frog']);
$this->setAdminUser();
-
+ $context = \context::instance_by_id($catwithid1->contextid);
$this->assertEquals([
'' => 'Choose...',
'abc123' => 'Category with idnumber [abc123] (1)',
- ], utils::get_categories_with_sharable_question_choices(\context_system::instance(), $USER->id));
+ ], utils::get_categories_with_sharable_question_choices($context, $USER->id));
}
public function test_get_sharable_question_choices(): void {
@@ -234,12 +236,13 @@ public function test_get_categories_with_sharable_question_choices_should_not_in
$questiongenerator->create_question('shortanswer', null,
['category' => $catwithid2->id, 'name' => 'Question', 'idnumber' => 'frog']);
+ $context = \context::instance_by_id($catwithid2->contextid);
// The random question should not appear in the counts.
$this->assertEquals([
'' => 'Choose...',
'pqr789' => 'Second category with [pqr789] (1)',
- ], utils::get_categories_with_sharable_question_choices(\context_system::instance()));
+ ], utils::get_categories_with_sharable_question_choices($context));
}
/**
@@ -299,17 +302,17 @@ public function test_get_categories_with_sharable_question_choices_should_not_in
$catwithid2 = $questiongenerator->create_question_category(
['name' => 'Second category with', 'idnumber' => 'pqr789']);
$questiongenerator->create_question_category();
-
$questiongenerator->create_question('shortanswer', null,
['category' => $catwithid2->id, 'name' => 'Question', 'idnumber' => 'frog']);
$this->create_hidden_question('shortanswer', null,
['category' => $catwithid2->id, 'name' => 'Question (hidden)', 'idnumber' => 'toad']);
+ $context = \context::instance_by_id($catwithid2->contextid);
// The hidden question should not appear in the counts.
$this->assertEquals([
'' => 'Choose...',
'pqr789' => 'Second category with [pqr789] (1)',
- ], utils::get_categories_with_sharable_question_choices(\context_system::instance()));
+ ], utils::get_categories_with_sharable_question_choices($context));
}
public function test_behaviour_choices(): void {
@@ -327,6 +330,7 @@ public function test_behaviour_choices(): void {
*
*/
public function test_create_attempt_at_embedded_question(): void {
+ global $COURSE;
$this->setAdminUser();
$this->resetAfterTest();
@@ -337,9 +341,12 @@ public function test_create_attempt_at_embedded_question(): void {
// Create course.
$course = $generator->create_course(['fullname' => 'Course 1', 'shortname' => 'C1']);
- $coursecontext = \context_course::instance($course->id);
+ // In unit test, the global $COURSE is always set to SITE, so we need to set it to the course we created.
+ $COURSE = $course;
+ $qbank = $generator->create_module('qbank', ['course' => $course->id], ['idnumber' => 'qbank1']);
+ $context = \context_module::instance($qbank->cmid);
// Create embed question.
- $question = $attemptgenerator->create_embeddable_question('truefalse', null, [], ['contextid' => $coursecontext->id]);
+ $question = $attemptgenerator->create_embeddable_question('truefalse', null, [], ['contextid' => $context->id]);
// Create page page that embeds a question.
$page = $generator->create_module('page', [
'course' => $course->id,
@@ -364,10 +371,11 @@ public function test_get_question_bank_url(): void {
$course = $this->getDataGenerator()->create_course();
/** @var \core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+ $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id]);
+ $context = \context_module::instance($qbank->cmid);
// Create a question with two versions.
- $cat = $questiongenerator->create_question_category(
- ['contextid' => \context_course::instance($course->id)->id]);
+ $cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
$firstversion = \question_bank::load_question($saq->id);
@@ -377,7 +385,7 @@ public function test_get_question_bank_url(): void {
// Prepare the expected result.
$expectedurl = new \moodle_url('/question/edit.php', [
- 'courseid' => $course->id,
+ 'cmid' => $context->instanceid,
'cat' => $secondversion->category . ',' . $secondversion->contextid,
'qperpage' => MAXIMUM_QUESTIONS_PER_PAGE,
'lastchanged' => $secondversion->id,
@@ -389,4 +397,66 @@ public function test_get_question_bank_url(): void {
// Check the URL using the second question id.
$this->assertEquals($expectedurl, utils::get_question_bank_url($secondversion));
}
+
+ /**
+ * Test getting shareable question banks.
+ */
+ public function test_get_shareable_question_banks(): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Create a course.
+ $course = $this->getDataGenerator()->create_course(['fullname' => 'Course 1', 'shortname' => 'C1']);
+ // Create a question bank.
+ $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => '']);
+ $qbank2 = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => 'qbank2']);
+ $generator = $this->getDataGenerator();
+ $attemptgenerator = $generator->get_plugin_generator('filter_embedquestion');
+ $attemptgenerator->create_embeddable_question('truefalse', null, [],
+ ['contextid' => \context_module::instance($qbank->cmid)->id]);
+ $attemptgenerator->create_embeddable_question('truefalse', null, [],
+ ['contextid' => \context_module::instance($qbank2->cmid)->id]);
+
+ $banks = utils::get_shareable_question_banks($course->id);
+ $this->assertArrayHasKey($qbank->cmid, $banks);
+ $this->assertArrayHasKey($qbank2->cmid, $banks);
+
+ $banks = utils::get_shareable_question_banks($course->id, null, 'qbank2');
+ $this->assertArrayHasKey($qbank2->cmid, $banks);
+ $this->assertArrayNotHasKey($qbank->cmid, $banks);
+ }
+ /**
+ * Test getting a question bank by idnumber.
+ */
+ public function test_get_qbank_by_idnumber(): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Create a course.
+ $course = $this->getDataGenerator()->create_course(['fullname' => 'Course 1', 'shortname' => 'C1']);
+ // Create a question bank.
+ $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => '']);
+ $qbank2 = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => 'abcd1234']);
+
+ $generator = $this->getDataGenerator();
+ $attemptgenerator = $generator->get_plugin_generator('filter_embedquestion');
+ $attemptgenerator->create_embeddable_question('truefalse', null, [],
+ ['contextid' => \context_module::instance($qbank->cmid)->id]);
+ $attemptgenerator->create_embeddable_question('truefalse', null, [],
+ ['contextid' => \context_module::instance($qbank2->cmid)->id]);
+
+ // Check that we can get the question bank.
+ $this->assertEquals($qbank->cmid, utils::get_qbank_by_idnumber($course->id));
+ $this->assertEquals($qbank2->cmid, utils::get_qbank_by_idnumber($course->id, 'abcd1234'));
+
+ $qbank3 = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => '']);
+ $attemptgenerator->create_embeddable_question('truefalse', null, [],
+ ['contextid' => \context_module::instance($qbank3->cmid)->id]);
+ // Can't get the correct question bank if there are multiple banks without idnumber.
+ $this->assertEquals(-1, utils::get_qbank_by_idnumber($course->id));
+ // Can't get a question bank doesn't exist in the course.
+ $this->assertEquals(null, utils::get_qbank_by_idnumber($course->id, 'C2'));
+ // Can't get a question bank with an idnumber that does not exist.
+ $this->assertEquals(null, utils::get_qbank_by_idnumber($course->id, 'randomidnumber'));
+ }
}
diff --git a/version.php b/version.php
index 0e4e65e..72f1504 100644
--- a/version.php
+++ b/version.php
@@ -24,10 +24,10 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2025050100;
+$plugin->version = 2025091602;
$plugin->requires = 2024042200; // Requires Moodle 4.4.
$plugin->component = 'filter_embedquestion';
$plugin->maturity = MATURITY_STABLE;
-$plugin->release = '2.3 for Moodle 4.4+';
+$plugin->release = '2.4 for Moodle 5.0+';
$plugin->outestssufficient = true;