Wagtail Snippets 实战指南:注册、渲染、可选功能 Mixin 与后台视图定制
2026/9/14 8:08:02 网站建设 项目流程

Wagtail Snippets 实战指南:注册、渲染、可选功能 Mixin 与后台视图定制

【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail

本文基于当前仓库的 Snippets 官方文档(docs/topics/snippets/index.md)及其关联章节展开,系统讲解 Wagtail Snippets 的核心概念、两种注册方式、前端渲染方案,以及如何通过PreviewableMixinRevisionMixinDraftStateMixinLockableMixinWorkflowMixin等 Mixin 按需为 snippet 增加页面级能力,并通过SnippetViewSet定制后台管理视图。读完本文,你可以完整掌握从定义一个 snippet 模型到为其配置预览、版本历史、草稿发布、锁定与审核工作流的全套实现路径,并能对照仓库源码验证其底层机制。

什么是 Snippets

Snippets 是"不需要完整网页来渲染"的内容片段,典型用途是让页头、页脚、侧边栏等次级内容(secondary content)在 Wagtail 后台中可编辑。从 docs/topics/snippets/index.md 的定义看,snippets 具备两个关键特征:

  1. 它们是普通的 Django 模型,不继承wagtail.models.Page,因此不会出现在 Wagtail 的页面树(tree)结构中;
  2. 要使其可编辑,需要为其配置panels(编辑面板),并通过register_snippet装饰器或函数将其声明为 snippet。

文档同时提醒了一个重要的取舍:默认情况下 snippets 缺少页面(Page)的许多特性,例如在后台排序、拥有定义好的 URL 等。在选择把某个内容类型建成 snippet 还是页面之前,需要谨慎评估其更适合哪种形态。

注册 Snippet:装饰器与函数两种方式

本章内容对应 docs/topics/snippets/registering.md。Wagtail 推荐将register_snippet作为函数使用(通常放在wagtail_hooks.py中),装饰器形式则出于便利和向后兼容保留。

方式一:@register_snippet装饰器

最简模型示例如下:

from django.db import models from wagtail.admin.panels import FieldPanel from wagtail.snippets.models import register_snippet # ... @register_snippet class Advert(models.Model): url = models.URLField(null=True, blank=True) text = models.CharField(max_length=255) panels = [ FieldPanel("url"), FieldPanel("text"), ] def __str__(self): return self.text

要点说明:

  • Advert使用基础 Django 模型类,编辑界面接近Page子类模型:字段由panels(或edit_handler)属性分配;
  • 除非进一步配置,snippet 不使用多标签页(tabs)字段分组,也不提供"保存为草稿"或"提交审核"功能;
  • panels列表决定编辑页显示哪些字段;务必通过def __str__(self):提供字符串表示,否则 snippet 在后台列表中无法辨识。

方式二:在wagtail_hooks.py中以函数调用(推荐)

# myapp/wagtail_hooks.py from wagtail.snippets.models import register_snippet from myapp.models import Advert register_snippet(Advert)

以函数方式注册的最大好处是:后续可以为该 snippet 挂上自定义的SnippetViewSet子类,实现 Django 模型与 Wagtail 专属逻辑(如panelsedit_handler)的解耦——这些定义从模型类上移到SnippetViewSet类上:

# myapp/wagtail_hooks.py from wagtail.admin.panels import FieldPanel from wagtail.snippets.models import register_snippet from wagtail.snippets.views.snippets import SnippetViewSet from myapp.models import Advert class AdvertViewSet(SnippetViewSet): model = Advert panels = [ FieldPanel("url"), FieldPanel("text"), ] # 用函数式注册传入自定义 SnippetViewSet 子类, # 而不是在模型类上直接使用 @register_snippet 装饰器 register_snippet(AdvertViewSet)

若需要更复杂的编辑面板定制,可覆写SnippetViewSet.get_edit_handler方法。

源码视角:注册是如何生效的

