MDL-84900 questions: Limit list of shared question banks
The "Switch question bank" modal was attempting to load every question bank in the system the user has access to. For a highly privileged user, this could mean loading the coursemodinfo cache for every course in the system, resulting in memory exhaustion. This change updates the API for getting shared question banks so it can be searched by name and limited to a number of results. There is a new external service that uses this functionality, and a Javascript data provider for calling the service. The autocomplete in the "Switch question bank" modal will now use this data provider for the "Other question bank" autocomplete, rather than loading all banks up front.
This commit is contained in:
@@ -268,6 +268,7 @@ $string['numqas'] = 'No. question attempts';
|
||||
$string['numquestions'] = 'No. questions';
|
||||
$string['numquestionsandhidden'] = '{$a->numquestions} (+{$a->numhidden} hidden +{$a->numdraft} draft)';
|
||||
$string['otherquestionbank'] = 'Other question banks';
|
||||
$string['otherquestionbankstoomany'] = '> {$a} results, please refine your search.';
|
||||
$string['page-question-x'] = 'Any question page';
|
||||
$string['page-question-edit'] = 'Question editing page';
|
||||
$string['page-question-category'] = 'Question category page';
|
||||
|
||||
@@ -3294,6 +3294,12 @@ $functions = array(
|
||||
'type' => 'write',
|
||||
'ajax' => true,
|
||||
],
|
||||
'core_question_search_shared_banks' => [
|
||||
'classname' => '\core_question\external\search_shared_banks',
|
||||
'description' => 'Get a list of shared question banks filtered by a search term.',
|
||||
'type' => 'read',
|
||||
'ajax' => true,
|
||||
],
|
||||
'core_message_set_unsent_message' => [
|
||||
'classname' => 'core_message\external\set_unsent_message',
|
||||
'description' => 'Store an unsent message string',
|
||||
|
||||
+1
-1
@@ -5,6 +5,6 @@ define("mod_quiz/add_question_modal",["exports","core/modal","core/fragment","co
|
||||
* @module mod_quiz/add_question_modal
|
||||
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=_interopRequireDefault(_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);class AddQuestionModal extends _modal.default{configure(modalConfig){modalConfig.large=!0,modalConfig.show=!0,modalConfig.removeOnClose=!0,this.setContextId(modalConfig.contextId),this.setAddOnPageId(modalConfig.addOnPage),this.quizCmId=modalConfig.quizCmId,this.bankCmId=modalConfig.bankCmId,this.originalTitle=modalConfig.title,super.configure(modalConfig)}constructor(root){super(root),this.contextId=null,this.addOnPageId=null}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setAddOnPageId(id){this.addOnPageId=id}getAddOnPageId(){return this.addOnPageId}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("mod_quiz","switch_question_bank",this.getContextId(),{quizcmid:this.quizCmId,bankcmid:this.bankCmId}));const placeholder=await(0,_str.getString)("searchbyname","mod_quiz");return await this.getBodyPromise(),await _formAutocomplete.default.enhance(Selector,!1,"",placeholder,!1,!0,"",!0),null===(_document$querySelect=document.querySelector(".search-banks .form-autocomplete-selection"))||void 0===_document$querySelect||_document$querySelect.classList.add("d-none"),this}}return _exports.default=AddQuestionModal,_exports.default}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=_interopRequireDefault(_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);class AddQuestionModal extends _modal.default{configure(modalConfig){modalConfig.large=!0,modalConfig.show=!0,modalConfig.removeOnClose=!0,this.setContextId(modalConfig.contextId),this.setAddOnPageId(modalConfig.addOnPage),this.quizCmId=modalConfig.quizCmId,this.bankCmId=modalConfig.bankCmId,this.originalTitle=modalConfig.title,super.configure(modalConfig)}constructor(root){super(root),this.contextId=null,this.addOnPageId=null}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setAddOnPageId(id){this.addOnPageId=id}getAddOnPageId(){return this.addOnPageId}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("mod_quiz","switch_question_bank",this.getContextId(),{quizcmid:this.quizCmId,bankcmid:this.bankCmId}));const placeholder=await(0,_str.getString)("searchbyname","mod_quiz");return 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"),this}}return _exports.default=AddQuestionModal,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=add_question_modal.min.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -132,7 +132,7 @@ export default class AddQuestionModal extends Modal {
|
||||
await AutoComplete.enhance(
|
||||
Selector,
|
||||
false,
|
||||
'',
|
||||
'core_question/question_banks_datasource',
|
||||
placeholder,
|
||||
false,
|
||||
true,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
define("core_question/question_banks_datasource",["exports","core/ajax","core/notification"],(function(_exports,_ajax,_notification){var obj;
|
||||
/**
|
||||
* Autocomplete data source for shared question banks.
|
||||
*
|
||||
* @module core_question/question_banks_datasource
|
||||
* @copyright 2025 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};var _default={transport:function(selector,query,callback){const contextId=document.querySelector(selector).dataset.contextid;if(!contextId)throw new Error("The attribute data-contextid is required on "+selector);(0,_ajax.call)([{methodname:"core_question_search_shared_banks",args:{contextid:contextId,search:query}}])[0].then(callback).catch(_notification.default.exception)},processResults:(selector,results)=>results.sharedbanks};return _exports.default=_default,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=question_banks_datasource.min.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"question_banks_datasource.min.js","sources":["../src/question_banks_datasource.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Autocomplete data source for shared question banks.\n *\n * @module core_question/question_banks_datasource\n * @copyright 2025 onwards Catalyst IT EU {@link https://catalyst-eu.net}\n * @author Mark Johnson <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport Notification from 'core/notification';\n\nexport default {\n\n transport: function(selector, query, callback) {\n const element = document.querySelector(selector);\n const contextId = element.dataset.contextid;\n\n if (!contextId) {\n throw new Error('The attribute data-contextid is required on ' + selector);\n }\n\n fetchMany([{\n methodname: 'core_question_search_shared_banks',\n args: {\n contextid: contextId,\n search: query,\n },\n }])[0]\n .then(callback)\n .catch(Notification.exception);\n },\n\n processResults: (selector, results) => {\n return results.sharedbanks;\n },\n};\n"],"names":["transport","selector","query","callback","contextId","document","querySelector","dataset","contextid","Error","methodname","args","search","then","catch","Notification","exception","processResults","results","sharedbanks"],"mappings":";;;;;;;;sKA2Be,CAEXA,UAAW,SAASC,SAAUC,MAAOC,gBAE3BC,UADUC,SAASC,cAAcL,UACbM,QAAQC,cAE7BJ,gBACK,IAAIK,MAAM,+CAAiDR,yBAG3D,CAAC,CACPS,WAAY,oCACZC,KAAM,CACFH,UAAWJ,UACXQ,OAAQV,UAEZ,GACHW,KAAKV,UACLW,MAAMC,sBAAaC,YAGxBC,eAAgB,CAAChB,SAAUiB,UAChBA,QAAQC"}
|
||||
@@ -0,0 +1,52 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Autocomplete data source for shared question banks.
|
||||
*
|
||||
* @module core_question/question_banks_datasource
|
||||
* @copyright 2025 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import {call as fetchMany} from 'core/ajax';
|
||||
import Notification from 'core/notification';
|
||||
|
||||
export default {
|
||||
|
||||
transport: function(selector, query, callback) {
|
||||
const element = document.querySelector(selector);
|
||||
const contextId = element.dataset.contextid;
|
||||
|
||||
if (!contextId) {
|
||||
throw new Error('The attribute data-contextid is required on ' + selector);
|
||||
}
|
||||
|
||||
fetchMany([{
|
||||
methodname: 'core_question_search_shared_banks',
|
||||
args: {
|
||||
contextid: contextId,
|
||||
search: query,
|
||||
},
|
||||
}])[0]
|
||||
.then(callback)
|
||||
.catch(Notification.exception);
|
||||
},
|
||||
|
||||
processResults: (selector, results) => {
|
||||
return results.sharedbanks;
|
||||
},
|
||||
};
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core_question\external;
|
||||
|
||||
use core_external\external_api;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_single_structure;
|
||||
use core_external\external_multiple_structure;
|
||||
use core_external\external_value;
|
||||
use core_question\local\bank\question_bank_helper;
|
||||
use core\context;
|
||||
|
||||
/**
|
||||
* Return a filtered of the user's shared question banks
|
||||
*
|
||||
* For use with core_question/question_banks_datasource as a source for autocomplete suggestions.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2025 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class search_shared_banks extends external_api {
|
||||
|
||||
/**
|
||||
* @var int The maximum number of banks to return.
|
||||
*/
|
||||
const MAX_RESULTS = 20;
|
||||
|
||||
/**
|
||||
* Define parameters for external function.
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters(
|
||||
[
|
||||
'contextid' => new external_value(PARAM_INT, 'The current context ID.'),
|
||||
'search' => new external_value(PARAM_TEXT, 'Search terms by which to filter the shared banks.', default: ''),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return ID and formatted name of question banks accessible by the user, in courses other than the one $contextid is in.
|
||||
*
|
||||
* @param int $contextid Context ID of the current activity
|
||||
* @param string $search String to filter results by question bank name
|
||||
* @return array
|
||||
*/
|
||||
public static function execute(int $contextid, string $search = ''): array {
|
||||
[
|
||||
'contextid' => $contextid,
|
||||
'search' => $search,
|
||||
] = self::validate_parameters(self::execute_parameters(), [
|
||||
'contextid' => $contextid,
|
||||
'search' => $search,
|
||||
]);
|
||||
|
||||
$modulecontext = context::instance_by_id($contextid);
|
||||
$courseid = $modulecontext->get_parent_context()->instanceid;
|
||||
|
||||
$sharedbanks = question_bank_helper::get_activity_instances_with_shareable_questions(
|
||||
notincourseids: [$courseid],
|
||||
havingcap: ['moodle/question:managecategory'],
|
||||
filtercontext: $modulecontext,
|
||||
search: $search,
|
||||
limit: self::MAX_RESULTS + 1, // Return up to 1 extra result, so we know there are more.
|
||||
);
|
||||
|
||||
$suggestions = array_map(
|
||||
fn($sharedbank) => [
|
||||
'value' => $sharedbank->modid,
|
||||
'label' => $sharedbank->coursenamebankname,
|
||||
],
|
||||
$sharedbanks,
|
||||
);
|
||||
|
||||
if (count($suggestions) > self::MAX_RESULTS) {
|
||||
// If there are too many results, replace the last one with a placeholder.
|
||||
$suggestions[array_key_last($suggestions)] = [
|
||||
'value' => 0,
|
||||
'label' => get_string('otherquestionbankstoomany', 'question', self::MAX_RESULTS),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'sharedbanks' => $suggestions,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Define return values.
|
||||
*
|
||||
* @return external_single_structure
|
||||
*/
|
||||
public static function execute_returns(): external_single_structure {
|
||||
return new external_single_structure([
|
||||
'sharedbanks' => new external_multiple_structure(
|
||||
new external_single_structure([
|
||||
'value' => new external_value(PARAM_INT, 'Module ID of the shared bank.'),
|
||||
'label' => new external_value(PARAM_TEXT, 'Formatted bank name'),
|
||||
]),
|
||||
'List of shared banks',
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,8 @@ class question_bank_helper {
|
||||
* it will only be included if the other parameters allow it.
|
||||
* @param ?context $filtercontext Optional context to use for all string filtering, useful for performance when calling with
|
||||
* parameters that will get banks across multiple contexts.
|
||||
* @param string $search Optional term to search question bank instances by name
|
||||
* @param int $limit The number of results to return (default 0 = no limit)
|
||||
* @return stdClass[]
|
||||
*/
|
||||
public static function get_activity_instances_with_shareable_questions(
|
||||
@@ -137,7 +139,9 @@ class question_bank_helper {
|
||||
array $havingcap = [],
|
||||
bool $getcategories = false,
|
||||
int $currentbankid = 0,
|
||||
context $filtercontext = null,
|
||||
?context $filtercontext = null,
|
||||
string $search = '',
|
||||
int $limit = 0,
|
||||
): array {
|
||||
return self::get_bank_instances(true,
|
||||
$incourseids,
|
||||
@@ -146,6 +150,8 @@ class question_bank_helper {
|
||||
$currentbankid,
|
||||
$havingcap,
|
||||
$filtercontext,
|
||||
$search,
|
||||
$limit,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,7 +174,7 @@ class question_bank_helper {
|
||||
array $havingcap = [],
|
||||
bool $getcategories = false,
|
||||
int $currentbankid = 0,
|
||||
context $filtercontext = null,
|
||||
?context $filtercontext = null,
|
||||
): array {
|
||||
return self::get_bank_instances(false,
|
||||
$incourseids,
|
||||
@@ -194,6 +200,8 @@ class question_bank_helper {
|
||||
* @param array $havingcap current user must have these capabilities on each bank context.
|
||||
* @param ?context $filtercontext Optional context to use for all string filtering, useful for performance when calling with
|
||||
* parameters that will get banks across multiple contexts.
|
||||
* @param string $search Optional term to search question bank instances by name
|
||||
* @param int $limit The number of results to return (default 0 = no limit)
|
||||
* @return stdClass[]
|
||||
*/
|
||||
private static function get_bank_instances(
|
||||
@@ -204,6 +212,8 @@ class question_bank_helper {
|
||||
int $currentbankid = 0,
|
||||
array $havingcap = [],
|
||||
?context $filtercontext = null,
|
||||
string $search = '',
|
||||
int $limit = 0,
|
||||
): array {
|
||||
global $DB;
|
||||
|
||||
@@ -245,13 +255,17 @@ class question_bank_helper {
|
||||
if ($plugin === self::get_default_question_bank_activity_name()) {
|
||||
$sql .= " AND p{$key}.type <> '" . self::TYPE_PREVIEW . "'";
|
||||
}
|
||||
if (!empty($search)) {
|
||||
$sql .= " AND " . $DB->sql_like("p{$key}.name", ":search{$key}", false);
|
||||
$params["search{$key}"] = "%{$search}%";
|
||||
}
|
||||
$pluginssql[] = $sql;
|
||||
}
|
||||
$pluginssql = implode(' ', $pluginssql);
|
||||
|
||||
// Build the SQL to filter out any requested course ids.
|
||||
if (!empty($notincourseids)) {
|
||||
[$notincoursesql, $notincourseparams] = $DB->get_in_or_equal($notincourseids, SQL_PARAMS_QM, 'param', false);
|
||||
[$notincoursesql, $notincourseparams] = $DB->get_in_or_equal($notincourseids, SQL_PARAMS_NAMED, 'param', false);
|
||||
$notincoursesql = "AND cm.course {$notincoursesql}";
|
||||
$params = array_merge($params, $notincourseparams);
|
||||
} else {
|
||||
@@ -260,7 +274,7 @@ class question_bank_helper {
|
||||
|
||||
// Build the SQL to include ONLY records belonging to the requested courses.
|
||||
if (!empty($incourseids)) {
|
||||
[$incoursesql, $incourseparams] = $DB->get_in_or_equal($incourseids);
|
||||
[$incoursesql, $incourseparams] = $DB->get_in_or_equal($incourseids, SQL_PARAMS_NAMED);
|
||||
$incoursesql = " AND cm.course {$incoursesql}";
|
||||
$params = array_merge($params, $incourseparams);
|
||||
} else {
|
||||
@@ -269,8 +283,8 @@ class question_bank_helper {
|
||||
|
||||
// Optionally order the results by the requested bank id.
|
||||
if (!empty($currentbankid)) {
|
||||
$orderbysql = " ORDER BY CASE WHEN cm.id = ? THEN 0 ELSE 1 END ASC, cm.id DESC ";
|
||||
$params[] = $currentbankid;
|
||||
$orderbysql = " ORDER BY CASE WHEN cm.id = :currentbankid THEN 0 ELSE 1 END ASC, cm.id DESC ";
|
||||
$params['currentbankid'] = $currentbankid;
|
||||
} else {
|
||||
$orderbysql = '';
|
||||
}
|
||||
@@ -284,7 +298,7 @@ class question_bank_helper {
|
||||
GROUP BY cm.id, cm.course
|
||||
{$orderbysql}";
|
||||
|
||||
$rs = $DB->get_recordset_sql($sql, $params);
|
||||
$rs = $DB->get_recordset_sql($sql, $params, limitnum: $limit);
|
||||
$banks = [];
|
||||
|
||||
foreach ($rs as $cm) {
|
||||
|
||||
@@ -62,11 +62,6 @@ class switch_question_bank implements \renderable, \templatable {
|
||||
[, $cm] = get_module_from_cmid($this->quizcmid);
|
||||
$cminfo = cm_info::create($cm);
|
||||
|
||||
$sharedbanks = question_bank_helper::get_activity_instances_with_shareable_questions(
|
||||
notincourseids: [$this->courseid],
|
||||
havingcap: ['moodle/question:managecategory'],
|
||||
filtercontext: $cminfo->context,
|
||||
);
|
||||
$coursesharedbanks = question_bank_helper::get_activity_instances_with_shareable_questions(
|
||||
incourseids: [$this->courseid],
|
||||
havingcap: ['moodle/question:managecategory'],
|
||||
@@ -77,12 +72,11 @@ class switch_question_bank implements \renderable, \templatable {
|
||||
return [
|
||||
'quizname' => $cminfo->get_formatted_name(),
|
||||
'quizcmid' => $this->quizcmid,
|
||||
'quizcontextid' => $cminfo->context->id,
|
||||
'hascoursesharedbanks' => !empty($coursesharedbanks),
|
||||
'coursesharedbanks' => $coursesharedbanks,
|
||||
'hasrecentlyviewedbanks' => !empty($recentlyviewedbanks),
|
||||
'recentlyviewedbanks' => $recentlyviewedbanks,
|
||||
'hassharedbanks' => !empty($sharedbanks),
|
||||
'sharedbanks' => $sharedbanks,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
{
|
||||
"quizname": "Quiz 1",
|
||||
"quizcmid": 1,
|
||||
"quizcontextid": 1,
|
||||
"hascoursesharedbanks": true,
|
||||
"coursesharedbanks": [
|
||||
{
|
||||
@@ -131,14 +132,9 @@
|
||||
<hr class="w-75">
|
||||
{{/hasrecentlyviewedbanks}}
|
||||
|
||||
{{#hassharedbanks}}
|
||||
<div class="search-banks">
|
||||
<h5>{{#str}}otherquestionbank, core_question{{/str}}</h5>
|
||||
<select class="form-select" id="searchbanks">
|
||||
<option value=""></option>
|
||||
{{#sharedbanks}}
|
||||
<option value="{{modid}}">{{{coursenamebankname}}}</option>
|
||||
{{/sharedbanks}}
|
||||
</select>
|
||||
</div>
|
||||
{{/hassharedbanks}}
|
||||
|
||||
<div class="search-banks">
|
||||
<h5>{{#str}}otherquestionbank, core_question{{/str}}</h5>
|
||||
<select class="form-select" id="searchbanks" data-contextid="{{quizcontextid}}">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -151,6 +151,54 @@ final class question_bank_helper_test extends \advanced_testcase {
|
||||
$this->assertEquals(1, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* We should be able to filter sharable question bank instances by name.
|
||||
*
|
||||
* @covers ::get_activity_instances_with_shareable_questions
|
||||
* @return void
|
||||
*/
|
||||
public function test_get_instances_by_name(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$user = self::getDataGenerator()->create_user();
|
||||
$roles = $DB->get_records('role', [], '', 'shortname, id');
|
||||
self::setUser($user);
|
||||
|
||||
$sharedmodgen = self::getDataGenerator()->get_plugin_generator('mod_qbank');
|
||||
$category1 = self::getDataGenerator()->create_category();
|
||||
$course1 = self::getDataGenerator()->create_course(['category' => $category1->id]);
|
||||
role_assign($roles['editingteacher']->id, $user->id, \core\context\course::instance($course1->id));
|
||||
|
||||
$sharedmods = [];
|
||||
for ($i = 1; $i <= 21; $i++) {
|
||||
$sharedmods[$i] = $sharedmodgen->create_instance(['course' => $course1, 'name' => "Shared bank {$i}"]);
|
||||
}
|
||||
$sharedmods[22] = $sharedmodgen->create_instance(['course' => $course1, 'name' => "Another bank"]);
|
||||
|
||||
// We get all banks with no parameters.
|
||||
$allsharedbanks = question_bank_helper::get_activity_instances_with_shareable_questions();
|
||||
$this->assertCount(22, $allsharedbanks);
|
||||
|
||||
// Searching for "2", we get the 4 banks with "2" in the name.
|
||||
$twobanks = question_bank_helper::get_activity_instances_with_shareable_questions(search: '2');
|
||||
$this->assertCount(4, $twobanks);
|
||||
$this->assertEquals(
|
||||
[$sharedmods[2]->cmid, $sharedmods[12]->cmid, $sharedmods[20]->cmid, $sharedmods[21]->cmid],
|
||||
array_map(fn($bank) => $bank->modid, $twobanks),
|
||||
);
|
||||
|
||||
// Searching for "Shared bank" with no limit, we should get all 21, but not "Another bank".
|
||||
$sharedbanks = question_bank_helper::get_activity_instances_with_shareable_questions(search: 'Shared bank');
|
||||
$this->assertCount(21, $sharedbanks);
|
||||
$this->assertEmpty(array_filter($sharedbanks, fn($bank) => in_array($bank->name, ['Another bank'])));
|
||||
|
||||
// Searching for "Shared bank" with a limit of 20, we should get all except number 21 and "Another bank".
|
||||
$limitedbanks = question_bank_helper::get_activity_instances_with_shareable_questions(search: 'Shared bank', limit: 20);
|
||||
$this->assertCount(20, $limitedbanks);
|
||||
$this->assertEmpty(array_filter($limitedbanks, fn($bank) => in_array($bank->name, ['Shared bank 21', 'Another bank'])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert creating a default mod_qbank instance on a course provides the expected boilerplate settings.
|
||||
*
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2025032800.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2025032800.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '5.0dev+ (Build: 20250328)'; // Human-friendly version name
|
||||
|
||||
Reference in New Issue
Block a user