This commit is contained in:
Huong Nguyen 2025-04-04 10:20:03 +07:00
commit ac92443315
14 changed files with 311 additions and 38 deletions

View File

@ -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';

View File

@ -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',

View File

@ -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

View File

@ -132,7 +132,7 @@ export default class AddQuestionModal extends Modal {
await AutoComplete.enhance(
Selector,
false,
'',
'core_question/question_banks_datasource',
placeholder,
false,
true,

View File

@ -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

View File

@ -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 <mark.johnson@catalyst-eu.net>\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"}

View File

@ -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 <mark.johnson@catalyst-eu.net>
* @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;
},
};

View File

@ -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 <mark.johnson@catalyst-eu.net>
* @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',
),
]);
}
}

View File

@ -134,6 +134,10 @@ class question_bank_helper {
* @param bool $getcategories optionally return the categories belonging to these banks.
* @param int $currentbankid optionally include the bank id you want included as the first result from the method return.
* 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(
@ -142,13 +146,19 @@ class question_bank_helper {
array $havingcap = [],
bool $getcategories = false,
int $currentbankid = 0,
?context $filtercontext = null,
string $search = '',
int $limit = 0,
): array {
return self::get_bank_instances(true,
$incourseids,
$notincourseids,
$getcategories,
$currentbankid,
$havingcap
$havingcap,
$filtercontext,
$search,
$limit,
);
}
@ -161,6 +171,8 @@ class question_bank_helper {
* @param bool $getcategories optionally return the categories belonging to these banks.
* @param int $currentbankid optionally include the bank id you want included as the first result from the method return.
* 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.
* @return stdClass[]
*/
public static function get_activity_instances_with_private_questions(
@ -169,13 +181,15 @@ class question_bank_helper {
array $havingcap = [],
bool $getcategories = false,
int $currentbankid = 0,
?context $filtercontext = null,
): array {
return self::get_bank_instances(false,
$incourseids,
$notincourseids,
$getcategories,
$currentbankid,
$havingcap
$havingcap,
$filtercontext,
);
}
@ -191,6 +205,10 @@ class question_bank_helper {
* @param int $currentbankid optionally include the bank id you want included as the first result from the method return.
* it will only be included if the other parameters allow it.
* @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(
@ -200,6 +218,9 @@ class question_bank_helper {
bool $getcategories = false,
int $currentbankid = 0,
array $havingcap = [],
?context $filtercontext = null,
string $search = '',
int $limit = 0,
): array {
global $DB;
@ -241,13 +262,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 {
@ -256,7 +281,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 {
@ -265,8 +290,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 = '';
}
@ -280,7 +305,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) {
@ -293,8 +318,9 @@ class question_bank_helper {
}
}
// Populate the raw record.
$banks[] = self::get_formatted_bank($cm, $currentbankid);
$banks[] = self::get_formatted_bank($cm, $currentbankid, filtercontext: $filtercontext);
}
$rs->close();
return $banks;
}
@ -305,9 +331,15 @@ class question_bank_helper {
*
* @param int $userid of the user to get recently viewed banks for.
* @param int $notincourseid if supplied don't return any in this course id
* @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.
* @return cm_info[]
*/
public static function get_recently_used_open_banks(int $userid, int $notincourseid = 0): array {
public static function get_recently_used_open_banks(
int $userid,
int $notincourseid = 0,
?context $filtercontext = null,
): array {
$prefs = get_user_preferences(self::RECENTLY_VIEWED, null, $userid);
$contextids = !empty($prefs) ? explode(',', $prefs) : [];
if (empty($contextids)) {
@ -328,7 +360,7 @@ class question_bank_helper {
if (!empty($notincourseid) && $notincourseid == $cm->course) {
continue;
}
$record = self::get_formatted_bank($cm);
$record = self::get_formatted_bank($cm, filtercontext: $filtercontext);
$banks[] = $record;
}
@ -370,12 +402,15 @@ class question_bank_helper {
/**
* Populate the raw record with data for use in rendering.
*
* @param stdClass $cm raw course_modules record to populate data from.
* @param int $currentbankid set an 'enabled' flag on the instance that matched this id.
* Used in qbank_bulkmove/bulk_move.mustache
* Used in qbank_bulkmove/bulk_move.mustache
* @param ?context $filtercontext Optional context in which to apply filters.
*
* @return stdClass
*/
private static function get_formatted_bank(stdClass $cm, int $currentbankid = 0): stdClass {
private static function get_formatted_bank(stdClass $cm, int $currentbankid = 0, ?context $filtercontext = null): stdClass {
$cminfo = cm_info::create($cm);
$concatedcats = !empty($cm->cats) ? explode(self::CATEGORY_SEPARATOR, $cm->cats) : [];
@ -390,14 +425,19 @@ class question_bank_helper {
}, $concatedcats);
$bank = new stdClass();
$bank->name = $cminfo->get_formatted_name(['escape' => false]);
$filteroptions = ['escape' => false];
if (!is_null($filtercontext)) {
$filteroptions['context'] = $filtercontext;
}
$bank->name = $cminfo->get_formatted_name($filteroptions);
$bank->modid = $cminfo->id;
$bank->contextid = $cminfo->context->id;
$bank->coursenamebankname = format_string($cminfo->get_course()->shortname, true,
['context' => $cminfo->context, 'escape' => false]) . " - {$bank->name}";
if (!isset($filteroptions['context'])) {
$filteroptions['context'] = context_course::instance($cminfo->get_course()->id);
}
$bank->coursenamebankname = format_string($cminfo->get_course()->shortname, true, $filteroptions) . " - {$bank->name}";
$bank->cminfo = $cminfo;
$bank->questioncategories = $categories;
return $bank;
}

View File

@ -62,25 +62,21 @@ 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']
);
$coursesharedbanks = question_bank_helper::get_activity_instances_with_shareable_questions(
incourseids: [$this->courseid],
havingcap: ['moodle/question:managecategory']
havingcap: ['moodle/question:managecategory'],
filtercontext: $cminfo->context,
);
$recentlyviewedbanks = question_bank_helper::get_recently_used_open_banks($this->userid);
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,
];
}
}

View File

@ -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>

View File

@ -160,6 +160,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.
*

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2025040100.01; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2025040100.02; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '5.0beta (Build: 20250401)'; // Human-friendly version name