从 wagtail/snippets/models.py 的源码结构看,注册机制有一个值得了解的细节——延迟注册(deferred registration)

  • 模块级列表SNIPPET_MODELS存放所有已注册的 snippet 模型;
  • register_snippet经常在模型尚未全部加载前被调用(wagtail_hooks.py的导入时机),直接构建 viewset 可能出问题,因此源码设置DEFER_REGISTRATION = True,先把(registerable, viewset)记入DEFERRED_REGISTRATIONS
  • WagtailSnippetsAppConfig.ready()确认所有模型加载完毕后,调用register_deferred_snippets()逐条处理;
  • _register_snippet_immediately会把模型类解析为 viewset 实例(模型类会自动包装成viewset(model=registerable)),最终通过viewsets.register(registerable)注册到全局 viewset 注册表。

另外,get_snippet_models()在返回前会调用search_for_hooks(),这正是"函数式注册必须写在wagtail_hooks.py中"的底层原因。权限层面,create_extra_permissions 会根据 Mixin 继承情况为 snippet 自动创建额外权限:继承DraftStateMixin的模型自动获得publish权限,继承LockableMixin的模型自动获得lockunlock权限,这些权限随后显示在后台"Groups"区域供分配。

渲染 Snippet:模板标签与页面绑定

本章内容对应 docs/topics/snippets/rendering.md。作为 Django 模型,snippets 有两条主要渲染路径:通过自定义模板标签,或纳入 Wagtail 页面的渲染流程。

用自定义模板标签输出 snippet 列表

最简做法是写一个 Django 自定义模板标签。在应用内新建templatetags目录下的 Python 文件,例如myproject/demo/templatetags/demo_tags.py

from django import template from demo.models import Advert register = template.Library() # ... # Advert snippets @register.inclusion_tag("demo/tags/adverts.html", takes_context=True) def adverts(context): return { "adverts": Advert.objects.all(), "request": context["request"], }

@register.inclusion_tag()接收两个参数:要渲染的模板,以及是否把 request 上下文传入模板(布尔值)。文档特别建议takes_context=True,因为一些 Wagtail 专属模板标签(如pageurl)依赖 request 上下文才能正常工作。

该 inclusion tag 对应的模板demo/tags/adverts.html

{% for advert in adverts %} <p> <a href="{{ advert.url }}"> {{ advert.text }} </a> </p> {% endfor %}

然后在任意页面模板中加载并使用:

{% load wagtailcore_tags demo_tags %} ... {% block content %} ... {% adverts %} {% endblock %}

模板标签函数也可以接收参数来过滤出特定实例,示例中为简洁起见直接返回Advert.objects.all()

把页面绑定到具体 snippet 实例

上面的模板标签输出的是"固定列表",适合侧边栏这类通用面板。若要在某页面上展示特定snippet 实例,可以在页面模型上定义指向 snippet 模型的外键,并在content_panels中加入对应的FieldPanel。例如让BookPage展示一条指定广告:

# ... class BookPage(Page): advert = models.ForeignKey( "demo.Advert", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) content_panels = Page.content_panels + [ FieldPanel("advert"), # ... ]

此时在模板中即可通过page.advert访问该 snippet。

若要为页面挂载多个snippet 实例,可以把FieldPanel放到BookPage的 inline 子对象上,而不是BookPage本身。每放置一次广告就对应一个子对象,文档称之为BookPageAdvertPlacement

from django.db import models from wagtail.models import Page, Orderable from modelcluster.fields import ParentalKey # ... class BookPageAdvertPlacement(Orderable, models.Model): page = ParentalKey( "demo.BookPage", on_delete=models.CASCADE, related_name="advert_placements" ) advert = models.ForeignKey( "demo.Advert", on_delete=models.CASCADE, related_name="+" ) class Meta(Orderable.Meta): verbose_name = "advert placement" verbose_name_plural = "advert placements" panels = [ FieldPanel("advert"), ] def __str__(self): return self.page.title + " -> " + self.advert.text class BookPage(Page): # ... content_panels = Page.content_panels + [ InlinePanel("advert_placements"), # ... ]

这些子对象可通过页面的advert_placements属性访问,再从中取关联的AdvertBookPage的模板中可以这样渲染:

{% for advert_placement in page.advert_placements.all %} <p> <a href="{{ advert_placement.advert.url }}"> {{ advert_placement.advert.text }} </a> </p> {% endfor %}

可选功能:按 Mixin 增量解锁页面级能力

