Skip to content

Latest commit

 

History

History
1070 lines (713 loc) · 42.4 KB

streamfield.md

File metadata and controls

1070 lines (713 loc) · 42.4 KB

采用StreamField特性的自由格式页面内容

Freeform page content using StreamField

Wagtail的StreamField特性,提供了一个适合于那些并不遵循固定结构 -- 诸如博客文章或新闻报道 -- 的一类页面的内容编辑模型,在这样的页面中,文本可能穿插有子标题、图片、拉取引用及视频等元素。此种内容编辑模型也适合于那些更为专用的内容类型,比如地图或图表(或编程博客、代码片段等)等。在该模型中,这些各异的内容类型是以序列的“块”来表示的,这些块可以重复并以任意顺序进行安排。

有关StreamField特性的更多背景知识,以及为什么要在文章主体使用StreamField,而不使用富文本字段的原因,请参阅博客文章Rich text fields and faster horses

StreamField还提供到一个丰富的,用于定义从简单的子块集合(比如由姓、名及相片组成的person),到带有自己的编辑界面的、完全定制化组件的定制块类型的API。在数据库中,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 ImageChooserPanel

class BlogPage(Page):
    author = models.CharField(max_length=255)
    date = models.DateField("发布日期")
    body = StreamField([
        ('heading', blocks.CharBlock(classname="full title")),
        ('paragragh', blocks.RichTextBlock()),
        ('image', ImageChooserBlock()),
    ])

    content_panels = Page.content_panels + [
        FieldPanel('author'),
        FieldPanel('date'),
        StreamField('body'),
    ]

注意:StreamField并不向后兼容诸如RichTextField这样的其他字段类型。如需将某个既有字段迁移到StreamField,请参考将RichTextFields迁移到StreamField

