使用 StreamField 实现页面灵活定制¶
StreamField 适合构建无固定结构的编辑模型,例如博客或新闻,这些页面正文经常是小标题、图片、引用及图像交替重复出现。也可以用于更特别的一些内容,例如图表、程序代码片段等。 在这种类型中,不同内容类型是以顺序’块(block)’的形式展示,但展示过程并无规律。
关于 StreamField 的更多背景需求,以及在文章主体中为什么应该使用它代替富文本编辑, 请参考博客 Rich text fields and faster horses.
StreamField 也提供了丰富的 API 接口给开发者定制块类型,从简单子块集成 (例如一个 ‘person’ 块包含姓、名及图片) 到复杂的拥有自身编辑界面的组件。 在数据库中,StreamField 的内容存贮为 JSON 字符串, 确保字段信息被保留,而不仅是 HTML 的展示内容。
使用 StreamField¶
StreamField
是模型的字段类型,定义模型时同使用其它字段类型的方法一致:
from django.db import models
from wagtail.core.models import Page
from wagtail.core.fields import StreamField
from wagtail.core import blocks
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
from wagtail.images.blocks import ImageChooserBlock
class BlogPage(Page):
author = models.CharField(max_length=255)
date = models.DateField("Post date")
body = StreamField([
('heading', blocks.CharBlock(form_classname="full title")),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
])
content_panels = Page.content_panels + [
FieldPanel('author'),
FieldPanel('date'),
StreamFieldPanel('body'),
]
注意: StreamField 并不向后兼容其它字段类型,例如 RichTextField 类型。如果需要改变模型的字段类型为 StreamField, 请参考 将 RichTextFields 改成 StreamField。
StreamField
是 (name, block_type)
元组的列表。 ‘name’ 用于在模型及 JSON 串中识别一个块 (命名应遵循 Python 变更命名规范: 使用小写字母及下划线,不能有空格)
‘block_type’ 应是块定义对象,参见如下说明。(有时, StreamField
也可以使用一个 StreamBlock
实例做为参数 - 参考 Structural block types 。)
这个列表定义了编辑字段时可以使用的块类型,页面作者可使用这个块任意多次,顺序也是根据需要任意设置(这里体现了’流’布局)。
StreamField
也有可选关键字参数 blank
, 默认为 false; 当设为 false 时,至少应该入一个块内容。
基本块类型¶
所有块类型接受以下关键字参数:
default
缺省值,新建空’块’时默认值。
label
提示标识,在编辑界面中引用块时的提示信息。缺省是块名可读版本。
icon
图标,在编辑菜单中标识块的图标。可用的图标名,参考 Wagtail 样式指南,可以增加
wagtail.contrib.styleguide
到项目配置的INSTALLED_APPS
设置中。template
模板文件,指定渲染这个块内容所使用的模板文件。参考 `Template rendering`_ 。
group
分组,说明块所在分类分组,同类分组的块将在编辑界面上集中显示在组名下面。
Wagtail 提供基本块类型有:
CharBlock¶
wagtail.core.blocks.CharBlock
单行文本输入。可跟如下关键字参数:
required
(缺省: True)设定为真时,字段必须输入。
max_length
,min_length
设定输入字符串最短和最长长度。
help_text
在字段输入旁边的帮助提示内容。
validators
字段校验器函数列表 (参考 Django Validators)。
form_classname
定义在页面编辑表单输出时增加的表单样式(
class
)属性。
在 2.11 版更改: class
属性之前通过关键字参数 classname
设置。
TextBlock¶
wagtail.core.blocks.TextBlock
多行文本输入。 参数可使用 CharBlock
中的 required
(缺省: True), max_length
, min_length
, help_text
, validators
和 form_classname
。
EmailBlock¶
wagtail.core.blocks.EmailBlock
单行邮件输入,使用邮件地址校验器保证邮件地址输入正确。可使用关键字参数 required
(缺省: True), help_text
, validators
和 form_classname
。
使用 EmailBlock
的例子请参考 Example: PersonBlock
IntegerBlock¶
wagtail.core.blocks.IntegerBlock
单行整数输入,使用校验器保证输入值为整数。 可使用关键字参数 required
(缺省: True), max_value
, min_value
, help_text
, validators
和 form_classname
。
使用 IntegerBlock
的例子请参考 Example: PersonBlock
FloatBlock¶
wagtail.core.blocks.FloatBlock
单行浮点数输入,使用校验器保证输入值为浮点数。 可使用关键字参数 required
(缺省: True), max_value
, min_value
, validators
和 form_classname
。
DecimalBlock¶
wagtail.core.blocks.DecimalBlock
单行十进制数字,使用校验器保证输入值为十进制数字。 可使用关键字参数 required
(缺省: True), help_text
, max_value
, min_value
, max_digits
, decimal_places
, validators
和 form_classname
。
使用 DecimalBlock
的例子请参考 Example: PersonBlock
RegexBlock¶
wagtail.core.blocks.RegexBlock
单行文件输入,要求输入内容符合正则表达式的要求。正则表达式必需是第一个参数,或者使用关键字参数 regex
指定。校验错误时,提示信息可以传递一个字典给关键字 error_messages
,字典的键名可以是
required
(当前输入内容不能为空时) 或 invalid
(当输入值不满足正则表达式要求时):
blocks.RegexBlock(regex=r'^[0-9]{3}$', error_messages={
'invalid': "Not a valid library card number."
})
可使用关键字参数 regex
, error_messages
, help_text
, required
(缺省: True), max_length
, min_length
, validators
和 form_classname
。
URLBlock¶
wagtail.core.blocks.URLBlock
单行文本输入,要求输入内容是一个有效的 URL. 可使用关键字参数 required
(缺省: True), max_length
, min_length
, help_text
, validators
和 form_classname
。
BooleanBlock¶
wagtail.core.blocks.BooleanBlock
复选框输入。可使用关键字参数 required
, help_text
和 form_classname
。 As with Django’s BooleanField
, a value of required=True
(the default) indicates that the checkbox must be ticked in order to proceed. For a checkbox that can be ticked or unticked, you must explicitly pass in required=False
.
DateBlock¶
wagtail.core.blocks.DateBlock
日期选择器输入。可使用关键字参数 required
(缺省: True), help_text
, validators
, form_classname
和 format
。
format
(缺省: None)日期格式。格式定义应满足 DATE_INPUT_FORMATS 设置。 如果没有设置,则 Wagtail 将使用
WAGTAIL_DATE_FORMAT
设置格式,都未设定的情况下使用 ‘%Y-%m-%d’。
TimeBlock¶
wagtail.core.blocks.TimeBlock
时间选择器输入。 可使用关键字参数 required
(缺省: True), help_text
, validators
和 form_classname
。
DateTimeBlock¶
wagtail.core.blocks.DateTimeBlock
日期时间选择器输入。 可使用关键字参数 required
(缺省: True), help_text
, format
, validators
和 form_classname
。
format
(default: None)日期时间格式。格式定义应满足 DATETIME_INPUT_FORMATS 设置。 如果没有设置,则 Wagtail 将使用
WAGTAIL_DATETIME_FORMAT
设置格式,都未设定的情况下使用 ‘%Y-%m-%d %H:%M’。
RichTextBlock¶
wagtail.core.blocks.RichTextBlock
富文本编辑器输入,可生成包括链接、粗/斜体等格式化正文。 可使用关键字参数 required
(缺省: True), help_text
, validators
, form_classname
, editor
和 features
。
editor
(缺省:default
)使用的富文本编辑器 (参考 Rich text)。
features
(缺省: None)设定编辑器的参数 (参考 Limiting features in a rich text field)。
RawHTMLBlock¶
wagtail.core.blocks.RawHTMLBlock
多行文本编辑器输入。输入的内容为 HTML 文本,在页面输出时可以不转义输出。 可使用关键字参数 required
(缺省: True), max_length
, min_length
, help_text
, validators
和 form_classname
。
警告
使用这个编辑器是输入的 HTML 中可以使用程序脚本, Wagtail 并不做安全性判断,脚本有可能会获得管理员权限,因此只有编辑人员是完全可信的人员才能考虑使用这个选项。
BlockQuoteBlock¶
wagtail.core.blocks.BlockQuoteBlock
文本输入,输入内容会包含在 HTML <blockquote> 标签对里面。 可使用关键字参数 required
(缺省: True), max_length
, min_length
, help_text
, validators
和 form_classname
。
ChoiceBlock¶
wagtail.core.blocks.ChoiceBlock
下拉选择框,可使用关键字参数
choices
选项列表,以 Django
choices
可接受的格式,或者一个可调用的方法返回结果满足格式要求。required
(缺省: True)为 true 时, 字段选择不能是空白。
help_text
在字段输入旁边的帮助提示内容。
validators
字段校验器函数列表 (参考 Django Validators)。
form_classname
定义在页面编辑表单输出时增加的表单样式(
class
)属性。widget
渲染表单字段时使用的组件 (参考 Django Widgets)。
ChoiceBlock
也可以定义为一个子类,以方便在多个地方引用。例如,如下定义的块:
blocks.ChoiceBlock(choices=[
('tea', 'Tea'),
('coffee', 'Coffee'),
], icon='cup')
也可以重写成 ChoiceBlock 的子类:
class DrinksChoiceBlock(blocks.ChoiceBlock):
choices = [
('tea', 'Tea'),
('coffee', 'Coffee'),
]
class Meta:
icon = 'cup'
StreamField
定义``ChoiceBlock`` 时使用 DrinksChoiceBlock()
即可。 注意,这只在 choices
是固定列表时有效,不能是可调用的方法。
MultipleChoiceBlock¶
wagtail.core.blocks.MultipleChoiceBlock
多项选择框,可使用关键字参数有:
choices
选项列表,以 Django
choices
可接受的格式,或者一个可调用的方法返回结果满足格式要求。required
(缺省: True)为 true 时, 字段选择不能是空白。
help_text
在字段输入旁边的帮助提示内容。
validators
字段校验器函数列表 (参考 Django Validators)。
form_classname
定义在页面编辑表单输出时增加的表单样式(
class
)属性。widget
渲染表单字段时使用的组件 (参考 Django Widgets)。
PageChooserBlock¶
wagtail.core.blocks.PageChooserBlock
页面选择组件,使用 Wagtail 浏览器进行页面选择。可使用关键字参数有:
required
(缺省: True)为 true 时, 不能不选择页面。
page_type
(缺省: Page)设定选择指定类型的页面。可以是一个模型类、模型名(字符串),或者是它们的列表。
can_choose_root
(缺省: False)可选择根页面。一个情况下,根页面是从不使用的,但在一些特定的场景是有意义的。例如使用 PageChooserBlock 提供一个子页面集合的起点,使用根结点就表示可以是’任可页面’。
DocumentChooserBlock¶
wagtail.documents.blocks.DocumentChooserBlock
文档选择组件,使用 Wagtail 文档浏览器进行文档选择,可以在选择时上传文档。可使用关键字参数只有 required
(缺省: True)。
ImageChooserBlock¶
wagtail.images.blocks.ImageChooserBlock
图片选择组件,使用 Wagtail 图片浏览器进行图片选择,可以在选择时上传图片。可使用关键字参数只有 required
(缺省: True)。
SnippetChooserBlock¶
wagtail.snippets.blocks.SnippetChooserBlock
片段选择组件,用于选择一个片段,需要一个位置参数:片段类,表示片段从哪个类中取。可使用关键字参数只有 required
(缺省: True)。
EmbedBlock¶
wagtail.embeds.blocks.EmbedBlock
内嵌对象编辑器,输入一个需要嵌入页面的多媒体对象的 URL (例如,一个云视频)。可使用关键字参数 required
(缺省: True), max_length
, min_length
和 help_text
。
StaticBlock¶
wagtail.core.blocks.StaticBlock
不包括任何字段内容,只是给模板传递一个特殊值。这在只想传递一个静态不需要编辑数值给编辑器时有用,例如一个地址、一个第三方的服务参数或一段较复杂代码的模板。
默认情况下, 缺省文本 (包含 label
位置关键字传入的内容) 将显示在编辑界面上,看起来块不是空的。也可以使用 admin_text
位置关键字来定义显示的内容:
blocks.StaticBlock(
admin_text='Latest posts: no configuration needed.',
# or admin_text=mark_safe('<b>Latest posts</b>: no configuration needed.'),
template='latest_posts.html')
StaticBlock
也可定义成子类,以便在多处引用:
class LatestPostsStaticBlock(blocks.StaticBlock):
class Meta:
icon = 'user'
label = 'Latest posts'
admin_text = '{label}: configured elsewhere'.format(label=label)
template = 'latest_posts.html'
Structural block types¶
除了上述基础块类型外,还可以定义结构化的块类型。新的类型由子块构成: 例如 ‘person’ 块可以由姓、名、头像构成,’轮播图’ 由不定数量的图片块构成。结构块可以嵌套,深度不限,还可以使用包含列表的块以及块的列表。
StructBlock¶
wagtail.core.blocks.StructBlock
结构块由一组固定顺序的子块组成。使用 (name, block_definition)
元组列表做为其第一个参数:
('person', blocks.StructBlock([
('first_name', blocks.CharBlock()),
('surname', blocks.CharBlock()),
('photo', ImageChooserBlock(required=False)),
('biography', blocks.RichTextBlock()),
], icon='user'))
另外也可以采用基于 StructBlock 子类的方法来实现上面功能:
class PersonBlock(blocks.StructBlock):
first_name = blocks.CharBlock()
surname = blocks.CharBlock()
photo = ImageChooserBlock(required=False)
biography = blocks.RichTextBlock()
class Meta:
icon = 'user'
Meta
类支持属性包括 default
, label
, icon
和 template
, 和传递给块构造函数的含义相同。
这里定义 PersonBlock()
做为块类型,可以在多个模型定义中进行复用。
body = StreamField([
('heading', blocks.CharBlock(form_classname="full title")),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
('person', PersonBlock()),
])
定制 StructBlock
在页面编辑器的更多显示选项请参考 定制 StructBlock 编辑界面 。
在模板中使用 StructBlock
值的方法参考 定制 StructBlock 的 value_class 。
ListBlock¶
wagtail.core.blocks.ListBlock
列表块定义许多类型相同的子块。编辑器可以添加不同数量的子块,可以执行重新排序和删除子块的操作。将子块做为第一个参数来构造这个类实例:
('ingredients_list', blocks.ListBlock(blocks.CharBlock(label="Ingredient")))
包括结构块在内的任何类型的块都可以做为列表块的子块。
('ingredients_list', blocks.ListBlock(blocks.StructBlock([
('ingredient', blocks.CharBlock()),
('amount', blocks.CharBlock(required=False)),
])))
如果要改变 ListBlock
在编辑界面中显示风格,使用 form_classname
关键字参数来传递值给 ListBlock
构造函数:
('ingredients_list', blocks.ListBlock(blocks.StructBlock([
('ingredient', blocks.CharBlock()),
('amount', blocks.CharBlock(required=False)),
]), form_classname='ingredients-list'))
实现中也可以将 form_classname
增加到 Meta
子类中:
class IngredientsListBlock(blocks.ListBlock):
ingredient = blocks.CharBlock()
amount = blocks.CharBlock(required=False)
class Meta:
form_classname = 'ingredients-list'
StreamBlock¶
wagtail.core.blocks.StreamBlock
流块由一系列不同类型子块构成,可以混合顺序使用,编辑时可重排显示顺序。 可以使用 StreamField 的全部机制,同时可以嵌入其它结构化的块类型中。使用 (name, block_definition)
元组列表做为第一个参数:
('carousel', blocks.StreamBlock(
[
('image', ImageChooserBlock()),
('quotation', blocks.StructBlock([
('text', blocks.TextBlock()),
('author', blocks.CharBlock()),
])),
('video', EmbedBlock()),
],
icon='cogs'
))
同 StructBlock 结合使用时, 子块列表也可以实现成 StreamBlock 的子类:
class CarouselBlock(blocks.StreamBlock):
image = ImageChooserBlock()
quotation = blocks.StructBlock([
('text', blocks.TextBlock()),
('author', blocks.CharBlock()),
])
video = EmbedBlock()
class Meta:
icon='cogs'
由于 StreamField
接受 StreamBlock
做为参数,在使用块类型列表的地方都可以使用 StreamBlock
,这样可重用一套块类型集:
class HomePage(Page):
carousel = StreamField(CarouselBlock(max_num=10, block_counts={'video': {'max_num': 2}}))
StreamBlock
使用如下关键字参数或 Meta
类属性:
required
(缺省: True)为 true 时至少编辑时至少应提供一个子块。这在使用
StreamBlock
做为 StreamField 最顶级块时忽略,在这种情况下使用 StreamField 字段的blank
属性定义值。min_num
字段中包含最少子区块数量。
max_num
字段中包含最多子区块数量。
block_counts
设定每种子类型块最少、最多数量,值为一个字典,字典里块名又映射到一个包含
min_num
和max_num
字段的字典.form_classname
定义编辑器中
StreamBlock
使用的风格样式名。('event_promotions', blocks.StreamBlock([ ('hashtag', blocks.CharBlock()), ('post_date', blocks.DateBlock()), ], form_classname='event-promotions'))
class EventPromotionsBlock(blocks.StreamBlock): hashtag = blocks.CharBlock() post_date = blocks.DateBlock() class Meta: form_classname = 'event-promotions'
Example: PersonBlock
¶
下面例子展示了如何基于 StructBlock
使用基础块类型生成复杂块类型 :
from wagtail.core import blocks
class PersonBlock(blocks.StructBlock):
name = blocks.CharBlock()
height = blocks.DecimalBlock()
age = blocks.IntegerBlock()
email = blocks.EmailBlock()
class Meta:
template = 'blocks/person_block.html'
模板渲染¶
StreamField 提供了块的 HTML 显示方法,可以做为一个整体输出,也可以分块输出。 使用 {% include_block %}
标签将块 HTML 渲染结果插入到页面中:
{% load wagtailcore_tags %}
...
{% include_block page.body %}
在缺省渲染过程中,流字段中的每个块都包含在 <div class="block-my_block_name">
元素中 (这里 my_block_name
是 StreamField 中定义的块名)。
如果需要使用自定义的 HTML 标记,可能遍历字段中各个值,然后对每个块使用 {% include_block %}
进行处理:
{% load wagtailcore_tags %}
...
<article>
{% for block in page.body %}
<section>{% include_block block %}</section>
{% endfor %}
</article>
如需进一步控制特定块的渲染方法,每个块都提供了 block_type
和 value
属性,可以判断这些属性然后分别进行处理:
{% load wagtailcore_tags %}
...
<article>
{% for block in page.body %}
{% if block.block_type == 'heading' %}
<h1>{{ block.value }}</h1>
{% else %}
<section class="block-{{ block.block_type }}">
{% include_block block %}
</section>
{% endif %}
{% endfor %}
</article>
默认情况下,每个块使用最简单的 HTML 标签修饰,或者根本不用。例如 CharBlock 值直接渲染成普通广西,ListBlock 输出子块时使用 <ul> 修饰。
在定制这个 HTML 渲染过程,可以通过 template
参数将渲染模板文件名传递给块。这对基于 StructBlock 的块特别有用:
('person', blocks.StructBlock(
[
('first_name', blocks.CharBlock()),
('surname', blocks.CharBlock()),
('photo', ImageChooserBlock(required=False)),
('biography', blocks.RichTextBlock()),
],
template='myapp/blocks/person.html',
icon='user'
))
或者在定义 StructBlock 子类时:
class PersonBlock(blocks.StructBlock):
first_name = blocks.CharBlock()
surname = blocks.CharBlock()
photo = ImageChooserBlock(required=False)
biography = blocks.RichTextBlock()
class Meta:
template = 'myapp/blocks/person.html'
icon = 'user'
在模板中,块值通过变量 value
取出:
{% load wagtailimages_tags %}
<div class="person">
{% image value.photo width-400 %}
<h2>{{ value.first_name }} {{ value.surname }}</h2>
{{ value.biography }}
</div>
由于 first_name
, surname
, photo
和 biography
以各自方式定义,也可以写成:
{% load wagtailcore_tags wagtailimages_tags %}
<div class="person">
{% image value.photo width-400 %}
<h2>{% include_block value.first_name %} {% include_block value.surname %}</h2>
{% include_block value.biography %}
</div>
使用 {{ my_block }}
与 {% include_block my_block %}
语句是同样的意思,但限制更多,模板中的 request
或 page
没有绑定; 因此建议只对不生成 HTML 的简单值进行处理,例如 PersonBlock 使用的模板:
{% load wagtailimages_tags %}
<div class="person">
{% image value.photo width-400 %}
<h2>{{ value.first_name }} {{ value.surname }}</h2>
{% if request.user.is_authenticated %}
<a href="#">Contact this person</a>
{% endif %}
{{ value.biography }}
</div>
这里 request.user.is_authenticated
在通过 {{ ... }}
渲染时测试不正确:
{# Incorrect: #}
{% for block in page.body %}
{% if block.block_type == 'person' %}
<div>
{{ block }}
</div>
{% endif %}
{% endfor %}
{# Correct: #}
{% for block in page.body %}
{% if block.block_type == 'person' %}
<div>
{% include_block block %}
</div>
{% endif %}
{% endfor %}
和 Django 的 {% include %}
标签一样, {% include_block %}
允许使用类似于 {% include_block my_block with foo="bar" %}
的语法传递额外的变量给模板:
{# In page template: #}
{% for block in page.body %}
{% if block.block_type == 'person' %}
{% include_block block with classname="important" %}
{% endif %}
{% endfor %}
{# In PersonBlock template: #}
<div class="{{ classname }}">
...
</div>
{% include_block my_block with foo="bar" only %}
的语法格式也是支持的,这里设置父模板中的变量只有 foo
传递给子模板。
除了使用父模板传来变量,子类中也可以重写 get_context
方法来增加模板上下文的变量:
import datetime
class EventBlock(blocks.StructBlock):
title = blocks.CharBlock()
date = blocks.DateBlock()
def get_context(self, value, parent_context=None):
context = super().get_context(value, parent_context=parent_context)
context['is_happening_today'] = (value['date'] == datetime.date.today())
return context
class Meta:
template = 'myapp/blocks/event.html'
在这个例子中,在模板中 is_happening_today
变更也是可以使用的。 当块通过 {% include_block %}
标签渲染模板时,关键字参数 parent_context
是可用的,其中存放者父模板中的上下文变量字典。
BoundBlock 与 value¶
全部块类型,不只是 StructBlock,都接受 template
参数来决定在页面上显示时如何渲染。但是负责处理 Python 基础数据类型的块,例如 CharBlock
和 IntegerBlock
在模板中应用时有一些限制,
这是因为这些 Python 内置类型(如 str
, int
等等) 并不能 ‘告知’ 是否用在模板渲染中。可能通过一个例子来了解下这种情况:
class HeadingBlock(blocks.CharBlock):
class Meta:
template = 'blocks/heading.html'
这里 blocks/heading.html
内容如下:
<h1>{{ value }}</h1>
这样就定义了一个类似文本字段的块,但输出时使用 <h1>
标签进行渲染:
class BlogPage(Page):
body = StreamField([
# ...
('heading', HeadingBlock()),
# ...
])
{% load wagtailcore_tags %}
{% for block in page.body %}
{% if block.block_type == 'heading' %}
{% include_block block %} {# This block will output its own <h1>...</h1> tags. #}
{% endif %}
{% endfor %}
这种设计设想是值是一个普通的字符串,但在模板 include_block 引用时输出了带 <h1> 标签的字符串。这在 Python 中使用非常容易造成困扰,但在这个地方可以完成预想的工作,
原因是 block 是通过遍历 StreamField 获得的条目,并不是块的本身的值。遍历时获得的值是 BoundBlock
类的实例,包括了值和块的定义内容。跟踪一下块定义,BoundBlock
是知道使用哪个模板渲染输出结果的。要获取本身编辑时输入的值,应使用 block.value
变量。所在在模板页面中使用 {% include_block block.value %}
就得到正常输入字符串,没有 <h1>
标签。
(严格说来,遍历 StreamField 字段获得的条目是 StreamChild
实例,包括 block_type
属性和 value
属性。)
有 Django 开发经验的开发者可以参考 Django Form 框架中的 BoundField
类来理解 BoundBlock
,BoundField
也是包含了表单字段以及在表单上如何将值渲染到 HTML 表单的定义。
正常情况下,开发时不需要担心这些内部细节,Wagtail 使用模板时会处理这些问题。但在复杂的设计中,例如访问 ListBlock
或 StructBlock
中的块时,这种情况下没有 BoundBlock
这层处理,
所以其中的条目不知道是在做模板渲染。下面例子,HeadingBlock
是一个 StructBlock 的子项:
class EventBlock(blocks.StructBlock):
heading = HeadingBlock()
description = blocks.TextBlock()
# ...
class Meta:
template = 'blocks/event.html'
blocks/event.html
文件内容:
{% load wagtailcore_tags %}
<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
{% include_block value.heading %}
- {% include_block value.description %}
</div>
这种情况下,value.heading
返回的是一个原始输入的字符串内容,而不是 BoundBlock
,这是必须的,否则像 {% if value.heading == 'Party!' %}
之类比较不可能为真。
这意味着 {% include_block value.heading %}
将渲染输出为普通字符串,没有 <h1>
标签。要生成带 <h1>
标签的模板,要使用 value.bound_blocks.heading
的形式
来明确指明使用 BoundBlock
实例的模板格式输出。
{% load wagtailcore_tags %}
<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
{% include_block value.bound_blocks.heading %}
- {% include_block value.description %}
</div>
实际上对这种复合块结构的输出,为了更具可读性,最好是相关的 HTML 标签编写在顶层复合块模板中。例如,要输出 <h1>
标签,可以在 EventBlock 的模板中定义:
{% load wagtailcore_tags %}
<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
<h1>{{ value.heading }}</h1>
- {% include_block value.description %}
</div>
这种限制不会影响 StructBlock 及 StreamBlock 做为 StructBlock 子结点的值, 因为 Wagtail 在实现这些复合块时知道它们渲染的模板,与放不放在 BoundBlock
中无关。
例如一个结构块嵌入另一个结构块中:
class EventBlock(blocks.StructBlock):
heading = HeadingBlock()
description = blocks.TextBlock()
guest_speaker = blocks.StructBlock([
('first_name', blocks.CharBlock()),
('surname', blocks.CharBlock()),
('photo', ImageChooserBlock()),
], template='blocks/speaker.html')
这样 EventBlock 模板中使用 {% include_block value.guest_speaker %}
会知道使用 blocks/speaker.html
渲染输出。
总结下来,使用 BoundBlocks 与普通值应参考如下准则:
在遍历 StreamField 或 StreamBlock (例如
{% for block in page.body %}
)时,将得到一系列 BoundBlocks 条目。对于 BoundBlock 对象实例, 使用
block.value
获取原始值。访问 StructBlock (例如
value.heading
) 子项返回原始值, 而要使用相关的 BoundBlock 时, 使用value.bound_blocks.heading
格式。ListBlock 是一个普通的 Python 列表; 遍历时返回原始值,而不是 BoundBlocks 条目。
StructBlock 和 StreamBlock 值是一至知道自已渲染模板的, 即使不在遍历的 BoundBlock 条目中。
定制 StructBlock
编辑界面¶
- 要改变
StructBlock
在页面编辑器上显示的方式,可以使用form_classname
指定 CSS 风格 (通过
StructBlock
构造函数的关键字参数,或Meta
子类的属性), 设置后会用指定值覆盖缺省的struct-block
:
class PersonBlock(blocks.StructBlock):
first_name = blocks.CharBlock()
surname = blocks.CharBlock()
photo = ImageChooserBlock(required=False)
biography = blocks.RichTextBlock()
class Meta:
icon = 'user'
form_classname = 'person-block struct-block'
这个就可以为这个块设置特定的 CSS 样式 person-block
,参考 insert_editor_css 勾子说明。
注解
Wagtail 编辑器已经内置了 struct-block
类以及其它相关元素的 CSS 样式定义。使用 form_classname
设定 StructBlock
时记得加上 struct-block
以简化定义内容。
如果要更进一步改变 HTML 标签,可以重写 Meta
子类的 form_template
属性,设定新的模板文件。模板中可以使用如下变量:
children
组成
StructBlock
所有子块的OrderedDict
,字典内容是包含子块的``BoundBlock``。通常在模板中使用render_form
来渲染他们。help_text
块帮助文本。
classname
CCS 的样式类名,保存
form_classname
的值(缺省是struct-block
)。block_definition
这个块的
StructBlock
类实例。prefix
块实例中表单的前缀,以保证各个表单中的标识是唯一的。
要增加其它的变量,可以重写块的 get_form_context
方法:
class PersonBlock(blocks.StructBlock):
first_name = blocks.CharBlock()
surname = blocks.CharBlock()
photo = ImageChooserBlock(required=False)
biography = blocks.RichTextBlock()
def get_form_context(self, value, prefix='', errors=None):
context = super().get_form_context(value, prefix=prefix, errors=errors)
context['suggested_first_names'] = ['John', 'Paul', 'George', 'Ringo']
return context
class Meta:
icon = 'user'
form_template = 'myapp/block_forms/person.html'
定制 StructBlock
的 value_class¶
要改变获取 StructBlock
值的方法,可以设置 value_class
属性 (通过 StructBlock
构造函数的关键字参数,或 Meta
子类的属性)
来重新定义获值的过程。
value_class
必须是 StructValue
子类,任何附加的方法要取得子块原始值通过方法的 self
参数 (例如 self.get('my_block')
)。
例:
from wagtail.core.models import Page
from wagtail.core.blocks import (
CharBlock, PageChooserBlock, StructValue, StructBlock, TextBlock, URLBlock)
class LinkStructValue(StructValue):
def url(self):
external_url = self.get('external_url')
page = self.get('page')
if external_url:
return external_url
elif page:
return page.url
class QuickLinkBlock(StructBlock):
text = CharBlock(label="link text", required=True)
page = PageChooserBlock(label="page", required=False)
external_url = URLBlock(label="external URL", required=False)
class Meta:
icon = 'site'
value_class = LinkStructValue
class MyPage(Page):
quick_links = StreamField([('links', QuickLinkBlock())], blank=True)
quotations = StreamField([('quote', StructBlock([
('quote', TextBlock(required=True)),
('page', PageChooserBlock(required=False)),
('external_url', URLBlock(required=False)),
], icon='openquote', value_class=LinkStructValue))], blank=True)
content_panels = Page.content_panels + [
StreamFieldPanel('quick_links'),
StreamFieldPanel('quotations'),
]
在模板中使用改变获取值的方法:
{% load wagtailcore_tags %}
<ul>
{% for link in page.quick_links %}
<li><a href="{{ link.value.url }}">{{ link.value.text }}</a></li>
{% endfor %}
</ul>
<div>
{% for quotation in page.quotations %}
<blockquote cite="{{ quotation.value.url }}">
{{ quotation.value.quote }}
</blockquote>
{% endfor %}
</div>
定制块类型¶
在需要定义用户界面 UI,或处理不是 Wagtail 自带的块类型时(不能通过已有字段类型结构组合成时), 可以通过定制块类型来达成目标。 更深入的了解,可以参考 Wagtail 内置各种块类的源代码。
Wagtail 提供了一个 wagtail.core.blocks.FieldBlock
基类来 Django 中已存在字段类型的封装。子类只需定义一个 field
属性返回表单字段对象:
class IPAddressBlock(FieldBlock):
def __init__(self, required=True, help_text=None, **kwargs):
self.field = forms.GenericIPAddressField(required=required, help_text=help_text)
super().__init__(**kwargs)
迁移¶
迁移文件中定义的 StreamField 字段¶
同 Django 的模型字段一样, 任何影响 StreamField 的模型定义的改变,将生成迁移文件包括变更字段的定义。 由于 StreamField 比通常模型复杂,这将导致项目迁移文件导入 更多信息,这会导致在移动或删除时的一些问题。
要迁移的话,StructBlock, StreamBlock 及 ChoiceBlock 实现额外逻辑以确保这些块的子类能解构成 StructBlock, StreamBlock 及 ChoiceBlock 实例,这样避免对定义类 的引用。这样做是可能是,因为块类型定义了继承的标准模式,也知道根据这些模式重构子类对应块。
如果定义了其它块类的子类,例如 FieldBlock
,需要在项目的生命周期中保持那个类定义,并实现 定制析构方法 说明块在类中
表述方法。同样定制一个 StructBlock、StreamBlock 或 ChoiceBlock 的子类解析到不能再分解的基础块类型。如果在构造器中增加了额外的参数,也需要提供自已的 deconstruct
方法。
将 RichTextFields 改成 StreamField¶
如果将即存的 RichTextField 改成 StreamField,数据库迁移完成时不会报错,因为在数据库中都使用文本字段。但 StreamField 是使用 JSON 表示数据,
所以已有的数据需要做特殊的转换才能正确取出并展示。StreamField 需要至少包含一个 RichTextBlock 块类型。(当变更模型时,不要忘记记将 FieldPanel
改成 StreamFieldPanel
)
生成迁移文件使用 ./manage.py makemigrations
,然后编辑文件成如下形式(这个例子中,demo.BlogPage
模型的 ‘body’ 字段将转换成 StreamField 字段中 RichTextBlock 类型块 rich_text
):
# -*- coding: utf-8 -*-
from django.db import models, migrations
from wagtail.core.rich_text import RichText
def convert_to_streamfield(apps, schema_editor):
BlogPage = apps.get_model("demo", "BlogPage")
for page in BlogPage.objects.all():
if page.body.raw_text and not page.body:
page.body = [('rich_text', RichText(page.body.raw_text))]
page.save()
def convert_to_richtext(apps, schema_editor):
BlogPage = apps.get_model("demo", "BlogPage")
for page in BlogPage.objects.all():
if page.body.raw_text is None:
raw_text = ''.join([
child.value.source for child in page.body
if child.block_type == 'rich_text'
])
page.body = raw_text
page.save()
class Migration(migrations.Migration):
dependencies = [
# leave the dependency line from the generated migration intact!
('demo', '0001_initial'),
]
operations = [
# leave the generated AlterField intact!
migrations.AlterField(
model_name='BlogPage',
name='body',
field=wagtail.core.fields.StreamField([('rich_text', wagtail.core.blocks.RichTextBlock())]),
),
migrations.RunPython(
convert_to_streamfield,
convert_to_richtext,
),
]
注意,上述迁移仅对已发布的页面对象生效。如果也想迁移草稿及以前的版本,请使用如下代码:
# -*- coding: utf-8 -*-
import json
from django.core.serializers.json import DjangoJSONEncoder
from django.db import migrations, models
from wagtail.core.rich_text import RichText
def page_to_streamfield(page):
changed = False
if page.body.raw_text and not page.body:
page.body = [('rich_text', {'rich_text': RichText(page.body.raw_text)})]
changed = True
return page, changed
def pagerevision_to_streamfield(revision_data):
changed = False
body = revision_data.get('body')
if body:
try:
json.loads(body)
except ValueError:
revision_data['body'] = json.dumps(
[{
"value": {"rich_text": body},
"type": "rich_text"
}],
cls=DjangoJSONEncoder)
changed = True
else:
# It's already valid JSON. Leave it.
pass
return revision_data, changed
def page_to_richtext(page):
changed = False
if page.body.raw_text is None:
raw_text = ''.join([
child.value['rich_text'].source for child in page.body
if child.block_type == 'rich_text'
])
page.body = raw_text
changed = True
return page, changed
def pagerevision_to_richtext(revision_data):
changed = False
body = revision_data.get('body', 'definitely non-JSON string')
if body:
try:
body_data = json.loads(body)
except ValueError:
# It's not apparently a StreamField. Leave it.
pass
else:
raw_text = ''.join([
child['value']['rich_text'] for child in body_data
if child['type'] == 'rich_text'
])
revision_data['body'] = raw_text
changed = True
return revision_data, changed
def convert(apps, schema_editor, page_converter, pagerevision_converter):
BlogPage = apps.get_model("demo", "BlogPage")
for page in BlogPage.objects.all():
page, changed = page_converter(page)
if changed:
page.save()
for revision in page.revisions.all():
revision_data = json.loads(revision.content_json)
revision_data, changed = pagerevision_converter(revision_data)
if changed:
revision.content_json = json.dumps(revision_data, cls=DjangoJSONEncoder)
revision.save()
def convert_to_streamfield(apps, schema_editor):
return convert(apps, schema_editor, page_to_streamfield, pagerevision_to_streamfield)
def convert_to_richtext(apps, schema_editor):
return convert(apps, schema_editor, page_to_richtext, pagerevision_to_richtext)
class Migration(migrations.Migration):
dependencies = [
# leave the dependency line from the generated migration intact!
('demo', '0001_initial'),
]
operations = [
# leave the generated AlterField intact!
migrations.AlterField(
model_name='BlogPage',
name='body',
field=wagtail.core.fields.StreamField([('rich_text', wagtail.core.blocks.RichTextBlock())]),
),
migrations.RunPython(
convert_to_streamfield,
convert_to_richtext,
),
]