本章内容对应 docs/topics/snippets/features.md。默认 snippet 缺少预览、版本(revisions)、工作流等页面特性;这些特性可以逐项通过继承对应的 Mixin 类为每个 snippet 模型添加。

让 Snippet 可预览(PreviewableMixin)

若 snippet 模型继承wagtail.models.PreviewableMixin,Wagtail 会在编辑器中自动加入实时预览面板。继承 Mixin 之外,模型还必须覆写get_preview_templateserve_preview

# ... from wagtail.models import PreviewableMixin # ... class Advert(PreviewableMixin, models.Model): url = models.URLField(null=True, blank=True) text = models.CharField(max_length=255) panels = [ FieldPanel("url"), FieldPanel("text"), ] def get_preview_template(self, request, mode_name): return "demo/previews/advert.html"

配套预览模板demo/previews/advert.html

<!DOCTYPE html> <html> <head> <title>{{ object.text }}</title> </head> <body> <a href="{{ object.url }}">{{ object.text }}</a> </body> </html>

默认上下文变量为request(一个伪造的django.http.HttpRequest对象)和object(snippet 实例);要自定义上下文可覆写get_preview_contextserve_preview默认返回使用上述 request、模板与上下文渲染出的TemplateResponse,可覆写以定制渲染与路由逻辑。

类似页面,覆写preview_modes属性可定义多个预览模式。例如带一个"Alternate"模式、不同模式使用不同模板与上下文的Advert

# ... from wagtail.models import PreviewableMixin # ... class Advert(PreviewableMixin, models.Model): url = models.URLField(null=True, blank=True) text = models.CharField(max_length=255) panels = [ FieldPanel("url"), FieldPanel("text"), ] @property def preview_modes(self): return PreviewableMixin.DEFAULT_PREVIEW_MODES + [("alt", "Alternate")] def get_preview_template(self, request, mode_name): templates = { "": "demo/previews/advert.html", # Default preview mode "alt": "demo/previews/advert_alt.html", # Alternate preview mode } return templates.get(mode_name, templates[""]) def get_preview_context(self, request, mode_name): context = super().get_preview_context(request, mode_name) if mode_name == "alt": context["extra_context"] = "Alternate preview mode" return context

让 Snippet 可搜索(index.Indexed)

若 snippet 模型继承wagtail.search.index.Indexed,Wagtail 会自动为该 snippet 类型的 chooser(选择器)界面加入搜索框:

# ... from wagtail.search import index # ... class Advert(index.Indexed, models.Model): url = models.URLField(null=True, blank=True) text = models.CharField(max_length=255) panels = [ FieldPanel("url"), FieldPanel("text"), ] search_fields = [ index.SearchField("text"), index.AutocompleteField("text"), ]

为 Snippet 保存版本(RevisionMixin)

若 snippet 模型继承wagtail.models.RevisionMixin,在 snippets 后台保存任何变更时都会自动保存版本。Mixin 提供:

  • revisions属性:返回该实例所有版本的 queryset;
  • wagtail.models.Revision模型的默认GenericRelation:确保 snippet 实例被删除时版本数据一并清理。

注意:默认GenericRelation未设置related_query_name,因此无法从Revision反向查询、过滤回 snippet 模型。如需要该能力,可自定义带related_query_nameGenericRelation(见源码中的RevisionMixin._revisions默认关系与revisions属性)。

Advert启用版本控制的写法:

# ... from django.contrib.contenttypes.fields import GenericRelation from wagtail.models import RevisionMixin # ... class Advert(RevisionMixin, models.Model): url = models.URLField(null=True, blank=True) text = models.CharField(max_length=255) # 若无自定义逻辑,可直接将该字段命名为 `revisions` _revisions = GenericRelation("wagtailcore.Revision", related_query_name="advert") panels = [ FieldPanel("url"), FieldPanel("text"), ] @property def revisions(self): # 如有必要可写自定义逻辑,例如处理多表继承。 # Mixin 本身已处理继承,因此这里是可选的。 return self._revisions.all()

