Wagtail StreamField 块如何编写自定义校验并只在发布时强制必填?
【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail
如果你在给 Wagtail 的 StreamField 写自定义块,会遇到两类常见诉求:一是块里有跨字段规则(比如"页面和 URL 必须填其一"),需要写自定义校验;二是希望某些必填字段只在发布时强制,草稿保存时允许留空,而另一些字段则始终必填。从 Wagtail 7.4 起,StreamField 块在保存页面草稿时默认采用延迟校验(deferred validation):必填约束在保存草稿时被跳过,在页面或 snippet 发布、计划发布或提交工作流时照常校验。本篇基于 StreamField validation 文档,讲清如何用clean方法写自定义校验、如何用required_on_save控制必填时机,以及如何让校验错误显示在具体的子块上。
校验机制:clean 方法与 is_deferred_validation
所有 StreamField 块都实现了clean方法:它接收块的值,返回清理后的值,或在值不合法时抛出ValidationError。内置规则(如 URLBlock 校验 URL 格式)也是通过这个方法实现的。对于 StructBlock 这类容器块,clean会递归调用子块的clean,并把子块的校验错误向上传递。
在保存草稿时执行校验的过程中,块实例的is_deferred_validation属性会被设置为True。这个属性就是"只在发布时强制"的实现依据——在clean里检查它,就能决定某条规则是否跳过。机制定义可在 wagtail/blocks/base.py 中的defer_required_validation和clean_deferred方法看到:顶层块的clean_deferred会先调用defer_required_validation()(置位is_deferred_validation = True)再调用clean,结束后恢复。
注意一个适用条件:延迟校验默认对"页面草稿,以及使用DraftStateMixin的 snippet"生效(见 docs/releases/7.4.md 中的 "Deferred validation for StreamField blocks when saving drafts")。这是 7.4 引入的默认行为,不需要额外配置。
第一步:用 is_deferred_validation 写"发布时才强制"的校验
官方文档给出的示例是一个 StructBlock,规则是:page或url在发布时至少填一个,但保存草稿时不要求;text则任何时候都必填。
from django.core.exceptions import ValidationError from wagtail.blocks import StructBlock, PageChooserBlock, URLBlock class LinkBlock(StructBlock): page = PageChooserBlock(required=False) url = URLBlock(required=False) text = CharBlock(required_on_save=True) def clean(self, value): result = super().clean(value) if not self.is_deferred_validation and not (result["page"] or result["url"]): raise ValidationError("Either page or URL must be specified") return result各部分的写法说明:
- 先调用
super().clean(value),让子块完成各自的标准校验,拿到清理后的result,再执行自己的跨字段规则; if not self.is_deferred_validation and ...是关键:is_deferred_validation为True(保存草稿)时整条规则被跳过,只有发布等场景下page和url都为空时才抛错;text = CharBlock(required_on_save=True)表示text在保存草稿时也要执行必填校验,与自定义规则形成对比。
required_on_save是字段块(FieldBlock)的选项,默认值为False(见 docs/reference/streamfield/blocks.md 中 Field block types 一节)。设为True后,该字段的必填约束在保存草稿时不会被告免。实现上,FieldBlock.defer_required_validation会把底层表单字段的required恢复为required_on_save的值(见 wagtail/blocks/field_block.py 第 66-69 行):普通必填字段在草稿态被置为required=False,而带required_on_save=True的字段保持必填。
第二步:控制错误信息渲染在哪个块上
默认情况下,在clean中抛出ValidationError会把错误挂在 StructBlock 整体上进行渲染。如果希望错误显示在具体的子块上,改用wagtail.blocks.StructBlockValidationError,其构造函数接受两个参数:
non_block_errors:挂在 StructBlock 整体上的错误消息列表或ValidationError实例列表;block_errors:以子块名称为键、ValidationError实例为值的字典,错误将显示在对应子块上。
例如把"描述必须包含关键词"的报错挂在description子块上:
from django.core.exceptions import ValidationError from wagtail.blocks import CharBlock, StructBlock, StructBlockValidationError, TextBlock class TopicBlock(StructBlock): keyword = CharBlock() description = TextBlock() def clean(self, value): result = super().clean(value) if result["keyword"] not in result["description"]: raise StructBlockValidationError( block_errors={ "description": ValidationError( "Description must contain the keyword" ) } ) return resultListBlock 和 StreamBlock 有对应的异常类wagtail.blocks.ListBlockValidationError和wagtail.blocks.StreamBlockValidationError,用法相同,区别是block_errors字典的键为块的数字索引。文档给出的示例是一个"数值必须升序"的 ListBlock:
from django.core.exceptions import ValidationError from wagtail.blocks import ListBlock, ListBlockValidationError class AscendingListBlock(ListBlock): # example usage: # price_list = AscendingListBlock(FloatBlock()) def clean(self, value): result = super().clean(value) errors = {} for i in range(1, len(result)): if result[i] < result[i - 1]: errors[i] = ValidationError("Values must be in ascending order") if errors: raise ListBlockValidationError(block_errors=errors) return result如何验证行为
可以在管理界面按场景验证:编辑一个含上述块的页面,保存草稿时,只填text而page/url留空应能正常保存,且text留空会立即报错;执行发布时,page/url均为空则显示 "Either page or URL must be specified"。
仓库测试代码 wagtail/tests/test_blocks.py 中的test_required_on_save也给出了直接的行为断言方式:blocks.CharBlock(required_on_save=True)上,无论是延迟校验路径block.clean_deferred("")还是普通路径block.clean(""),空值都会抛出ValidationError;而未设置该选项的CharBlock只会在clean("")时抛错,clean_deferred("")通过。如果你的块是嵌套在 StreamBlock 里使用的,is_deferred_validation由顶层块的clean_deferred统一置位并传播到子块,无需在子块上重复处理。
限制:模型级校验方法不会检查块
文档特别指出,StreamField 中块的校验发生在表单字段(wagtail.blocks.base.BlockField)上,而不是模型字段(wagtail.fields.StreamField)上。因此对页面实例调用my_page.full_clean()这类模型级校验方法,捕获不到 StreamField 数据中的非法块。这个限制只在 StreamField 数据绕过表单字段、通过程序化方式写入时才构成实际问题;只要数据经过管理界面的表单提交,校验都会正常执行。
小结
完整路径是:在块子类中重写clean并先调用super().clean(value);用is_deferred_validation决定哪些规则只在发布时强制;对需要草稿也必填的字段块设置required_on_save=True;用StructBlockValidationError/ListBlockValidationError/StreamBlockValidationError把错误定位到具体子块。机制入口分别是 docs/advanced_topics/streamfield_validation.md(本文依据的文档)和 wagtail/blocks/base.py 中的defer_required_validation/clean_deferred。
【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考