使用 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, validatorsform_classname

EmailBlock

wagtail.core.blocks.EmailBlock

单行邮件输入,使用邮件地址校验器保证邮件地址输入正确。可使用关键字参数 required (缺省: True), help_text, validatorsform_classname

使用 EmailBlock 的例子请参考 Example: PersonBlock

IntegerBlock

wagtail.core.blocks.IntegerBlock

单行整数输入,使用校验器保证输入值为整数。 可使用关键字参数 required (缺省: True), max_value, min_value, help_text, validatorsform_classname

使用 IntegerBlock 的例子请参考 Example: PersonBlock

FloatBlock

wagtail.core.blocks.FloatBlock

单行浮点数输入,使用校验器保证输入值为浮点数。 可使用关键字参数 required (缺省: True), max_value, min_value, validatorsform_classname

DecimalBlock

wagtail.core.blocks.DecimalBlock

单行十进制数字,使用校验器保证输入值为十进制数字。 可使用关键字参数 required (缺省: True), help_text, max_value, min_value, max_digits, decimal_places, validatorsform_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, validatorsform_classname

URLBlock

wagtail.core.blocks.URLBlock

单行文本输入,要求输入内容是一个有效的 URL. 可使用关键字参数 required (缺省: True), max_length, min_length, help_text, validatorsform_classname

BooleanBlock

wagtail.core.blocks.BooleanBlock

复选框输入。可使用关键字参数 required, help_textform_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_classnameformat

format (缺省: None)

日期格式。格式定义应满足 DATE_INPUT_FORMATS 设置。 如果没有设置,则 Wagtail 将使用 WAGTAIL_DATE_FORMAT 设置格式,都未设定的情况下使用 ‘%Y-%m-%d’。

TimeBlock

wagtail.core.blocks.TimeBlock

时间选择器输入。 可使用关键字参数 required (缺省: True), help_text, validatorsform_classname

DateTimeBlock

wagtail.core.blocks.DateTimeBlock

日期时间选择器输入。 可使用关键字参数 required (缺省: True), help_text, format, validatorsform_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, editorfeatures

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, validatorsform_classname

警告

使用这个编辑器是输入的 HTML 中可以使用程序脚本, Wagtail 并不做安全性判断,脚本有可能会获得管理员权限,因此只有编辑人员是完全可信的人员才能考虑使用这个选项。

BlockQuoteBlock

wagtail.core.blocks.BlockQuoteBlock

文本输入,输入内容会包含在 HTML <blockquote> 标签对里面。 可使用关键字参数 required (缺省: True), max_length, min_length, help_text, validatorsform_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_lengthhelp_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, icontemplate, 和传递给块构造函数的含义相同。

这里定义 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_nummax_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_typevalue 属性,可以判断这些属性然后分别进行处理:

{% 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, photobiography 以各自方式定义,也可以写成:

{% 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 %} 语句是同样的意思,但限制更多,模板中的 requestpage 没有绑定; 因此建议只对不生成 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 基础数据类型的块,例如 CharBlockIntegerBlock 在模板中应用时有一些限制, 这是因为这些 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 类来理解 BoundBlockBoundField 也是包含了表单字段以及在表单上如何将值渲染到 HTML 表单的定义。

正常情况下,开发时不需要担心这些内部细节,Wagtail 使用模板时会处理这些问题。但在复杂的设计中,例如访问 ListBlockStructBlock 中的块时,这种情况下没有 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 与普通值应参考如下准则:

  1. 在遍历 StreamField 或 StreamBlock (例如 {% for block in page.body %})时,将得到一系列 BoundBlocks 条目。

  2. 对于 BoundBlock 对象实例, 使用 block.value 获取原始值。

  3. 访问 StructBlock (例如 value.heading) 子项返回原始值, 而要使用相关的 BoundBlock 时, 使用 value.bound_blocks.heading 格式。

  4. ListBlock 是一个普通的 Python 列表; 遍历时返回原始值,而不是 BoundBlocks 条目。

  5. 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,
        ),
    ]