一个容易踩坑的点:如果 snippet 模型使用了 Django 的ManyToManyField,需要把基类从django.db.models.Model改为modelcluster.models.ClusterableModel,并把ManyToManyField换成ParentalManyToManyField;inline 模型继续使用ParentalKey而非ForeignKey。这样才能让关系数据被存进版本里。文档给出一个带分类与图片的Shirt示例:

# ... from django.db import models from modelcluster.fields import ParentalKey, ParentalManyToManyField from modelcluster.models import ClusterableModel from wagtail.models import RevisionMixin # ... class ShirtColour(models.Model): name = models.CharField(max_length=255) panels = [FieldPanel("name")] class ShirtCategory(models.Model): name = models.CharField(max_length=255) panels = [FieldPanel("name")] class Shirt(RevisionMixin, ClusterableModel): name = models.CharField(max_length=255) colour = models.ForeignKey( "shirts.ShirtColour", on_delete=models.SET_NULL, blank=True, null=True ) categories = ParentalManyToManyField("shirts.ShirtCategory", blank=True) revisions = GenericRelation("wagtailcore.Revision", related_query_name="shirt") panels = [ FieldPanel("name"), FieldPanel("colour"), FieldPanel("categories", widget=forms.CheckboxSelectMultiple), InlinePanel("images"), ] class ShirtImage(models.Model): shirt = ParentalKey("shirts.Shirt", related_name="images") image = models.ForeignKey( "wagtailimages.Image", on_delete=models.CASCADE, related_name="+" ) caption = models.CharField(max_length=255, blank=True) panels = [ FieldPanel("image"), FieldPanel("caption"), ]

RevisionMixin包含latest_revision字段,需要落库——改动后务必执行makemigrationsmigrate。启用 Mixin 后,snippets 后台的任何变更都会创建一个包含 snippet 状态快照的Revision实例,并挂接到编辑操作的 audit log 条目上,可在 snippet 历史页面回滚到旧版本或对比两个版本的差异。也可以在程序中调用save_revision()保存版本;启用 Mixin 后建议对每个已存在的实例至少保存一次版本,以填充数据库中的latest_revision字段。

保存草稿变更(DraftStateMixin)

若 snippet 模型继承wagtail.models.DraftStateMixin,Wagtail 会:在列表视图自动加入 live/draft 状态列;把编辑器的"Save"动作菜单改为"Save draft";并新增"Publish"动作菜单。此后在后台保存的变更都会作为版本保存,直到发布才反映到"live"实例上。

由于DraftStateMixin依靠版本机制保存草稿,因此必须同时继承RevisionMixin。另外,若面板定义中包含PublishingPanel,还可以为模型实例设置定时发布(scheduled publishing)。

Advert同时具备草稿与定时发布的定义:

# ... from django.contrib.contenttypes.fields import GenericRelation from wagtail.admin.panels import PublishingPanel from wagtail.models import DraftStateMixin, RevisionMixin # ... class Advert(DraftStateMixin, RevisionMixin, models.Model): url = models.URLField(null=True, blank=True) text = models.CharField(max_length=255) _revisions = GenericRelation("wagtailcore.Revision", related_query_name="advert") panels = [ FieldPanel("url"), FieldPanel("text"), PublishingPanel(), ] @property def revisions(self): return self._revisions

关键操作与注意事项:

  • DraftStateMixin引入的新字段同样需要makemigrations+migrate落库;
  • 可在程序中通过instance.publish(revision)revision.publish()发布版本;启用 Mixin 后,建议为每个已存在实例至少发布一次,以填充latest_revisionlive_revision字段;
  • 若使用定时发布,需定期运行publish_scheduled管理命令;
  • 发布 snippet 实例需要该模型上的publish权限。对应用了DraftStateMixin的模型,Wagtail 会自动创建对应权限并显示在后台"Groups"区域(对应源码 wagtail/snippets/models.py 中的create_extra_permissions)。

文档同时给出了一条重要警告:Wagtail 目前尚无机制阻止编辑者把未发布(draft)的 snippet 放进页面。若要把DraftStateMixin启用的 snippet 包含进页面,务必自行加入检查逻辑(例如检查其live字段),以决定草稿 snippet 的渲染方式。

锁定 Snippet(LockableMixin)

