news 2026/10/9 1:25:56

Moodle 5.0 qbank_bulkmove 升级指南:批量移动题目从「服务端表单渲染」迁移到「Modal + WebService」架构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Moodle 5.0 qbank_bulkmove 升级指南:批量移动题目从「服务端表单渲染」迁移到「Modal + WebService」架构
  • 教育
  • 后端
  • 前端

【免费下载链接】moodle

Moodle - the world's open source learning platform

项目地址:https://gitcode.com/gh_mirrors/mo/moodle
点击查看免费下载

本文以仓库中 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; }

该方法接收三个参数并返回渲染数据数组:

参数类型说明
$addcontextsarray参与渲染分类下拉菜单的上下文(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为能力过滤条件,并把当前题库置顶。随后:

  1. 用question_category_selector构建当前题库的分类选择器,选中项格式为"{当前分类ID},{当前contextID}",并开启autocomplete;
  2. 用single_button生成一个默认disabled的 "Move questions" 按钮,携带data-action="bulkmovesave";
  3. 返回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

项目地址:https://gitcode.com/gh_mirrors/mo/moodle
点击查看免费下载
上一篇:Salvo框架中OpenAPI查询参数别名序列化问题解析
下一篇:Ruoyi-AI项目低代码平台功能演进与AI集成实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 1:23:19

用Go+微信小程序开发校园论坛:JWT与游标分页实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/9 1:23:04

ponytail 插件怎么用?轻量化任务编排与快捷触发实战指南

1. 从“ponytail”这个标题说起&#xff1a;它到底是什么第一次看到“ponytail”这个词&#xff0c;很多人脑子里蹦出来的画面大概是扎起来的马尾辫。但如果它出现在技术社区、插件市场或者效率工具的讨论里&#xff0c;那它大概率不是发型教程&#xff0c;而是一个被开发者拿来…

作者头像 李华
网站建设 2026/10/9 1:23:02

OpenShell:Windows经典开始菜单的稳定增强方案

1. OpenShell 是什么&#xff1a;一个被严重误读的开源项目名称OpenShell 这个名字在当前技术社区里&#xff0c;正经历一场典型的“语义漂移”——它既不是某个新发布的跨平台终端模拟器&#xff0c;也不是某家创业公司推出的云 Shell 服务&#xff0c;更不是 macOS 或 Window…

作者头像 李华
网站建设 2026/10/9 1:22:36

huggingface 下载方法 测试ok

目录 2026.05国内替代下载方法&#xff1a; 官网&#xff1a;https://huggingface.co/ 202610最新下载命令 windows系统下载 测试ok&#xff1a; python下载方法&#xff1a; ~/.bashrc 缓存目录&#xff0c;默认模型下载目录 设置缓存目录&#xff1a; git下载方法 …

作者头像 李华