- 教育
- 后端
- 前端
【免费下载链接】moodle
Moodle - the world's open source learning platform
本文以仓库中 qbank_bulkmove 升级说明 为骨架,系统梳理 Moodle 5.0 中「批量移动题目」功能的两次 API 废弃(Deprecation)及对应替代方案,并结合helper、renderer、output/bulk_move、AMD 模态框模块与core_question\external\move_questionsWebService 的源码实现,说明新旧两套架构的差异、迁移路径与测试验证方式。读完本文,你将掌握 qbank_bulkmove 在 Moodle 5.0 后的正确调用姿势,以及如何在不破坏既有功能的前提下完成第三方扩展的适配升级。
一、升级概览:一次针对批量移动题目的架构重构
Moodle 5.0 对qbank_bulkmove插件做了两处标记为 Deprecated 的变更,全部对应追踪条目MDL-71378:
| 废弃 API | 替代方案 |
|---|---|
qbank_bulkmove\helper::get_displaydata | 由模态框(modal)与 WebService 取代,参见qbank_bulkmove/modal_question_bank_bulkmove与core_question_external\move_questions |
qbank_bulkmove\output\renderer::render_bulk_move_form | 由qbank_bulkmove\output\bulk_move取代 |
从整体方向看,这是一次前后端分离式重构:旧实现由服务端 PHP 直接拼装「分类下拉 + 提交表单」并渲染整页模板;新实现改为「浏览器端 Modal 模态框选择目标题库与分类 + WebService 异步执行移动」的两段式交互。以下各节分别拆解旧实现、新实现与迁移要点。
二、被废弃的旧实现:get_displaydata与render_bulk_move_form
2.1helper::get_displaydata:拼装表单展示数据
旧版的数据装配入口位于 classes/helper.php,其职责是生成批量移动表单所需的三项数据:
#[\core\attribute\deprecated( replacement: 'replaced by a modal and webservice. See qbank_bulkmove/modal_question_bank_bulkmove and core_question_external\move_questions', since: '5.0', mdl: 'MDL-71378' )] public static function get_displaydata(array $addcontexts, \moodle_url $moveurl, \moodle_url $returnurl): array { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); $displaydata = []; $displaydata ['categorydropdown'] = \qbank_managecategories\helper::question_category_select_menu($addcontexts, false, 0, '', -1, true); $displaydata ['moveurl'] = $moveurl; $displaydata['returnurl'] = $returnurl; return $displaydata; }该方法接收三个参数并返回渲染数据数组:
| 参数 | 类型 | 说明 |
|---|---|---|
$addcontexts | array | 参与渲染分类下拉菜单的上下文(context)列表 |
$moveurl | \moodle_url | 表单提交所指向的移动处理脚本 URL |
$returnurl | \moodle_url | 取消操作时返回的 URL |
返回数组包含三个键:categorydropdown(由 qbank_managecategories\helper::question_category_select_menu 生成的下拉框 HTML)、moveurl、returnurl。可以看出,旧方案依赖插件qbank_managecategories在服务端直接产出分类下拉的完整 HTML,数据与视图强耦合。
2.2renderer::render_bulk_move_form:服务端渲染整个表单
视图层入口位于 classes/output/renderer.php:
#[\core\attribute\deprecated('qbank_bulkmove\output\bulk_move', since: '5.0', mdl: 'MDL-71378')] public function render_bulk_move_form($displaydata) { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); return $this->render_from_template('qbank_bulkmove/bulk_move', $displaydata); }它接收get_displaydata的返回结果,直接渲染 templates/bulk_move.mustache 模板。该模板内含「题库(bank)下拉」「分类下拉」与「Move questions 提交按钮」三块区域,其中分类选择复用了core_question/question_category_selector局部模板:
<div class="search-banks"> <h5>{{#str}}questionbank, question{{/str}}</h5> <select class="form-select bulk-move d-none" id="searchbanks" >public static function bulk_move_questions(string $movequestionselected, \stdClass $tocategory): void { global $DB, $CFG; require_once($CFG->libdir .'/questionlib.php'); if ($questionids = explode(',', $movequestionselected)) { [$usql, $params] = $DB->get_in_or_equal($questionids); $sql = "SELECT q.*, c.contextid FROM {question} q JOIN {question_versions} qv ON qv.questionid = q.id JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid JOIN {question_categories} c ON c.id = qbe.questioncategoryid WHERE q.id {$usql}"; $questions = $DB->get_records_sql($sql, $params); foreach ($questions as $question) { question_require_capability_on($question, 'move'); } question_move_questions_to_category($questionids, $tocategory->id); } }它通过{question}、{question_versions}、{question_bank_entries}、{question_categories}四表联查,为每个待移动题目调用question_require_capability_on($question, 'move')做逐题能力校验,最终委托给核心库函数question_move_questions_to_category。同时,helper 中的process_question_ids(array $rawquestions)(源码 L87-L101)负责把表单 post 中的q<ID>形式的键解析为题目 ID 数组与逗号分隔串,在新架构中仍可复用。
四、新架构之二:qbank_bulkmove\output\bulk_move输出类
替代renderer::render_bulk_move_form的是 classes/output/bulk_move.php 中的bulk_move类(实现\renderable与\templatable),其类注释明确写道:"Output class to create a modal template with selects for question banks, question categories, and a move button."—— 即专门为模态框生成「题库选择、分类选择、移动按钮」三段式内容。
4.1 构造与数据导出
构造器接收两个 ID(源码 L52-L55):
public function __construct(int $currentbankid, int $currentcategoryid) { $this->currentbankid = $currentbankid; $this->currentcategoryid = $currentcategoryid; }export_for_template()(源码 L63-L108)是整个新渲染链路的灵魂,它按当前题库模块是否「发布共享题目」走两条数据路径:
- 共享题库(
plugin_supports('mod', $modname, FEATURE_PUBLISHES_QUESTIONS)为真):调用question_bank_helper::get_activity_instances_with_shareable_questions(...); - 私有题库:调用
question_bank_helper::get_activity_instances_with_private_questions(...),并限定在当前课程(incourseids: [$currentbankcm->course])。
两者都以moodle/question:add为能力过滤条件,并把当前题库置顶。随后:
- 用
question_category_selector构建当前题库的分类选择器,选中项格式为"{当前分类ID},{当前contextID}",并开启autocomplete; - 用
single_button生成一个默认disabled的 "Move questions" 按钮,携带data-action="bulkmovesave"; - 返回
bank、categories、save、contextid四项模板数据。
分类与题库的候选集合都来自 question_bank_helper.php 的两个公开静态方法,其底层get_bank_instances()负责构建 SQL 并支持按能力、课程、搜索词与数量上限过滤,动态加载其余题库与分类正是依托这一层。
4.2 Fragment 输出入口
该类本身不直接渲染页面,而是通过 lib.php 中的 Fragment 回调函数按需输出:
function qbank_bulkmove_output_fragment_bulk_move(array $args) { global $OUTPUT; $currentbankid = clean_param($args['context']->instanceid, PARAM_INT); $currentcategoryid = clean_param($args['categoryid'], PARAM_INT); $qbankcatchooser = new \qbank_bulkmove\output\bulk_move($currentbankid, $currentcategoryid); return $OUTPUT->render($qbankcatchooser); }它从 Fragment 参数中提取instanceid(当前题库的 course module ID)与categoryid,实例化bulk_move并渲染——这就是 Modal 前端通过Fragment.loadFragment('qbank_bulkmove', 'bulk_move', ...)拉取弹窗内容的服务端端点。
五、新架构之三:AMD 模态框modal_question_bank_bulkmove
前端核心是 amd/src/modal_question_bank_bulkmove.js 中的ModalQuestionBankBulkmove类(继承core/modal的 Modal)。
5.1 初始化与触发
init(contextId, categoryId)(源码 L59-L72)注册全局点击监听:当点击「批量操作」下拉中的dropdown-item且其name === 'move'时,创建模态框并传入当前 context/category。该 JS 的加载由批量操作类负责,见下文第六节。
initSelectedCategoryId()会优先解析当前 URL 中的filter参数(从中取出category.values[0])作为初始分类,否则使用传入的categoryId——这样既尊重用户在题库页的筛选状态,又保证默认行为可预期。
5.2 弹窗内容加载与交互
display()(源码 L117-L133)通过Fragment.loadFragment('qbank_bulkmove', 'bulk_move', currentBankContextId, {categoryid})获取第四节的 Fragment 内容,然后:
enhanceSelects()(源码 L300-L324):调用AutoComplete.enhance把「题库选择」下拉增强为自动补全框(数据源为core_question/question_banks_datasource),把「分类选择」下拉也增强为自动补全框;registerEnhancedEventListeners():监听分类变化以刷新保存按钮状态;监听题库切换时通过Fragment.loadFragment('core_question', 'category_selector', ...)动态替换分类选择器,仅展示所选题库的分类,并处理自动补全「搜索受限」的占位选项;updateSaveButtonState():当目标分类存在且与当前分类不同时才启用「Move questions」按钮,避免无效提交。
5.3 确认页与最终提交
点击保存按钮后,displayConfirmMove()(源码 L169-L193)将模态框切换为确认视图:标题变为confirm,正文为confirmmove("Are you sure you want to move these questions?"),并从 bulk_move_footer.mustache 渲染「Confirm / Cancel」两个页脚按钮。
moveQuestionsAfterConfirm()(源码 L269-L293)收集表格中所有勾选的题目(选择器table#categoryquestions input[id^="checkq"]),调用core_question/repository的moveQuestions(targetContextId, targetCategoryId, questionids, returnurl)——其底层即走第三节的core_question_external\move_questionsWebService——成功后把浏览器导航到返回 URL(含新分类的 filter 参数)。
六、批量操作注册:从功能入口看新旧衔接
新旧架构在入口层面是无缝衔接的:批量操作注册仍由 classes/bulk_move_action.php 与 classes/plugin_feature.php 负责,它们不属于废弃范围。
bulk_move_action extends \core_question\local\bank\bulk_action_base(源码 L29-L75)定义:标题movetobulkaction("Move to...")、键move、能力要求moodle/question:moveall与moodle/question:add。其initialise_javascript()从题库页cat页面变量解析当前分类(缺省时回退到question_get_default_category),然后通过$PAGE->requires->js_call_amd('qbank_bulkmove/modal_question_bank_bulkmove', 'init', [contextid, categoryid])初始化模态框——这是连接第五节前端模块与题库页的挂载点;plugin_feature::get_bulk_actions()(源码 L37-L46)在题目历史版本列表页(is_listing_specific_versions())返回空数组、不提供批量移动,其余页面返回[new bulk_move_action($qbank)]。
插件字符串位于 lang/en/qbank_bulkmove.php,包括bulkmoveheader("Move the selected questions to...")、movequestions、confirmmove、warning("You must select a question bank before you can select a category.")等,且声明privacy:metadata为不存储任何个人数据。
七、行为验证:Behat 测试用例
仓库提供了 tests/behat/bulk_move.feature 覆盖新架构的关键行为(共 328 行),典型场景包括:
- 插件开关联动:在站点管理「Question bank plugins」中禁用/启用 "Bulk move questions" 后,题库页「With selected」批量菜单中
move操作随之消失/出现; - 共享题库分类限定:选择某个共享题库后,自动补全的候选分类仅包含该题库自身的分类(如选择了 Test quiz 的分类时,
Test questions 1、Test questions 6可见而其他题库的分类不可见),验证了第五节updateCategorySelector的动态替换逻辑; - 场景同时覆盖了跨课程题库(Course 1/2/3、三个 qbank 活动)与课程级分类的移动路径,可以作为集成测试直接运行。
八、第三方开发者迁移速查
如果你的代码或自定义插件调用了被废弃的 API,请按下表对照迁移:
| 旧 API(Moodle 5.0 起 Deprecated) | 新 API / 新机制 |
|---|---|
\qbank_bulkmove\helper::get_displaydata($addcontexts, $moveurl, $returnurl) | 使用core_question_external\move_questionsWebService(参数newcontextid/newcategoryid/questionids/returnurl),或直接复用\qbank_bulkmove\helper::bulk_move_questions()+process_question_ids() |
\qbank_bulkmove\output\renderer::render_bulk_move_form($displaydata) | 实例化\qbank_bulkmove\output\bulk_move($currentbankid, $currentcategoryid)并渲染;页面内嵌交互改用qbank_bulkmove/modal_question_bank_bulkmoveAMD 模块 |
| 依赖整页表单提交的移动流程 | Fragment 动态加载(qbank_bulkmove_output_fragment_bulk_move)+AutoComplete自动补全 + Modal 确认页两段式交互 |
几点结论与提醒:
- 仓库内没有任何对旧 API 的活动调用残留——move_questions.php 调用的是未废弃的
helper::bulk_move_questions;旧方法仅保留声明并触发emit_deprecation告警; - 从 version.php 可见该组件当前版本为
2026042000、要求 Moodle 核心2026041000、成熟度MATURITY_STABLE; - 两个被废弃方法的
@todo MDL-82413注释表明最终移除计划在Moodle 6.0,请务必在此之前完成自有代码的迁移,避免升级后出现白屏或功能缺失; - 新架构对用户可见行为的影响:移动操作从「整页表单」变为「弹窗选择 + 确认」,且移动完成后 URL 的
filter参数会自动切换到新分类,便于在题库页直接看到移动结果。
九、小结
Moodle 5.0 对qbank_bulkmove的这次升级,本质是把「批量移动题目」从服务端全量表单渲染重构为Fragment + Modal + WebService的现代交互链路:bulk_move输出类负责服务端数据装配,modal_question_bank_bulkmove负责弹窗与自动补全交互,core_question_external\move_questions负责带权限校验的异步执行,而helper::bulk_move_questions这一底层移动逻辑得以保留复用。对于升级维护者而言,只需按第八节的对照表替换两处废弃调用,即可平滑过渡到新架构,并享受分类级联过滤、跨题库移动与更安全的逐题权限校验带来的体验与稳定性提升。
- 教育
- 后端
- 前端
【免费下载链接】moodle
Moodle - the world's open source learning platform
相关推荐
Moodle enrol_guest 插件 5.0 升级指南:enrol_guest_enrol_form 弃用与动态表单迁移
Moodle enrol_guest 插件 5.0 升级指南:enrol_guest_enrol_form 弃用与动态表单迁移 本文基于仓库中 enrol_gu
教育后端前端Moodle tool_mfa 插件升级指南:5.0/5.2 渲染器重构、因子管理表迁移与语言字符串弃用全解析
Moodle tool_mfa 插件升级指南:5.0/5.2 渲染器重构、因子管理表迁移与语言字符串弃用全解析 本文围绕 Moodle https://link
教育后端前端PixiJS v8 迁移指南:从 v7 平滑升级到新一代渲染架构
PixiJS v8 迁移指南:从 v7 平滑升级到新一代渲染架构 PixiJS v8 是自 v7 以来的一次重大架构升级,引入了 WebGPU 渲染支持、异步初
前端图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考