若 snippet 模型继承wagtail.models.LockableMixin,Wagtail 会为其实例增加锁定能力:编辑时"Status"侧边面板显示锁定信息,有权限的用户可见锁定/解锁按钮。若模型同时启用了定时发布,Wagtail 会自动锁定已排定发布的实例。类似页面,锁定者本人仍后可编辑,除非WAGTAILADMIN_GLOBAL_EDIT_LOCK设为True

# ... from wagtail.models import LockableMixin # ... class Advert(LockableMixin, models.Model): url = models.URLField(null=True, blank=True) text = models.CharField(max_length=255) panels = [ FieldPanel("url"), FieldPanel("text"), ]

Mixin 顺序有硬性要求:LockableMixin要放在其他 Mixin之后RevisionMixin之前(从左到右)。例如与DraftStateMixinRevisionMixin并存时正确写法是class MyModel(DraftStateMixin, LockableMixin, RevisionMixin),且系统检查(system check)会强制校验这个顺序。LockableMixin引入的字段同样需要makemigrations+migrate。锁定/解锁分别需要模型上的lockunlock权限,Wagtail 会自动创建并显示在"Groups"区域。

为 Snippet 启用工作流(WorkflowMixin)

若 snippet 模型继承wagtail.models.WorkflowMixin,Wagtail 会允许为该模型指派工作流;指派后编辑器会出现"Submit for moderation"等工作流动作菜单项,状态侧边面板也会显示当前工作流信息。由于工作流依赖版本与发布机制,继承WorkflowMixin必须同时继承RevisionMixinDraftStateMixin,并推荐同时继承LockableMixin,使 snippet 在流程中处于锁定状态、仅审核人可编辑。

Mixin 提供workflow_states属性(该实例所有 workflow state 的 queryset),并带有到wagtail.models.WorkflowState模型的默认GenericRelation用于实例删除时的清理。默认关系同样没有related_query_name,如需从WorkflowState反向查询可自定义GenericRelation

Advert启用工作流(含锁定)的完整定义:

# ... from wagtail.models import DraftStateMixin, LockableMixin, RevisionMixin, WorkflowMixin # ... class Advert( WorkflowMixin, DraftStateMixin, LockableMixin, RevisionMixin, models.Model ): url = models.URLField(null=True, blank=True) text = models.CharField(max_length=255) _revisions = GenericRelation("wagtailcore.Revision", related_query_name="advert") workflow_states = GenericRelation( "wagtailcore.WorkflowState", content_type_field="base_content_type", object_id_field="object_id", related_query_name="advert", for_concrete_model=False, ) panels = [ FieldPanel("url"), FieldPanel("text"), ] @property def revisions(self): return self._revisions

相关字段变化同样需要makemigrations+migrate。启用 Mixin 后可通过后台的 workflow 设置给 snippet 模型指派工作流;后台仪表盘与工作流报告也会把提交到工作流的 snippet(与页面并列)展示出来。

为 Snippet 添加标签(Tagging)

给 snippet 打标签与给页面打标签非常相似,唯一区别是:若应用RevisionMixin,应使用taggit.managers.TaggableManager而非modelcluster.contrib.taggit.ClusterTaggableManager

# ... from modelcluster.fields import ParentalKey from modelcluster.models import ClusterableModel from taggit.models import TaggedItemBase from taggit.managers import TaggableManager # ... class AdvertTag(TaggedItemBase): content_object = ParentalKey( "demo.Advert", on_delete=models.CASCADE, related_name="tagged_items" ) class Advert(ClusterableModel): # ... tags = TaggableManager(through=AdvertTag, blank=True) panels = [ # ... FieldPanel("tags"), ]

在视图中使用标签的更多细节可参考仓库文档 docs/advanced_topics/tags.md。

Snippet 内的 Inline 模型

类似页面,snippet 内可以嵌套其他模型。这要求 snippet 模型继承modelcluster.models.ClusterableModel而非django.db.models.Model

from django.db import models from modelcluster.fields import ParentalKey from modelcluster.models import ClusterableModel from wagtail.models import Orderable class BandMember(Orderable): band = ParentalKey("music.Band", related_name="members", on_delete=models.CASCADE) name = models.CharField(max_length=255) @register_snippet class Band(ClusterableModel): name = models.CharField(max_length=255) panels = [FieldPanel("name"), InlinePanel("members")]

