接入 Course Blocks API:为 MFE 生成可嵌入讨论链接的设计与实现)
Open edX 上下文内讨论In-Context Discussions接入 Course Blocks API为 MFE 生成可嵌入讨论链接的设计与实现【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform本文是 Open edX 平台 discussions 应用架构决策记录系列中 0005-in-context-discussion-course-blocks.rst 的深度技术解读。该决策回答了单元Unit级上下文内讨论如何被前端消费这一关键问题由 edx-platform 通过 learning MFE 已在使用的 Course Blocks API 下发可直接嵌入 iframe 的讨论话题链接。读完本文你将掌握discussions_embed_url字段的生成机制、discussions_group_at_subsection分组设置对 API 返回结果的影响以及与之对应的 Transformer、URL 工具与数据模型的源码级实现。一、决策背景从链接讨论到提供讨论给前端1.1 前置设计ADR 0004 中的链接机制在 0004-in-context-discussions-linking.rst 中Open edX 团队为新的讨论体系设计了单元与讨论话题的关联方式移除 discussions XBlock改为直接在单元上打标mark a Unit as discussable被标记的单元会以上下文内in-context方式显示讨论 UI课程发布时将plugin_settings拷贝到课程对象的discussions_settingsJSON 字段中存储形如discussions_enable_in_context、discussions_enable_graded_units、discussions_custom_visibility等配置在单元块上新增布尔字段discussions_enabled表达该单元是否可讨论引入通用映射模型DiscussionTopicLink以数据库行的形式记录单元 usage key ↔ 外部讨论 id的对应关系并支持discussions_group_at_subsection按小节subsection/sequence聚合讨论。1.2 本决策要解决的问题前端如何拿到这些链接ADR 0004 解决了如何把讨论链接到单元但还缺少最后一块拼图这些链接的讨论如何提供给前端使前端能在正确的视图中展示上下文内讨论。这正是 0005-in-context-discussion-course-blocks.rst 的核心诉求。其需求非常收敛原文只有一句话An API to access linked discussions for a Unit.即提供一个 API用于访问与单元相关联的讨论。二、核心决策复用 Course Blocks API 下发嵌入链接2.1 决策内容决策Decision部分的原文结论是A direct link to the topic that needs to be embedded can be generated by edx-platform and provided to MFEs via the course blocks API which is already used by the learning MFE. The learning MFE can then directly embed this link in an iframe as a sidebar.拆解为三层含义链接由 edx-platform 生成服务端负责把讨论话题 id转换为可供 MFE 直接使用的完整 URL通过 Course Blocks API 下发直接复用 learning MFE 已经在使用的 course blocks API无需为讨论功能单独新建一套数据拉取通道MFE 以 iframe 侧边栏方式消费learning MFE 拿到链接后直接嵌入 iframe 作为侧边栏实现在课程单元旁就地讨论。Course Blocks API 的入口位于 lms/djangoapps/course_blocks/api.py其中get_course_blocks约 57 行起负责组装课程块结构与各 transformer 注入的附加字段。2.2 请求方式requested_fields 字段筛选前端通过requested_fields参数请求特定字段。当请求requested_fieldsdiscussions_embed_url时API 会在对应单元vertical 块上返回讨论嵌入链接。原文档给出的示意响应如下{ ... block-v1:edXDemoXDemo_Coursetypeverticalblockvertical_98cf62510471: { id: block-v1:edXDemoXDemo_Coursetypeverticalblockvertical_98cf62510471, block_id: vertical_98cf62510471, lms_web_url: http..., legacy_web_url: http..., student_view_url: http..., discussions_embed_url: http://localhost:2002/discussions/course-v1:edXDemoXDemo_Course/topics/zooming-diagrams/ type: vertical, display_name: Zooming Diagrams }, ... }对响应逐字段解读字段含义id/block_id块的完整 usage key 与块 id例如block-v1:edXDemoXDemo_Coursetypeverticalblockvertical_98cf62510471type块类型此处为vertical单元display_name块的显示名如 Zooming Diagramslms_web_url/legacy_web_url/student_view_url课程块 API 常规返回的各类查看地址discussions_embed_url本决策新增的核心字段可嵌入 iframe 的讨论话题地址注意discussions_embed_url的路径结构为/discussions/{course_key}/topics/{topic_id}/其中topic_id即zooming-diagrams对应 ADR 0004 中DiscussionTopicLink.external_id在外部讨论服务如 cs_comments_service中的 commentable_id。三、无讨论单元的返回行为原文档特别强调了一条边界规则For units that dont have a linked discussion, no link will be returned.未关联讨论的单元不会返回该字段而非返回空字符串或 null。这意味着前端可以依据字段是否存在直接判断是否渲染讨论侧边栏无需再做二次校验。这一行为在源码测试中得到印证见下文第六节。四、小节聚合配置discussions_group_at_subsection4.1 设置项与默认值新讨论体验中引入了名为discussions_group_at_subsection的设置用于把讨论按小节subsection而非单元unit聚合。原文档明确By default this setting is disabled and the sidebar next to a unit will only show threads from that unit. However, if this setting is enabled then the MFE should show threads related to all the units from the subsection in the sidebar.默认关闭单元旁的侧边栏只展示该单元自己的话题开启后侧边栏展示该小节下所有单元相关的话题。4.2 开启后 API 的返回变化开启该设置后Blocks API 不再返回单元级话题链接而是为整个 subsection 返回一条链接MFE 可在 UI 层以不同方式呈现如小节级讨论面板。原文档示例{ ... block-v1:edXDemoXDemo_Coursetypeverticalblockvertical_98cf62510471: { id: block-v1:edXDemoXDemo_Coursetypeverticalblockvertical_98cf62510471, block_id: vertical_98cf62510471, lms_web_url: http..., legacy_web_url: http..., student_view_url: http..., discussions_embed_url: http://localhost:2002/discussions/course-v1:edXDemoXDemo_Course/category/lesson-2-lets-get-interactive/ type: vertical, display_name: Zooming Diagrams }, ... }对比两种 URL 形态场景路径形态路径含义单元级讨论默认/discussions/{course_key}/topics/{topic_id}/直达某个具体话题小节级聚合开启后/discussions/{course_key}/category/{category_name}/进入某小节category聚合视图4.3 配置的实际落点从源码看该设置最终存放在DiscussionsConfiguration的plugin_configurationJSON 字段中。在 serializers.py 中可以看到group_at_subsection: instance.plugin_configuration.get(group_at_subsection, False)即读取配置时以False为默认值与 ADR 中默认禁用的描述一致。这也与 ADR 0004 的设计相呼应provider 级配置如edx-next键下的discussions_group_at_subsection整体随课程结构存储与导出。五、源码级实现DiscussionsTopicLinkTransformer 如何注入字段5.1 Transformer 概览字段注入的核心实现在 transformers.py 的DiscussionsTopicLinkTransformer中。它继承自BlockStructureTransformer在课程块结构构建阶段向每个已链接讨论的块注入附加字段class DiscussionsTopicLinkTransformer(BlockStructureTransformer): WRITE_VERSION 1 READ_VERSION 1 EXTERNAL_ID discussions_id EMBED_URL discussions_url classmethod def name(cls): return discussions_link值得注意的细节ADR 文档中 API 层字段名为discussions_embed_url而 Transformer 实际注入的 xblock 字段为discussions_urlEMBED_URL常量二者是设计文档与实现之间的命名演变前端请求的公开字段以 Course Blocks API 暴露为准。5.2 transform 的过滤与注入逻辑transform方法的核心逻辑分三步确定 provider通过DiscussionsConfiguration.get(usage_info.course_key).provider_type取当前课程激活的讨论 provider查询话题映射从DiscussionTopicLink中过滤context_key课程、provider_idprovider_type且enabled_in_contextTrue的链接覆写块字段对每个链接向对应usage_key的块注入discussions_id外部话题 id与discussions_urlMFE 嵌入链接。topic_links DiscussionTopicLink.objects.filter( context_keyusage_info.course_key, provider_idprovider_type, enabled_in_contextTrue, ) for topic_link in topic_links: block_structure.override_xblock_field( topic_link.usage_key, DiscussionsTopicLinkTransformer.EXTERNAL_ID, topic_link.external_id, ) mfe_embed_link get_discussions_mfe_topic_url(usage_info.course_key, topic_link.external_id) if mfe_embed_link: block_structure.override_xblock_field( topic_link.usage_key, DiscussionsTopicLinkTransformer.EMBED_URL, mfe_embed_link, )两个实现要点enabled_in_contextTrue的过滤与 ADR 0004移除链接时仅禁用、不删除数据的设计一致——被禁用的链接仍在表中但不会进入前端只有当get_discussions_mfe_topic_url生成了有效链接时才覆写EMBED_URL天然保证了没有链接就不返回字段的边界行为。5.3 URL 生成与 DISCUSSIONS_MICROFRONTEND_URL链接生成的底层工具在 url_helpers.py。核心函数get_discussions_mfe_topic_url(course_key, topic_id, viewNone)拼接出/discussions/{course_key}/topics/{topic_id}路径并统一走_get_url_with_view_query_paramsdef _get_url_with_view_query_params(path: str, view: Optional[str] None) - str: if settings.DISCUSSIONS_MICROFRONTEND_URL is None: return url f{settings.DISCUSSIONS_MICROFRONTEND_URL}/{path} query_params {} if view in_context: query_params.update({inContext: True}) if query_params: url f{url}?{urlencode(query_params)} return url关键事实生成的链接以 Django 设置项DISCUSSIONS_MICROFRONTEND_URL为基址如开发环境中的http://localhost:2002该设置未配置时返回空串Transformer 因而跳过字段注入预留了inContextTrue查询参数通道当以viewin_context调用时链接会携带该参数供 MFE 在 iframe 内切换为上下文内讨论视图。当前 Transformer 调用get_discussions_mfe_topic_url时未传 view默认生成不带参数的链接。六、数据模型支撑DiscussionTopicLink 与 DiscussionsConfiguration6.1 DiscussionTopicLink链接的单一事实来源映射模型定义在 models.py约 558 行起实际字段比 ADR 0004 中的概念模型更完整字段类型说明context_keyLearningContextKeyField讨论所属的学习上下文课程键带索引usage_keyUsageKeyField关联的块 usage keynull表示课程级话题titleCharField(255)话题标题groupForeignKey(CourseUserGroup)分群讨论时所属用户组可空provider_idCharField(32)讨论 provider 标识external_idCharField(255)外部论坛服务中的话题 id如 cs_comments_service 的 commentable_id带索引enabled_in_contextBooleanField(defaultTrue)是否在课程中上下文内展示该话题orderingPositiveIntegerField(nullTrue)话题在上下文内的排序contextJSONField(defaultdict)附加上下文信息如所属 section、subsection其中usage_keyenabled_in_context正是 Transformer 查询所依赖的核心字段。6.2 DiscussionsConfiguration课程级讨论配置同一文件中的DiscussionsConfiguration模型为课程提供讨论配置其中与上下文内讨论直接相关的字段包括enable_in_context默认True为课程中每个非计分单元创建讨论话题并显示 UIenable_graded_units默认False是否也为计分单元创建话题unit_level_visibility默认True是否需要逐单元手动开启讨论plugin_configurationJSONFieldprovider 级插件配置discussions_group_at_subsection即存放于此provider_type当前讨论 provider id默认由get_default_provider_type()决定新结构讨论启用时返回openedx否则返回legacy。此外supports_in_context_discussions()方法用于判断当前 provider 是否支持上下文内讨论能力是前端/API 决定是否展示讨论 UI 的能力依据。七、测试验证行为如何被固化单元测试 tests/test_transformer.py 直接对应本文第三节描述的边界行为测试搭建了包含一个可讨论单元discussion_enabledTrue并创建对应DiscussionTopicLink与一个不可讨论单元discussion_enabledFalse的课程结构随后断言可讨论单元EMBED_URL字段值等于http://discussions-mfe/{course_id}/topics/test-topic-idEXTERNAL_ID等于test-topic-id不可讨论单元EMBED_URL与EXTERNAL_ID均为None。该测试同时验证了 URL 拼接格式/topics/{topic_id}与无链接不返回字段的约定可作为理解 ADR 0005 预期行为的可执行规范。八、端到端流程与 MFE 消费建议综合 ADR 与源码上下文内讨论从配置到前端展示的完整链路为配置管理员在课程中启用上下文内讨论并可选开启discussions_group_at_subsection存于DiscussionsConfiguration.plugin_configuration建链信号/任务依据配置在外部讨论服务创建话题并向DiscussionTopicLink写入usage_key ↔ external_id映射见 models.py 与 handlers.py注入learning MFE 请求 Course Blocks API 时DiscussionsTopicLinkTransformer在块结构上覆写discussions_url设计文档中的discussions_embed_url渲染MFE 检测到字段存在即以 iframe 将该 URL 嵌入单元侧边栏若启用小节聚合则改用/category/形态链接并呈现小节级视图。对前端开发者的落地建议以字段是否存在判断是否渲染讨论侧边栏无需请求额外接口根据 URL 路径形态/topics/vs/category/区分单元级与小节级两种讨论视图并同步读取配置接口中的group_at_subsection默认False以保持 UI 一致关注DISCUSSIONS_MICROFRONTEND_URL配置它决定了嵌入链接的基址部署环境不同该值亦不同。九、总结ADR 0005 的价值在于用最小改动打通了后端链接 → 前端展示的通路不新建专用 API而是复用 learning MFE 已在使用的 Course Blocks API通过 Transformer 注入讨论嵌入链接同时通过discussions_group_at_subsection在单元级与小节级两种讨论粒度之间提供可切换能力。从 transformers.py、url_helpers.py 与 models.py 的源码可以看出该设计始终保持映射表为单一事实来源、provider 无关、可平滑切换的架构原则为后续接入更多讨论 provider 留足了扩展空间。【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考