StreamField构造函数的参数,是一个(name, block_type)的元组的清单。name用于在模板与内部的JSON表示中对块类型进行标识(同时应遵循Python变量名的约定:小写字母与下划线、没有空格),而block_type就应是一个下面所讲到的块定义的对象。(此外,StreamField也可传入单个的StreamBlock实例 -- 请参阅结构化的块类型

这样就定义了可在该字段里使用的一套可用块类型。该页面的作者可自由使用这些块,以任意顺序,想用几次就用几次。

StreamField还接受可选的关键字参数blank,该参数默认为False;在其为False时,就必须为该字段提供至少一个的块,方能视为该字段有效。

基本的块类型

所有块类型,都接受一下的可选关键字参数:

  • default

    应接受到的一个新的“空”块的默认值。

  • label

    在引用到此块时,编辑器界面所显示的标签 -- 默认为该块名称的一个美化了的版本(或在莫个上下文中没有指定名称时--比如在某个listBlock里 -- 的空字符串)。

  • icon

    在可用块类型菜单用于显示该块类型的图标的名称。可通过在项目的INSTALLED_APPS中,加入wagtail.contrib.styleguide,来开启图标名称的清单,该清单的更多信息,请参阅Wagtail样式手册

  • template

    到将用于在前端上渲染此块的Django模板的路径。请参考模板渲染

  • group

    用于对此块进行分类的组,即所有有着同样组名称的块,将在编辑器界面中,以该组名称作为标题显示在一起。

Wagtail提供了以下一些基本块类型:

CharBlock

wagtail.core.blocks.CharBlock

一种单行的文本输入。接受一下关键字参数:

  • required (默认值:True

    在为True时,该字段不能留空。

  • max_length, min_length

    确保字符串至多或至少有给定的长度。

  • help_text

    显示于该字段旁边的帮助文本。

TextBlock

wagtail.core.blocks.TextBlock

一个多行的文本输入。与CharBlock一样,接受关键字参数关键字required(默认值:True)、max_lengthmin_lengthhelp_text

EmailBlock

wagtail.core.blocks.EmailBlock

一个单行的email输入,会验证email字段是一个有效的Email地址。接受关键字参数required(默认值:True)与help_text

IntegerBlock

wagtail.core.blocks.IntegerBlock

一个单行的整数输入,会验证该整数是一个有效的整数。接受关键字参数required(默认值:True)、max_valuemin_valuehelp_text

FloatBlock

wagtail.core.blocks.FloatBlock

一个单行的浮点数输入,会验证该值是一个有效的浮点数。接受关键字参数required(默认值:True)、max_valuemin_value

DecimalBlock

wagtail.core.blocks.DecimalBlock

一个单行的小数输入,会验证该整数是一个有效的小数。接受关键字参数required(默认值:True)、help_textmax_valuemin_valuemax_digitsdecimal_places

有关DecimalBlock的用例,请参阅示例:PersonBlock

RegexBlock

wagtail.core.blocks.RegexBlock

一个单行的文本输入,会将该字符串与一个正则表达式进行比对。用于验证的正则表达式,必须作为第一个参数,或一关键字参数regex进行提供。为了对用于表示验证错误的消息文本进行定制,就要将一个包含了键required(用于不显示消息)或invalid(用于在不匹配值时显示的消息)的字典,作为关键字参数error_messages加以传入。

    blocks.RegexBlock(regex=r`^[0-9]{3}$`, error_messages={
        'invalid': "不是一个有效的图书馆卡编号"
    })

接受regexhelp_textrequired(默认值:True)、max_lengthmin_lengtherror_messages关键字参数。

URLBlock

wagtail.core.blocks.URLBlock

一个单行的文本输入,会验证其字符串为一个有效的URL。接受关键字参数required(默认值:True)、max_lengthmin_lengthhelp_text

Boolean_Block

wagtail.core.blocks.BooleanBlock

一个复选框。接受关键字参数requiredhelp_text。与Django的BooleanField一样,一个required=True(默认的)值表明必须勾选该复选框才能继续。对于一个即可勾选也可不勾选的复选框,就必须显式的传入required=False

DateBlock

wagtail.core.blocks.DateBlock

一个日期选择器。接受required(默认值:True)、help_textformat关键字参数。

format(默认值:None

日期格式。该参数必须是在DATE_INPUT_FORMATS设置项中能识别的格式之一。在没有指定的该参数时,Wagtail将使用WAGTAIL_DATE_FORMAT的设置,而回滚到%Y-%m-%d的格式。

译者注 此块类型为何没有start_dateend_date这样的关键字参数呢?

TimeBlock

wagtail.core.blocks.TimeBlock

一个时间拾取器。接受关键字参数required(默认值:True)与help_text

DateTimeBlock

wagtail.core.blocks.DateTimeBlock

一个结合了日期/时间的拾取器。接受关键字参数required(默认值:True)、help_textformat

format(默认值:None

日期格式。该参数必须是在DATETIME_INPUT_FORMATS设置项中能识别的格式之一。在没有指定的该参数时,Wagtail将使用WAGTAIL_DATETIME_FORMAT的设置,而回滚到%Y-%m-%d %H:%M的格式。

RichTextBlock

wagtail.core.blocks.RichTextBlock

一个用于创建包含链接、粗体/斜体等内容的格式化文本的所见即所得的文本编辑器。接受关键字参数features,用于制定所允许的特性集合(请参阅在富文本字段中对特性进行限制)。

RawHTMLBlock

wagtail.core.blocks.RawHTMLBlock

一个用于输入原始HTML的文本编辑区域,这些原始HTML将在页面输出中,进行转义渲染。接受关键字参数required(默认值:True)、max_lengthmin_lengthhelp_text

警告 在使用此种块时,没有防止站点编辑将恶意脚本,包括那些可能在有另一名管理员查看该页面时,允许当前管理员寻求获取到管理员权限的脚本,插入到页面的机制。所以除非能够充分信任站点编辑,那么请不要使用此种块类型。

BlockQuoteBlock

wagtail.core.blocks.BlockQuoteBlock

一个文本字段,其内容将以一个HTML的<blockquote>标签对包围起来。接受关键字参数required(默认值:True)、max_lengthmin_lengthhelp_text

ChoiceBlock

wagtail.core.blocks.ChoiceBlock

一个用于从选项清单中进行选择的下拉式选择框。接受以下关键字参数:

  • choices

    一个选项清单,以所有Django模型字段的choices参数所能接受的格式;或者一个可返回此种清单的可调用元素。

  • required(默认:True

    在为True时,该字段不能留空。

  • help_text

    显示于该字段旁边的帮助文本

ChoiceBlock也可以被子类化,而生成一个有着同样的、在所有地方都用到的选项清单的可重用块。比如下面这个块的定义:

blocks.ChoiceBlock(choices=[
    ('tea': '茶'),
    ('coffee': '咖啡'),
], icon="cup")

就可以被重写为ChoiceBlock的一个子类:

class DrinksChoiceBlock(blocks.ChoiceBlock):

    choices = [
        ('tea': '茶'),
        ('coffee': '咖啡'),
    ]

    class Meta:
        icon = 'cup'

此时StreamField的那些定义就可以在完整的ChoiceBlock定义处,对DrinksChoiceBlock()加以引用了。请注意这仅在choices是一个固定清单,而非可调用元素时,才能工作。

PageChooserBlock

wagtail.core.blocks.PageChooserBlock

一个用于选择页面对象的控件,使用了Wagtail的页面浏览器(a control for selecting a page object, using Wagtail's page browser)。 接受以下关键字参数:

  • required(默认:True

    在为True时,该字段不能留空

  • target_model(默认:Page

    将选择限制到一个或更多的特定页面类型。此关键字参数接受某个页面模型类、模型名称(作为字符串),或他们的一个清单或元组。

  • can_choose_root(默认:False

    在为True时,站点编辑可将Wagtail树的根,选作页面。正常情况下这样做是不可取的,因为Wagtail树的根绝不会是一个可用的页面,但在某些特殊场合,这样做却是恰当的。比如在某个块提供了相关文章种子时,就会使用一个PageChooserBlock,来选择由哪个站点文章子板块,来作为相关文章种子来源,而树根就对应“所有板块”(for example, a block providing a feed of related articles could use a PageChooserBlock to select which subsection of the site articles will be taken from, with the root corresponding to 'everywhere')。

DocumentChooserBlock

wagtail.core.block.DocumentChooserBlock

一个允许网站编辑选择某个既有文档对象,或上传新的文档对象的控件。接受关键字参数required(默认:True)。

ImageChooserBlock

wagtail.core.blocks.ImageChooserBlock

一个允许网站编辑选择某个既有图片,或上传新的图片的控件。接受关键字参数required(默认:True)。

SnippetChooserBlock

一个允许站点编辑选取内容片段对象的控件。需要一个位置性参数:应从哪个内容片段类处选取。接受关键字参数required(默认:True)。

EmbedBlock

wagtail.core.blocks.EmbedBlock

用于站点编辑输入一个到媒体条目(比如Youtube视频)URL,而作为页面上嵌入的媒体的控件。接受关键字参数required(默认:True)、max_lengthmin_lengthhelp_text

StaticBlock

wagtail.core.blocks.StaticBlock

这是一个不带有字段的块,因此在渲染其模板时,不会传递特定的值给其模板。这在需要站点编辑插入某些任何时候都不变的内容,或无需在页面编辑器中进行配置的内容时,比如某个地址、来自第三方服务的嵌入代码,或一些在模板使用到模板标签时的更为复杂的代码等,尤为有用。

默认将在编辑器界面显示一些文本(在传入了label关键字参数时,就是该关键字参数),因此该块看起来并不是空的。但可通以关键字参数admin_text传入一个文本字符串,而对其进行整个的定制:

blocks.StaticField(
    admin_text='最新文章:无需配置。',
    # 或 admin_text=mark_safe('<b>最新文章</b>:无需配置。'),
    template='latest_posts.html'
)

StaticBlock也可以进行子类化,而生成一个带有在任何地方都可使用的某些配置的一个可重用块:

class LatestPostsStaticBlock(blocks.StaticBlock):

    class Meta:
        icon = 'user'
        lable = '最新文章'
        admin_text = '{label}: 在其他地方配置'.format(label=label)
        template = 'latest_posts.html'

结构化的块类型

Structural block types

除了上面的这些基本块类型外,定义一些由子块所构成的新的块类型也是可行的:比如由姓、名与照片构成的person块,或由不限制数量的图片块构成的一个carousel块。这类结构可以任意深度进行嵌套,从而令到某个结构包含块清单,或者结构的清单。

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类支持属性defaultlabelicontemplate,这些属性与将他们传递给该块的构造器时,有着同样的意义。

上面的代码将PersonBlock()定义为了一个可在模型定义中想重用多少次都可以的块类型。

body = StreamField([
    ('heading', blocks.CharBlock(classname="full title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
    ('author', PersonBlock()),
])

更多有关对页面编辑器中的StrucBlock的显示进行定制的选项,请参阅定制StructBlock的编辑界面

同时还可对如何将StructBlock的值加以准备,以在模板中使用而进行定制 -- 请参阅定制StructBlock的值类

ListBlock

wagtail.core.blocks.ListBlock

由许多同样类型的子块所构成的块。站点编辑可将不限数量的子块添加进来,并对其进行重新排序与删除。取子块的定义作为他的首个参数:

('ingredients_list', blocks.ListBlock(
    blocks.CharBlock(label='营养成分')
))

可将所有块类型作为子块的类型,包括结构化块类型:

('ingredients_list', blocks.ListBlock(
    blocks.StructBlock([
        ('ingredient', blocks.CharBlock()),
        ('amount', blocks.CharBlock(required=False)),
    ])
))

StreamBlock

wagtail.core.blocks.StreamBlock

一种由一系列不同类型的子块构成的快,这些子块可混合在一起,并依意愿进行重新排序。作为StreamField本身的整体机制而进行使用,也可在其他结构化块类型加以嵌套或使用。将一个(name, block_definition)元组清单,作为其首个参数:

('carousel', blocks.StreamField([
    ('image', ImageChooserBlock()),
    ('quotation', blocks.StuctBlock([
        ('text', blocks.TextBlock()),
        ('author', blocks.CharBlock()),
    ])),
    ('video', EmbedBlock()),
], icon='cogs'))

StructBlock一样,子块清单也可作为StreamBlock的子类加以提供:

class CarouselBlock(blocks.StreamBlock):

    image = blocks.ImageChooserBlock()
    quotation = blocks.StructBlock([
        ('text', blocks.TextBlock()),
        ('author', blocks.CharBlock()),
    ])
    video = EmbedBlock()

    class Meta:
        
        icon = 'cogs'

因为StreamField在块类型清单处接受了一个StreamBlock的实例作为参数,这就令到在不重复定义的情况下,重复使用一套通用的块类型成为可能(since StreamField accepts an instance of StreamBlock as a parameter, in place of a list block types, this makes it possible to re-use a common set of block types without repeating definitions):

class HomePage(Page):

    carousel = StreamField(CarouselBlock(max_num=10, block_counts={'video': {'max_num': 2}}))

StreamBlock接受以下选项,作为关键字参数或Meta的属性:

  • required(默认:True

    在为True时,就要至少提供一个子块。这在将StreamBlock作为某个StreamField的顶级块使用时被忽略;在此情况下,该StreamFieldblank属性优先。

  • min_num

    StreamBlock至少应有的子块数量。

  • max_num

    StreamBlock最多应有的子快数量。

  • block_counts

    指定各个子块类型下最小与最大数量,是以子块名称到可选的min_nummax_num字典的映射字典。

示例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表示,也为各个单独的块提供了HTML表示(StreamField provides an HTML representation for the stream content as a whole, as well as for each individual block)。而要将此HTML包含进页面中,就要使用{% include_block %} 标签。

{% raw %}

{% load wagtailcore_tags %}

...

{% include_block page.body %}

{% endraw %}

在默认的渲染中,该流的各个块是包围在<div class="block-my_block_name">元素中的(其中my_block_name就是在该StreamField定义中所给的块名称)。若要提供自己的HTML标记,可对该字段的值进行迭代,并依次在各个块调用{% include_block %}

{% raw %} ...

<article>
    {% for block in page.body %}
        <section>{% include_block block %}</section>
    {% endfor %}
</article>

{% endraw %}

为实现对特定块类型渲染的更多控制,各个块对象都提供了block_typevalue属性:

{% raw %} ...

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

{% endraw %}

默认各个块都是使用简单的、最小的HTML标记,或完全不使用HTML进行渲染的。比如CharBlock就是作为普通文本进行渲染的,而ListBlock则会将其子块输出到一个<ul>包装器中。如要用定制的HTML渲染方式来覆写此行为,可将一个template参数传递给该块,从而给到一个要进行渲染的模板文件名。这样做对于一些从StructBlock派生的定制块类型,有为有用:

('person', blocks.StructBlock(
    [
        ('first_name', blocks.CharBlock()),
        ('surname', blocks.CharBlock()),
        ('photo', ImageChooserBlock()),
        ('biography', blocks.RichTextBlock()),
    ],
    tempalte='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进行访问:

{% raw %}

{% load wagtailimages_tags %}

<div class="person">
    {% image value.photo width-400 %}
    <h2>{{ value.first_name }} {{ value.surname }}</h2>
    {{ value.biography }}
</div>

{% endraw %}

因为first_namesurnamephotobiography都是以其自己地位作为块进行定义的,所以这也可写为下面这样:

{% raw %}

{% load wagtailimages_tags wagtailcore_tags %}

<div>
    {% image value.photo width-400 %}
    <h2>{% include_block value.first_name %} {% include_block value.surname %}</h2>
    {% include_block value.biography %}
</div>

{% endraw %}

{{ myblock }} 的写法大致与 {% include_block my_block %}等价,但短的形式限制更多,因为其没有将来自所调用模板的变量,比如requestpage,加以传递;因为这个原因,只建议在一些不会渲染其自己的HTML的简单值上使用这种短的形式。比如在PersonBlock使用了如下模板时:

{% raw %}

{% load wagtailiamges_tags %}

<div class="person">
    {% image value.photo width-400 %}
    <h2>{{ value.first_name }} {{ value.surname }}</h2>

    {% if request.user.is_authenticated %}
        <a href="#">联系此人</a>
    {% endif %}

    {{ value.biography }}
</div>

{% endraw %}

那么这里的request.user.is_authenticated测试,在经由{{ ... }}这样的标签进行渲染时便不会工作:

{% raw %}

{# 错误的写法: #}

{% for block in page.body %}
    {% if block.block_type == 'person' %}
        <div>{{ block }}</div>
    {% endif %}
{% endfor %}

{# 正确的写法: #}

{% for block in page.body %}
    {% if block.block_type == 'person' %}
        <div>{% include_block block %}</div>
    {% endif %}
{% endfor %}

{% endraw %}

与Django的{% include %}标签类似,{% include_block %} 也允许通过{% include_block with foo="bar" %}语法,将额外变量传递给所包含的模板:

{% raw %}

{# 在页面模板中: #}

{% for block in page.body %}
    {% if block.block_type == 'person' %}
        {% include_block block with classname="important" %}
    {% endif %}
{% endfor %}

{# 在PersonBlock的模板中: #}
<div class="{{ classname }}"></div>

{% endraw %}

还支持 {% 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关键字参数会是可用的,且他将是一个从调用该块的模板中传递过来的变量的字典。

BoundBlocks与值

所有块类型,而不仅是StructBlock,都接受一个用于确定他们将如何在某个页面上进行渲染的template参数。但对于那些处理基本Python数据类型的块,比如CharBlockIntegerBlock,在于何处模板生效上有着一些局限,因为这些内建类型(strint等等)无法就他们的模板渲染进行“干预”。作为此问题的一个示例,请思考一下的块定义:

class HeadingBlock(blocks.CharBlock):
    class Meta:
        template = 'blocks/heading.html'

其中block/heading.html的构成是:

<h1>{{ value }}</h1>

这就给到一个与普通文本字段一样表现的块,但在其被渲染时,是将其输出封装在h1标签中的:

class BlogPage(Page):
    body = StreamField([
        # ...
        ('heading', HeadingBlock()),
        # ...
    ])

{% raw %}

{% load wagtailcore_tags %}

{% for block in page.body %}
    {% if block.block_type == 'heading' %}
        {% include_block block %} {# 此块将输出他自己的 <h1>...</h1> 标签。 #}
    {% endif %}
{% endfor %}

{% endraw %}

此种安排 -- 一个期望表示普通文本字符串,但在某个模板上有着其自己的定制HTML表示 -- 通常将是以Python达成的非常糟糕的事,不过在这里将奏效,因为在对某个StreamField进行迭代是所获取到的条目,并非这些块的真实“原生”值。相反,每个条目都是作为一个BoundBlock -- 一个表示值与值的块定义的对,的实例而加以返回的。BoundBlock通过对块定义保持跟踪,而始终知道要进行渲染的模板。而要获取到底层值 -- 在本例中,就是标题的文本内容 -- 就需要访问block.value。实际上,如在页面中输出{% include_block block.value %},将发现他是以普通文本进行渲染的,而不带有<h1>标签。

(更为准确地说,在对某个StreamField进行迭代时,其所返回的条目,是StreamChild类的实例,StreamChild类提供了block_typevalue两个属性)

有经验的Django开发者可能会发现,将这个与Django的表单框架中,表示表单字段值与其相应的表单字段定义对的BoundField类,进行比较而有所帮助,从而明白是怎样将值作为HTML表单字段进行渲染的。

大多数时候,都无需担心这些内部细节问题;Wagtail将在期望使用模板渲染的任何地方,而进行模板渲染。不过在某些此种设想并不完整的情况下 --也就是说,在访问ListBlockStructBlock的子块时。在这些情况下,就没有BoundBlock的封装器,进而其条目就无法依赖于获悉其自己的渲染模板。比如,请考虑以下设置,其中的HeadingBlockStructBlock的一个子块:

class EventBlock(blocks.StructBlock):
    heading = HeadingBlock()
    description = blocks.TextBlock()
    # ...

    class Meta:
        template = 'blocks/event.html'

blocks/event.html:

{% raw %}

{% load wagtailcore_tags %}
<div class="event {% if value.heading == "聚会!" %}lots-of-ballons{% endif %} ">
    {% include_block value.bound_blocks.heading %}
    - {% include_block value.description %}
</div>

{% endraw %}

在具体实践中,在EventBlock的模板中把<h1>标签显式地写出来,将更为自然且更具可读性:

{% raw %}

<div class="event {% if value.heading == "聚会!"%}lots-of-balloons{% endif %}">
    <h1>{{ value.heading }}</h1>
    - {% include_block value.description %}

{% endraw %}

这种局限性并不存在于作为StructBlock子块的StructBlockStreamBlock,因为Wagtail是将他们作为知悉其自己的渲染模板的复杂对象,就算在没有封装在一个BoundBlock中,而加以实现的。比如在一个StructBlock嵌套于另一个StructBlock中事:

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的模板中,将如预期的那样,从blocks/speaker.html拾取渲染模板。

总的来说,BoundBlocks 与普通值之间的互动,遵循以下规则:

1. 在对StreamFieldStreamBlock的值进行迭代时(就像在

{% raw %}

{% for block in page.body %}

{% endraw %} 中那样),将获取到一系列的BoundBlocks。

"注" 这里如写成一行,将导致gitbook 无法构建,报出错误:Template error: unexpected end of file

2. 在有着一个BoundBlock实例时,可以block.value访问到其普通值。

3. 对StructBlock子块的访问(比如在value.heading中那样),将返回一个普通值;而要获取到BoundBlock的值,就要使用value.bound_blocks.heading语法。

4. ListBlock的值,是一个普通的Python清单;对ListBlock的迭代,将返回普通的子元素值。

5. 与BoundBlock不同,StructBlockStreamBlock的值,总是知道如何去渲染他们自己的模板,就算仅有着普通值。

定制StructBlock的编辑界面

要对呈现在页面编辑器中的StructBlock的样式进行定制,可为其指定一个form_classname的属性(既可以作为StructBlock构造器的一个关键字参数,也可放在某个子类的Meta中),以覆写struct-block这个默认值:

class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock()
    biography = blocks.RichTextBlock()

    class Meta:
        icon = 'user'
        form_classname = 'person-block struct-block'

此时便可为该块提供定制的CSS了,以该指定的CSS类名称为目标,通过 insert_editor_css钩子

注意 Wagtail的编辑器样式机制,有着一些struct-block类及其他相关元素的内建样式。在制定了form_classname的值时,将覆写已经应用到StructBlock那些CSS类,因此必须记得要同时要指定struct-blockCSS类。

而对于那些需要修改HTML标记的更具扩展性的定制,则可在Meta中覆写form_template属性,以制定自己的模板路径。此种模板中支持以下这些变量:

  • children

    所有构成该StructBlock的子块的一个BoundBlocks的OrderedDict;通常StructBlock指定的模板,将调用这些OrderedDict上的render_form方法。

  • help_text

    如有制定help_text, 则为该块的帮助文本。

  • classname

    form_classname所传递的CSS类的名称(默认为struct-block)。

  • block_definition

    定义此块的StructBlock实例。

  • prefix

    该块实例的用在表单字段上的前缀,确保了在整个表单范围表单字段的唯一性。

可通过覆写块的get_form_context方法,来加入一些额外的变量:

class PersonBlock(blocks.StructBlock):

    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock()
    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_name'] = ['John', 'Paul', 'George', 'Ringo']
        return context

    class Meta:
        icon = 'user'
        form_template = 'myapp/block_forms/person.html'

StructBlock的值类进行定制

Custom value class for StructBlock

可通过指定一个value_class的属性(即可作为StructBlock构造器的一个关键字参数,也可放在某个子类的Meta中),来对StructBlock子块的值如何加以准备,而实现对StructBlock值的可用方法的定制。

而该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='链接文本', required=True)
    page = PageChoooserBlock(label='页面', required=False)
    external_url = URLBlock(label='外部URL', required=False)

    class Meta:
        icon = 'site'
        value_class = LinkStructValue

class MyPage(Page):
    quick_links = StreamField([('链接', QuickLinkBlock())], blank=True)
    quotations = StreamField([('引用'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'),
    ]

此时所扩展的值类方法,就在模板中可用了:

{% raw %}

{% load watailcore_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>

{% endraw %}

对块类型进行定制

在需要实现某个定制UI,或要处理某种Wagtail内建的块类型所未提供(且无法作为既有字段的一个结构而构建出来)的数据类型时,就要定义自己的定制块类型了。请参考Wagtail的内建块类的源代码,以获取更详细的说明。

对于那些简单地将既有Django表单字段进行封装的块类型,Wagtail提供了一个抽象类wagtail.core.blocks.FieldBlock作为助手类(a helper(class))。那些子类就只需设置一个返回该表单字段对象的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定义

StreamField definitions within migrations

就如同Django中的所有模型字段一样,所有会对StreamField造成影响的对模型定义的修改,都将造就一个包含该字段定义的“冻结”副本的数据库迁移文件。因为一个StreamField定义比一个典型的模型定义更为复杂,所以就会存在来自导入到数据库迁移的项目的增加了可能性的定义 -- 而这就会在后期这些定义被移动或删除时,导致一些问题出现(as with any model field in Django, any changed to a model definition that affect a StreamField will result in a migration file that contains a 'frozen' copy of that field definition. Since a StreamField definition is more complex that a typical model field, there is an increased likelihood of definitions from your project being imported into the migration -- which would cause problems later on if those definitions are moved or deleted)。

为消除此问题,StructBlockStreamBlock以及ChoiceBlock都实现了额外的逻辑,以确保这些块的所有子类都被解构到StructBlockStreamBlock以及ChoiceBlock的普通实例 -- 通过这种方式,数据库迁移避免了有着对定制类定义的任何引用。这之所以能做到的原因,在于这些块类型都提供了继承的标准模式,且他们知悉如何对遵循此模式的全部子类的块定义,进行重构。

在多任何其他块类,比如FieldBlock进行子类化时,都将需要在项目的生命周期保留子类的定义,或者实现一个就类而论可完全地表达块的定制结构方法,以确保子类存在。与此类似,在将某个StructBlockStreamBlockChoiceBlock的子类定制到其不再能作为基本块类型所能表达的时候 -- 比如将额外参数添加到了构造器 -- 那么就需要提供自己的deconstruct方法了。

将富文本字段迁移到StreamField

Migrating RichTextFields to StreamField

在将某个既有的RichTextField修改为StreamField,并如寻常那样创建并运行一个数据库迁移时,迁移将正确无误的完成,因为两种字段都在数据库中使用了一个文本列。但StreamField使用的是一个JSON来表示他的数据,因此现有的文本就需要使用一个数据迁移来进行转换,以令到其再度可以访问。那么StreamField就需要包含一个RichTextBlock作为其一个可用的块类型,以完成这种转换。随后该字段就可以通过创建一个新的数据库迁移(./manage.py makemigration --empty myapp),并将该迁移做如下编辑(在下面的示例中, demo.BlogPage模型的body字段,正被转换成一个带有名为rich_textRichTextBlockStreamField), 而得以转换了:

# -*- coding: utf-8 -*-
from django.db import models, migration
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 or child in page.body
                if child.block_type == 'rich_text'
            ])
            page.body = raw_text
            page.save()

class Migration(migrations.Migration):
    dependencies = [
        # 保持之前生成的数据库迁移的依赖完整!
        ('demo', '0001_initial'),
    ]

    operations = [
        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:
            # 其已经是有效的JSON了,所以保留即可
            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', 'definition non-JSON string')

    if body:
        try:
            body_data = json.loads(body)
        except ValudeError:
            # 显然其不是一个 StreamField, 所以保留即可
            pass
        else:
            raw_text = ''.join([
                child['value']['rich_text'] for child in body_data
                if child['type'] == 'rich_text'
            ])
            revision_data['body'] = raw_text
            chaned = 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)
                revison.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 = [
        # 完整保留生成的数据库迁移的依赖行
        ('demo', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(
            convert_to_streamfield,
            convert_to_richtext,
        ),
    ]