页面 inline 模型的使用方法(docs/topics/writing_templates.md 之外的 inline 章节同样适用于 snippet)。

定制 Snippet 后台视图:SnippetViewSet

本章内容对应 docs/topics/snippets/customizing.md。每个 snippet 模型后台视图的进一步定制都通过自定义SnippetViewSet类完成。它是wagtail.admin.viewsets.ModelViewSet的子类,附带 snippets 专属的默认属性,因此完整支持ModelViewSet的定制能力(自定义列表视图——加列、加过滤器、创建自定义菜单项等)。前提是:如注册章节所述,先用函数方式调用register_snippet注册模型。

从源码结构看,SnippetViewSet定义在 wagtail/snippets/views/snippets.py,而SnippetViewSetGroup继承自ModelViewSetGroup(见 wagtail/snippets/views/snippets.py)。

完整示例:Member 模型与 MemberViewSet

先看一个带筛选器的示例模型:

# models.py from django.db import models from wagtail.admin.filters import WagtailFilterSet class Member(models.Model): class ShirtSize(models.TextChoices): SMALL = "S", "Small" MEDIUM = "M", "Medium" LARGE = "L", "Large" EXTRA_LARGE = "XL", "Extra Large" name = models.CharField(max_length=255) shirt_size = models.CharField( max_length=5, choices=ShirtSize.choices, default=ShirtSize.MEDIUM ) def first_name(self): return self.name.split()[0] first_name.admin_order_field = "name" first_name.short_description = "First name" class MemberFilterSet(WagtailFilterSet): class Meta: model = Member fields = ["shirt_size"]

对应的SnippetViewSet子类:

# wagtail_hooks.py from wagtail.admin.panels import FieldPanel, ObjectList, TabbedInterface from wagtail.admin.ui.tables import UpdatedAtColumn from wagtail.snippets.models import register_snippet from wagtail.snippets.views.snippets import SnippetViewSet from myapp.models import Member, MemberFilterSet class MemberViewSet(SnippetViewSet): model = Member icon = "user" list_display = ["name", "first_name", "shirt_size", UpdatedAtColumn()] list_per_page = 50 copy_view_enabled = False inspect_view_enabled = True admin_url_namespace = "member_views" base_url_path = "internal/member" filterset_class = MemberFilterSet # 也可以不用 filterset_class,改用以下方式: # list_filter = ["shirt_size"] # 或 # list_filter = {"shirt_size": ["exact"], "name": ["icontains"]} edit_handler = TabbedInterface( [ ObjectList([FieldPanel("name")], heading="Details"), ObjectList([FieldPanel("shirt_size")], heading="Preferences"), ] ) register_snippet(MemberViewSet)

图标(icon)

SnippetViewSet上定义icon属性可指定该 snippet 类型在后台各处使用的图标;图标需先在 Wagtail 图标库中注册(参考 docs/advanced_topics/icons.md)。未设置时默认使用"snippet"图标。

URL 命名空间与基础路径

  • url_namespace属性可覆写为视图 URL 模式使用自定义命名空间;未设置时默认为wagtailsnippets_{app_label}_{model_name}
  • 覆写url_prefix可定制相对 Wagtail 后台 URL 的基础路径;未设置时默认为snippets/app_label/model_name

snippet chooser 视图也有对应的 URL 定制入口:chooser_admin_url_namespacechooser_base_url_pathget_chooser_admin_url_namespaceget_chooser_admin_base_path

列表视图(Listing view)

通过SnippetViewSet上的各类属性可定制列表视图的自定义列、过滤器、分页等,具体参考ModelViewSet的列表视图定制文档。此外可覆写get_queryset方法定制列表视图的基础 queryset。

复制视图与检查视图

  • 复制视图(copy view)默认启用,拥有模型add权限的用户可访问;设置copy_view_enabled = False可关闭;
  • 检查视图(inspect view)默认关闭(对大多数模型用处不大);设置inspect_view_enabled = True可开启。

模板前缀

模板定制方式与ModelViewSet相同,区别在于template_prefix默认值为wagtailsnippets/snippets/

侧边栏菜单项

