ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Wagtail 4.2 版本深度解析:StreamField 数据迁移、片段锁定与工作流、富文本与编辑器新体验

2026/9/14 19:40:03 拓冰建站 浏览量
Wagtail 4.2 版本深度解析:StreamField 数据迁移、片段锁定与工作流、富文本与编辑器新体验 Wagtail 4.2 版本深度解析StreamField 数据迁移、片段锁定与工作流、富文本与编辑器新体验【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtailWagtail 4.2发布于 2023 年 2 月 6 日是 Wagtail 在 Django 内容管理领域的一次重要功能更新核心亮点包括官方级 StreamField 数据迁移工具集、面向片段的锁定LockableMixin与工作流WorkflowMixin支持、全新的fullpageurl模板标签、基于 Stimulus 的前端重构CSP 兼容、内置于用户栏的无障碍检查器以及富文本编辑器与MultipleChooserPanel面板的大幅升级。本文以 docs/releases/4.2.md 发布说明为主体结合 docs/advanced_topics/streamfield_migrations.md、docs/topics/snippets/features.md 等配套文档与仓库源码为你完整拆解这些新特性的用法、底层实现与升级注意点。一、StreamField 数据迁移工具告别手写 JSON 转换为什么数据迁移如此必要StreamField 在数据库中只对应一个 JSON 文本列无论内部块结构如何变化Django 生成的 schema 迁移都不会感知到任何结构变化——数据库 schema 不改变改变的是 JSON 数据本身。因此一旦你修改了 StreamField 的块定义例如重命名一个块已有数据必须通过数据迁移改写为新结构否则旧数据会静默丢失。在 4.2 之前开发者通常需要手写RunPython迁移函数自行递归 JSON 数据。当遇到嵌套块、多字段、多版本revision时这一过程极易出错。4.2 为此引入了一套官方工具集中暴露在三个模块中wagtail.blocks.migrations.migrate_operation提供MigrateStreamData迁移操作wagtail.blocks.migrations.operations提供常用的内置数据操作重命名、删除、结构调整等wagtail.blocks.migrations.utils提供遍历、格式化等辅助工具该特性最初由 Sandil Ranasinghe 以 wagtail-streamfield-migration-toolkit含test_simple_structures.py、test_nested_structures.py、test_old_list.py、test_bad_data.py等。场景一将 RichTextField 迁移为 StreamField这是最典型的历史数据迁移场景RichTextField与StreamField底层都是文本列./manage.py makemigrations生成的AlterField不会报错但旧文本并非 StreamField 的 JSON 结构需要额外转换。完整示例见 docs/advanced_topics/streamfield_migrations.mdimport json from django.core.serializers.json import DjangoJSONEncoder from django.db import migrations import wagtail.blocks import wagtail.fields def convert_to_streamfield(apps, schema_editor): BlogPage apps.get_model(demo, BlogPage) for page in BlogPage.objects.all(): page.body json.dumps( [{type: rich_text, value: page.body}], clsDjangoJSONEncoder ) page.save() def convert_to_richtext(apps, schema_editor): BlogPage apps.get_model(demo, BlogPage) for page in BlogPage.objects.all(): if page.body: stream json.loads(page.body) page.body .join( [child[value] for child in stream if child[type] rich_text] ) page.save() class Migration(migrations.Migration): dependencies [ # 保留生成的依赖行 (demo, 0001_initial), ] operations [ migrations.RunPython( convert_to_streamfield, convert_to_richtext, ), # 保留生成的 AlterField 行 migrations.AlterField( model_nameBlogPage, namebody, fieldwagtail.fields.StreamField( [(rich_text, wagtail.blocks.RichTextBlock())], ), ), ]注意上述写法只处理已发布页面。若还要迁移草稿页与页面修订revisions原文档给出了更健壮的版本见 streamfield_migrations.md对页面先尝试json.loads判断是否已是合法 JSON对修订则通过revision_data.get(body)处理Revision中保存的字段快照未通过 JSON 解析的才包装成[{type: rich_text, value: body}]。场景二用MigrateStreamData完成块重命名等结构迁移假设blog.BlogPage有一个 StreamFieldclass BlogPage(Page): content StreamField( [ (stream1, blocks.StreamBlock([(field1, blocks.CharBlock())])), ] )现在将field1重命名为block1。先用 Django 生成空迁移python manage.py makemigrations --empty blog生成的文件形如# Generated by Django 4.0.3 on 2022-09-09 21:33 from django.db import migrations class Migration(migrations.Migration): dependencies [...] operations []关键前提该迁移或其依赖链上的迁移必须依赖 Wagtail 核心迁移例如(wagtailcore, 0069_log_entry_jsonfield)Wagtail 4 起0076_modellogentry_revision亦可因为工具需要Revision模型相关的迁移才能正常运行。然后填入MigrateStreamData完整示例见 streamfield_migrations.mdfrom django.db import migrations from wagtail.blocks.migrations.migrate_operation import MigrateStreamData from wagtail.blocks.migrations.operations import RenameStreamChildrenOperation class Migration(migrations.Migration): dependencies [...] operations [ MigrateStreamData( app_nameblog, model_nameBlogPage, field_namecontent, operations_and_block_paths[ ( RenameStreamChildrenOperation(old_namefield1, new_nameblock1), stream1, ), ], ), ]MigrateStreamData会替你处理取出数据 → 递归遍历 → 应用操作 → 写回数据含 revision的完整链路你只需要描述改哪里、怎么改。块路径block path规则operations_and_block_paths是(operation, block_path)元组列表每个操作会应用到所有匹配该路径的块operations_and_block_paths [(operation1, block_path1), (operation2, block_path2), ...]块路径是从顶层StreamBlockStreamField 中所有块的容器到目标嵌套块的、以.分隔的类型名序列要操作顶层 StreamBlock 本身时路径为空字符串匹配顶层field1块 → 路径field1匹配嵌套在nested1下的deepnested1块 → 路径nested1.deepnested1当路径穿过 ListBlock 时必须使用item作为该子节点名例如匹配list1内 StructBlock 的char1→ 路径list1.item.char1。内置操作一览重命名与删除操作注意它们作用于被操作块的父块值因此块路径要指向父块RenameStreamChildrenOperation(old_name, new_name)RenameStructChildrenOperation(old_name, new_name)RemoveStreamChildrenOperation(name)RemoveStructChildrenOperation(name)结构调整操作StreamChildrenToListBlockOperation(block_name)把StreamBlock中指定类型的所有子块合并进一个 ListBlockStreamChildrenToStreamBlockOperation(block_names)block_names是列表把多个类型的子块合并进一个 StreamBlockStreamChildrenToStructBlockOperation(block_name)为每个指定类型的子块新建 StructBlock 并移入例如把顶层char1包进struct1mystream StreamField([(char1, CharBlock()) ...], ...)StreamChildrenToStructBlockOperation(char1, struct1)数据会从{type: char1, value: Value1}变为{type: struct1, value: {char1: Value1}}。需要注意这类结构变化不会保留原块 id新块在结构上已不同。其他操作AlterBlockValueOperation等。编写自定义操作继承wagtail.blocks.migrations.operations.BaseBlockOperation实现两个成员即可该类继承abc.ABC二者必填apply(block_value)对块值做实际修改并返回新值operation_name_fragmentproperty用于生成迁移名官方示例是一个把 CharBlock 字符串截断到指定长度的操作from wagtail.blocks.migrations.operations import BaseBlockOperation class MyBlockOperation(BaseBlockOperation): def __init__(self, length): super().__init__() self.length length # 需要在操作上保存参数 def apply(self, block_value): # block_value 即 CharBlock 的字符串值 return block_value[: self.length] property def operation_name_fragment(self): return truncate_{}.format(self.length)block_value的结构因块类型而异非结构块如 CharBlock直接传值字符串等StreamBlock[{type: ..., value: ..., id: ...}, ...]StructBlock{type1: ..., type2: ..., ...}ListBlock[{type: item, value: ..., id: ...}, ...]做结构性修改例如改块类型时由于apply只改变块的 value通常需要操作父块的值可参考RenameStreamChildrenOperation的实现。旧版 ListBlock 数据兼容Wagtail 2.16 之前 ListBlock 子项以普通 Python 列表保存2.16 之后以带type/id的ListValue保存。处理旧数据时可用wagtail.blocks.migrations.utils.formatted_list_child_generator统一取到新格式的子项。二、片段Snippet锁定LockableMixin从 4.2 起片段模型可以继承 LockableMixin从而获得与页面一致的锁定能力锁定后其他用户无法编辑编辑界面会在状态侧栏展示锁定信息有权限的用户可看到锁定/解锁按钮。# ... from wagtail.models import LockableMixin # ... class Advert(LockableMixin, models.Model): url models.URLField(nullTrue, blankTrue) text models.CharField(max_length255) panels [ FieldPanel(url), FieldPanel(text), ]从源码看LockableMixin提供三个字段locking.pylockedBooleanField默认 False不可编辑locked_atDateTimeField可空locked_byFK 到AUTH_USER_MODEL删除用户时置空related_namelocked_%(class)ss同时它内置一个系统检查wagtailcore.E005locking.py强制LockableMixin必须位于RevisionMixin之前MRO 中靠前它还重写了with_content_json确保回退修订时保留locked/locked_at/locked_by三个对象级字段而不是被修订快照覆盖locking.py。使用要点混用多个 mixin 时顺序应为class MyModel(DraftStateMixin, LockableMixin, RevisionMixin)LockableMixin 在其他 mixin 之后、RevisionMixin 之前与定时发布scheduled publishing配合时Wagtail 会自动锁定已排定发布的实例与页面一致加锁用户本人仍可编辑除非设置WAGTAILADMIN_GLOBAL_EDIT_LOCK True见 docs/reference/settings.md锁定/解锁分别需要模型的lock与unlock权限应用 mixin 后 Wagtail 会自动创建对应权限并在后台组页面展示新增字段需要运行./manage.py makemigrations与./manage.py migrate。三、片段工作流WorkflowMixinWorkflowMixin让片段模型可以分配工作流编辑保存后可提交审核审核通过再发布状态侧栏会展示当前工作流信息编辑器会出现提交审核等操作菜单。官方示例features.md# ... from wagtail.models import DraftStateMixin, LockableMixin, RevisionMixin, WorkflowMixin # ... class Advert( WorkflowMixin, DraftStateMixin, LockableMixin, RevisionMixin, models.Model ): # ...使用要点WorkflowMixin必须与RevisionMixin、DraftStateMixin同时使用继承顺序为WorkflowMixin, DraftStateMixin, RevisionMixin系统检查wagtailcore.E006会强制校验见 wagtail/models/workflows.py推荐同时启用LockableMixin使工作流中的实例可被锁定、仅审核者可编辑mixin 提供workflow_states属性返回该实例的所有工作流状态查询集并自带一个默认GenericRelation_workflow_states基于base_content_typeobject_id用于实例删除时自动清理工作流状态workflows.py默认的GenericRelation没有related_query_name无法从WorkflowState反查片段如需此能力可自定义带related_query_name的GenericRelation。为支持片段工作流Workflow/Task/WorkflowState/TaskState模型均做了泛化改造详见下文升级注意事项。四、fullpageurl模板标签一行输出带域名的完整 URL4.2 新增fullpageurl模板标签Django 模板与 Jinja2 均可用用于输出页面的绝对完整 URL含协议与域名非常适合社交分享标签Open Graph等需要绝对地址的场景。Django 模板用法docs/topics/writing_templates.md{% load wagtailcore_tags %} ... meta propertyog:url content{% fullpageurl page %} /Jinja2 中同样可用fullpageurl通过jinja2.pass_context注入为全局函数见 wagtail/jinja2tags.py。与pageurl不同pageurl在同站点内输出相对路径/foo/bar/、跨站点才输出绝对地址而fullpageurl始终输出如https://example.com/foo/bar/的绝对地址。二者都支持fallback关键字参数当传入的 page 为None时返回命名 URLview name解析出的地址——fullpageurl还会把以/开头的 fallback 相对路径用request.build_absolute_uri补全为绝对地址源码见 wagtail/templatetags/wagtailcore_tags.py。底层实现调用page.get_full_url(request...)这要求模板上下文中有request。对应的测试覆盖了基本输出、命名 URL fallback、绝对 fallback 与非法 page 参数四种情况见 wagtail/tests/tests.py。五、富文本编辑器体验再升级4.0 引入的富文本 UI 改进见 docs/releases/4.0.md包括行内工具栏、/命令面板、字符计数、粘贴自动创建链接、RTL 支持、焦点感知占位符、空标题高亮、插入时拆分 StreamField 块等在 4.2 中得到进一步打磨用户可在**行内浮动工具栏与编辑器顶部的固定工具栏**之间二选一两者均展示全部格式化选项/命令面板与块选择器现在包含除文本样式外的所有格式化选项命令面板与块选择器在光标任意位置均可用可随时插入内容、转换已有内容甚至能在段落中间拆分 StreamField 块块选择器改为双列展示减少滚动即可看到更多选项。六、MultipleChooserPanel批量选择关联对象的 InlinePanel 变体对于在页面中挂载大量图片/文档/链接这类场景传统InlinePanel需要逐条添加子表单体验较差。4.2 新增MultipleChooserPanel由 YouGov 赞助开发点击添加后直接打开 chooser 弹窗可一次多选对象返回编辑页时自动生成对应数量的子面板并预填。面板定义docs/reference/panels.mdclass BlogPageGalleryImage(Orderable): page ParentalKey(BlogPage, on_deletemodels.CASCADE, related_namegallery_images) image models.ForeignKey( wagtailimages.Image, on_deletemodels.CASCADE, related_name ) caption models.CharField(blankTrue, max_length250) panels [ FieldPanel(image), FieldPanel(caption), ]在BlogPage上MultipleChooserPanel( gallery_images, labelGallery images, chooser_field_nameimage )必填参数chooser_field_name指定 chooser 所关联的ForeignKey字段名该面板是InlinePanel的子类wagtail/admin/panels/multiple_chooser_panel.py要求内联模型包含指向实现了 Wagtail chooser 接口的模型的外键——图片、文档、片段、页面均满足其他模型可通过注册自定义ChooserViewSet接入未传chooser_field_name会抛出ImproperlyConfigured从源码看其BoundPanel通过 telepath 打包 chooser widget 定义js_context.pack前端模板为wagtailadmin/panels/multiple_chooser_panel.html示例模型位于 wagtail/test/testapp/models.py测试见 wagtail/admin/tests/test_edit_handlers.py。七、无障碍检查器作者自助修复可访问性问题4.2 在页面**用户栏userbar**中集成了一个基于 Axe 测试引擎的无障碍检查器扫描当前加载页面并展示错误结果帮助内容作者在发布前自查并修复无障碍问题对标 ATAG 2.0 指南。本版本默认开启三条规则。它与其他用户栏项目一样可通过construct_wagtail_userbarhook 配置docs/reference/hooks.md。例如移除该项from wagtail.admin.userbar import AccessibilityItem hooks.register(construct_wagtail_userbar) def remove_userbar_accessibility_checks(request, items): items[:] [item for item in items if not isinstance(item, AccessibilityItem)]源码层面AccessibilityItem继承自内容检查基类wagtail/admin/userbar.py其配置通过get_axe_configuration/get_axe_context/get_axe_options等生成模板相关测试见 wagtail/admin/tests/test_userbar.py。同一版本还把用户栏整体改造成了自定义元素Web Component通过 shadow DOM 避免与宿主页面样式冲突见下文。八、CSP 兼容与 Stimulus 采用4.2 开始使用 Stimulus 框架驱动管理后台的客户端交互源自 RFC 78并重构了以下后台组件全站 Skip Link 组件仪表盘的升级通知消息列表过滤器的自动提交Wagtail 图标精灵icon sprite的加载页面锁定/解锁操作工作流启用操作这既提升了代码可维护性也推动后台逐步兼容严格的 CSPContent Security Policy规则。前端重构的配套动作还包括将initButtonSelects、initSkipLink、initTooltips、initTagField、initDismissibles等从core.js迁移为独立 TypeScript 文件并补充 JSDoc 与单元测试以及用URLSearchParams取代手写的 URL 查询参数解析函数详见 client/src/ 下的控制器与组件目录。九、其他值得关注的新功能测试断言增强WagtailPageTestCase.assertCanCreate支持publishTrue关键字参数用于决定是否发布页面见 docs/reference/testing.md 中的 testing referencepurge_embeds管理命令一键删除数据库中所有缓存的 embed 对象。官方建议在修改 embed 设置或 embed 服务商变更策略后运行避免后续使用读到旧缓存见 docs/reference/management_commands.md后台侧栏可调整大小页面编辑器的侧面板现在支持拖拽调节宽度FormPage支持将form_fields作为 APIField 暴露方便在 API 响应中返回表单字段定义页面 slug 重复时的校验错误信息更明确后台界面与程序化建页均适用DraftStateMixin片段自动获得 Publish 权限类型片段以草稿保存后停留在编辑页基础项目模板自动从 search description 字段填充 meta description 标签rebuild_references_index命令支持--verbosity 0静默运行前端缓存失效器新增对azure-mgmt-cdn≥ 10 与azure-mgmt-frontdoor≥ 1 的支持新增系统检查当django-storages后端被配置为允许覆盖写时给出警告ListBlock 版本对比比较页面版本时按条目展示差异管理后台设计系统补齐带图标的次要按钮button bicolor button--icon button-secondary及button-small变体。十、升级注意事项Upgrade considerations升级到 4.2 前请逐项核对以下破坏性变更完整清单见 docs/releases/4.2.md1. 图片字段切换为WagtailImageFieldAbstractImage与AbstractRendition改用 Wagtail 专有的WagtailImageField它继承 DjangoImageField但用 Willow。若你使用了自定义图片模型会生成一条新迁移。2.InlinePanel内不再支持评论评论系统早期错误地放开了InlinePanel字段的评论支持导致保存时评论丢失或挂到错误的条目上。4.2 起移除了该能力已有评论不再显示。官方通过 issue #9685 跟踪后续是否正式恢复。3. 模板标签/包含统一使用classname命名以下模板标签与 include 的参数名做了统一旧名会失效需手动更新名称新写法classname旧写法icon注意{% icon namespinner classname... %}{% icon namespinner class_name... %}dialog_toggle{% dialog_toggle classname... %}{% dialog_toggle class_name... %}paginate{% paginate pages classname... %}{% paginate pages classnames... %}tab_nav_link{% include wagtailadmin/shared/tabs/tab_nav_link.html with classname... %}{% include ... with classes... %}side_panel_button{% include wagtailadmin/shared/side_panels/includes/side_panel_button.html with classname... %}{% include ... with classes... %}其中icon标签仍兼容class_name带弃用警告未来版本会移除。4.InlinePanel的 JavaScript 函数改为类内部未文档化的InlinePanel(...)JS 函数被转换为类调用处需改为new InlinePanel(...)子表单控件改为自动初始化不再需要手动调用initChildControls、updateChildCount、updateMoveButtonDisabledStates、updateAddButtonState。Python 侧的InlinePanel面板类型不受影响。5. 设置项更名WAGTAILADMIN_GLOBAL_PAGE_EDIT_LOCK更名为 WAGTAILADMIN_GLOBAL_EDIT_LOCK用于控制用户能否编辑自己锁定的页面与片段。6. 用户栏改为 Web Componentwagtailuserbar模板标签现在把用户栏初始化为自定义元素wagtail-userbar使用 shadow DOM 避免样式冲突。自定义用户栏位置时请把样式目标从.wagtail-userbar改为wagtail-userbar::part(userbar) { bottom: 30px; }7.Workflow/Task方法签名变化为支持片段工作流Workflow.start()的page参数更名为obj。若你实现了自定义 Task 类型需要同步更新page_locked_for_user()更名为Task.locked_for_user()旧名弃用未来移除user_can_access_editor()、locked_for_user()、user_can_lock()、user_can_unlock()、get_actions()中的page参数更名为obj。8.WorkflowState/TaskState模型结构调整WorkflowState.page外键被替换为GenericForeignKeyWorkflowState.content_object底层由新增的WorkflowState.base_content_type与WorkflowState.object_id字段组合定义TaskState.page_revision外键更名为TaskState.revision。9.SearchForm校验逻辑变化wagtail.admin.forms.search.SearchForm内部类不再把空搜索词视为非法。任何依赖form.is_valid()判断是否对 queryset 应用search()过滤的扩展代码需要改为显式检查form.cleaned_data[q]是否非空。十一、Bug 修复与维护亮点4.2 修复了大量问题按主题归类有助于理解本次质量改进的重点无障碍/高对比模式工作流时间线图标、认证表单边框、按钮与链接一致性、logo、标签字段、tooltip、用户栏菜单在 Windows 高对比模式下均得到修复帮助文本与 skip link 的链接对比度达标焦点轮廓色提高对比度富文本编辑器行内工具栏水平定位、占位符颜色、字段轮廓遮挡、折叠文本域多余的水平拖拽手柄等问题修复/命令面板与块选择器按钮与虚线引导线居中对齐StreamField / 块DecimalBlock正确处理requiredFalse时的None值块选择器不再重复添加块4.2.1 中进一步修复ChooserBlock.extract_references改用模型类而非字符串工作流取消工作流后编辑表单立即显示为解锁状态4.2.1 修复了被组外用户锁定时无法推进GroupApprovalTask的问题性能与稳定性latest_revision指针不再随翻译复制被拷贝批量清理大量修订时防止内存耗尽parse_query_string支持从字符串解析多个 key/value 对4.2.1 修复图片上传到要求文件指针位于文件开头的存储后端时的失败以及 SQLite FTS 搜索中无关模型匹配泄漏的问题。维护方面除前文提到的 TypeScript 迁移与 Stimulus 采用外还包含wagtail.admin.panels拆分为子模块原有导出保留、从通用视图提取修订/草稿状态与锁定/解锁 mixin、为wagtail.core等 3.0 弃用导入补充弃用警告、wagtail.schedule.cancel补齐日志4.2.1等。升级到 4.3 或更高版本前若你的项目依赖上述内部 API建议先对照 docs/releases/ 下后续版本的发布说明确认移除进度。结语Wagtail 4.2 是一次编辑体验与数据安全并重的版本StreamField 数据迁移工具把最易出错的历史数据改造工作标准化片段获得锁定、工作流与草稿发布能力标志着 Wagtail 的内容治理模型从页面全面扩展到片段富文本与 chooser 面板的改进直接提升内容作者的日常操作效率而 Stimulus 重构与无障碍检查器则体现了项目在长期可维护性与可访问性上的持续投入。对于正在使用 4.0/4.1 或更早版本的项目本文升级注意事项一节中的每一项都值得在升级前逐一验证。【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考