默认情况下,注册 snippet 模型会在侧边栏菜单中增加一个"Snippets"菜单项。设置add_to_admin_menu = True可让某个 snippet 模型拥有独立的顶级菜单项:

from wagtail.snippets.views.snippets import SnippetViewSet class AdvertViewSet(SnippetViewSet): model = Advert icon = "crosshairs" menu_label = "Advertisements" menu_name = "adverts" menu_order = 300 add_to_admin_menu = True

多个 snippet 模型还可以用一个SnippetViewSetGroup归到同一个菜单项下:把model属性设在各自的SnippetViewSet类上,然后注册SnippetViewSetGroup子类(而不是逐个注册每个模型或 viewset):

from wagtail.snippets.views.snippets import SnippetViewSet, SnippetViewSetGroup class AdvertViewSet(SnippetViewSet): model = Advert icon = "crosshairs" menu_label = "Advertisements" menu_name = "adverts" class ProductViewSet(SnippetViewSet): model = Product icon = "desktop" menu_label = "Products" menu_name = "banners" class MarketingViewSetGroup(SnippetViewSetGroup): items = (AdvertViewSet, ProductViewSet) menu_icon = "folder-inverse" menu_label = "Marketing" menu_name = "marketing" # 使用 SnippetViewSetGroup 归组时,只需注册 # SnippetViewSetGroup 类本身,无需逐个注册每个模型或 viewset register_snippet(MarketingViewSetGroup)

默认侧边栏"Snippets"菜单项只展示没有配置独立菜单项的 snippet 模型;若所有 snippet 模型都有独立菜单项,"Snippets"菜单项将不再显示。该行为可通过设置WAGTAILSNIPPETS_MENU_SHOW_ALL改变——从 wagtail/snippets/views/snippets.py 的get_snippet_models_for_index_view源码可见:当getattr(settings, "WAGTAILSNIPPETS_MENU_SHOW_ALL", False)为真时,索引视图返回全部 snippet 模型,否则过滤掉已注册独立菜单项的模型;该行为也有对应测试(见 wagtail/snippets/tests/test_index_view.py 与 wagtail/snippets/tests/test_viewset.py)。

安全细节:URL 防护与注册校验

除文档主体外,仓库源码还体现了两个值得了解的保护机制:

  1. URL 防篡改:wagtail/snippets/views/snippets.py 中的get_snippet_model_from_url_params会从app_name/model_name组合解析模型,若解析出的模型不在get_snippet_models()注册列表中,直接抛出Http404——防止有人拼造 URL 去编辑未注册为 snippet 的内容类型;
  2. 注册时机保证:如前文所述,register_snippet通过DEFER_REGISTRATION标志与DEFERRED_REGISTRATIONS队列确保 viewset 在模型全部加载后才构建,规避了"hooks 早于模型就绪"引发的构造问题(对应仓库内注释中提到的历史缺陷)。

小结与实践建议

  • 选型判断:内容需要页面树位置、URL、排序时优先用 Page;独立的、被多处引用的内容块(页眉页脚、广告、导航等)用 snippet;
  • 注册方式:新代码一律推荐wagtail_hooks.py中函数式注册 + 自定义SnippetViewSet,把 Wagtail 关注点与 Django 模型解耦;
  • 能力增量:预览、搜索、版本、草稿、锁定、工作流按需求逐个叠加 Mixin,叠加后记得makemigrations+migrate,并注意 Mixin 继承顺序(DraftStateMixin → LockableMixin → RevisionMixin,系统检查会强制校验);
  • 草稿风险:启用DraftStateMixin后在页面中引用 snippet 时,自行检查live字段,避免把未发布内容渲染到前台;
  • 视图定制:列、过滤器、分页、复制/检查视图、菜单与 URL 路径均可在SnippetViewSet上声明式配置,模板覆盖前缀为wagtailsnippets/snippets/

关联文档全文见:注册 snippet、渲染 snippet、可选功能、定制后台视图;核心实现位于 wagtail/snippets/models.py 与 wagtail/snippets/views/snippets.py。

【免费下载链接】wagtailA Django content management system focused on flexibility and user experience项目地址: https://gitcode.com/GitHub_Trending/wa/wagtail

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询