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

Freeform page content using StreamField

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

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

StreamField还提供到一个丰富的,用于定义从简单的子块集合(比如由姓、名及相片组成的person),到带有自己的编辑界面的、完全定制化组件的定制块类型的API。在数据库中,StreamField内容是作为JSON进行存储的,确保了该字段的全部信息内容都得以保留,而不仅是其HTML的表现形式。

使用StreamField

StreamField是一个可像所有其他字段一样,在页面模型中进行定义的模型字段:

  1. from django.db import models
  2. from wagtail.core.models import Page
  3. from wagtail.core.fields import StreamField
  4. from wagtail.core import blocks
  5. from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
  6. from wagtail.images.blocks import ImageChooserPanel
  7. class BlogPage(Page):
  8. author = models.CharField(max_length=255)
  9. date = models.DateField("发布日期")
  10. body = StreamField([
  11. ('heading', blocks.CharBlock(classname="full title")),
  12. ('paragragh', blocks.RichTextBlock()),
  13. ('image', ImageChooserBlock()),
  14. ])
  15. content_panels = Page.content_panels + [
  16. FieldPanel('author'),
  17. FieldPanel('date'),
  18. StreamField('body'),
  19. ]

注意: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加以传入。

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

接受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也可以被子类化,而生成一个有着同样的、在所有地方都用到的选项清单的可重用块。比如下面这个块的定义:

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

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

  1. class DrinksChoiceBlock(blocks.ChoiceBlock):
  2. choices = [
  3. ('tea': '茶'),
  4. ('coffee': '咖啡'),
  5. ]
  6. class Meta:
  7. 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传入一个文本字符串,而对其进行整个的定制:

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

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

  1. class LatestPostsStaticBlock(blocks.StaticBlock):
  2. class Meta:
  3. icon = 'user'
  4. lable = '最新文章'
  5. admin_text = '{label}: 在其他地方配置'.format(label=label)
  6. template = 'latest_posts.html'

结构化的块类型

Structural block types

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

StructBlock

wagtail.core.blocks.StructBlock

一个由在一起显示的子块的固定组别所构成的块。取一个(name, block_definition)的元组,作为其首个参数:

  1. ('person', blocks.StructBlock([
  2. ('first_name', blocks.CharBlock()),
  3. ('surname', blocks.CharBlock()),
  4. ('photo', ImageChooserBlock(required=False)),
  5. ('biography', blocks.RichTextBlock()),
  6. ], icon='user'))

此外,子块清单也可在某个StructBlock的子类中加以提供:

  1. class PersonBlock(blocks.StructBlock):
  2. first_name = blocks.CharBlock()
  3. surname = blocks.CharBlock()
  4. photo = ImageChooserBlock(required=False)
  5. biography = blocks.RichTextBlock()
  6. class Meta:
  7. icon = 'user'

Meta类支持属性defaultlabelicontemplate,这些属性与将他们传递给该块的构造器时,有着同样的意义。

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

  1. body = StreamField([
  2. ('heading', blocks.CharBlock(classname="full title")),
  3. ('paragraph', blocks.RichTextBlock()),
  4. ('image', ImageChooserBlock()),
  5. ('author', PersonBlock()),
  6. ])

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

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

ListBlock

wagtail.core.blocks.ListBlock

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

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

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

  1. ('ingredients_list', blocks.ListBlock(
  2. blocks.StructBlock([
  3. ('ingredient', blocks.CharBlock()),
  4. ('amount', blocks.CharBlock(required=False)),
  5. ])
  6. ))

StreamBlock

wagtail.core.blocks.StreamBlock

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

  1. ('carousel', blocks.StreamField([
  2. ('image', ImageChooserBlock()),
  3. ('quotation', blocks.StuctBlock([
  4. ('text', blocks.TextBlock()),
  5. ('author', blocks.CharBlock()),
  6. ])),
  7. ('video', EmbedBlock()),
  8. ], icon='cogs'))

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

  1. class CarouselBlock(blocks.StreamBlock):
  2. image = blocks.ImageChooserBlock()
  3. quotation = blocks.StructBlock([
  4. ('text', blocks.TextBlock()),
  5. ('author', blocks.CharBlock()),
  6. ])
  7. video = EmbedBlock()
  8. class Meta:
  9. 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):

  1. class HomePage(Page):
  2. 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的块类型中:

  1. from wagtail.core import blocks
  2. class PersonBlock(blocks.StructBlock):
  3. name = blocks.CharBlock()
  4. height = blocks.DecimalBlock()
  5. age = blocks.IntegerBlock()
  6. email = blocks.EmailBlock()
  7. class Meta:
  8. 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 %}

  1. {% load wagtailcore_tags %}
  2. ...
  3. {% include_block page.body %}

{% endraw %}

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

{% raw %} …

  1. <article>
  2. {% for block in page.body %}
  3. <section>{% include_block block %}</section>
  4. {% endfor %}
  5. </article>

{% endraw %}

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

{% raw %} …

  1. <article>
  2. {% for block in page.body %}
  3. {% if block.block_type == 'heading' %}
  4. <h1>{{ block.value }}</h1>
  5. {% else %}
  6. <section class="block-{{ block.block_type }}">
  7. {% include_block block %}
  8. </section>
  9. {% endif %}
  10. {% endfor %}
  11. </article>

{% endraw %}

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

  1. ('person', blocks.StructBlock(
  2. [
  3. ('first_name', blocks.CharBlock()),
  4. ('surname', blocks.CharBlock()),
  5. ('photo', ImageChooserBlock()),
  6. ('biography', blocks.RichTextBlock()),
  7. ],
  8. tempalte='myapp/blocks/person.html',
  9. icon='user'
  10. ))

或在将其定义为StructBlock的子类时:

  1. class PersonBlock(blocks.StructBlock):
  2. first_name = blocks.CharBlock()
  3. surname = blocks.CharBlock()
  4. photo = ImageChooserBlock(required=False)
  5. biography = blocks.RichTextBlock()
  6. class Meta:
  7. template = 'myapp/blocks/person.html'
  8. icon = 'user'

在模板中,块的值可以变量value进行访问:

{% raw %}

  1. {% load wagtailimages_tags %}
  2. <div class="person">
  3. {% image value.photo width-400 %}
  4. <h2>{{ value.first_name }} {{ value.surname }}</h2>
  5. {{ value.biography }}
  6. </div>

{% endraw %}

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

{% raw %}

  1. {% load wagtailimages_tags wagtailcore_tags %}
  2. <div>
  3. {% image value.photo width-400 %}
  4. <h2>{% include_block value.first_name %} {% include_block value.surname %}</h2>
  5. {% include_block value.biography %}
  6. </div>

{% endraw %}

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

{% raw %}

  1. {% load wagtailiamges_tags %}
  2. <div class="person">
  3. {% image value.photo width-400 %}
  4. <h2>{{ value.first_name }} {{ value.surname }}</h2>
  5. {% if request.user.is_authenticated %}
  6. <a href="#">联系此人</a>
  7. {% endif %}
  8. {{ value.biography }}
  9. </div>

{% endraw %}

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

{% raw %}

  1. {# 错误的写法: #}
  2. {% for block in page.body %}
  3. {% if block.block_type == 'person' %}
  4. <div>{{ block }}</div>
  5. {% endif %}
  6. {% endfor %}
  7. {# 正确的写法: #}
  8. {% for block in page.body %}
  9. {% if block.block_type == 'person' %}
  10. <div>{% include_block block %}</div>
  11. {% endif %}
  12. {% endfor %}

{% endraw %}

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

{% raw %}

  1. {# 在页面模板中: #}
  2. {% for block in page.body %}
  3. {% if block.block_type == 'person' %}
  4. {% include_block block with classname="important" %}
  5. {% endif %}
  6. {% endfor %}
  7. {# PersonBlock的模板中: #}
  8. <div class="{{ classname }}"></div>

{% endraw %}

还支持 {% include_block my_block with foo="bar" only %}语法,以指明除了来自父模板的foo变量外,无其他变量传递给子模板。

除了从父模板进行变量传递外,块子类也可通过对get_context方法进行重写,传递他们自己额外的模板变量:

  1. import datetime
  2. class EventBlock(blocks.StructBlock):
  3. title = blocks.CharBlock()
  4. date = blocks.DateBlock()
  5. def get_context(self, value, parent_context=None):
  6. context = super().get_context(value, parent_context=parent_context)
  7. context['is_happening_today'] = (value['date'] == datetime.date.today())
  8. return context
  9. class Meta:
  10. template = 'myapp/blocks/event.html'

在此示例中:变量is_happening_today将在该块的模板中成为可用。在该块是经由某个{% include_block%}标签进行渲染时,parent_context关键字参数会是可用的,且他将是一个从调用该块的模板中传递过来的变量的字典。

BoundBlocks与值

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

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

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

  1. <h1>{{ value }}</h1>

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

  1. class BlogPage(Page):
  2. body = StreamField([
  3. # ...
  4. ('heading', HeadingBlock()),
  5. # ...
  6. ])

{% raw %}

  1. {% load wagtailcore_tags %}
  2. {% for block in page.body %}
  3. {% if block.block_type == 'heading' %}
  4. {% include_block block %} {# 此块将输出他自己的 <h1>...</h1> 标签。 #}
  5. {% endif %}
  6. {% 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的一个子块:

  1. class EventBlock(blocks.StructBlock):
  2. heading = HeadingBlock()
  3. description = blocks.TextBlock()
  4. # ...
  5. class Meta:
  6. template = 'blocks/event.html'

blocks/event.html:

{% raw %}

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

{% endraw %}

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

{% raw %}

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

{% endraw %}

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

  1. class EventBlock(blocks.StructBlock):
  2. heading = HeadingBlock()
  3. description = blocks.TextBlock()
  4. guest_speaker = blocks.StructBlock([
  5. ('first_name', blocks.CharBlock()),
  6. ('surname', blocks.CharBlock()),
  7. ('photo', ImageChooserBlock()),
  8. ], template='blocks/speaker.html')

那么在EventBlock的模板中,将如预期的那样,从blocks/speaker.html拾取渲染模板。

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

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

{% raw %}

  1. {% 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这个默认值:

  1. class PersonBlock(blocks.StructBlock):
  2. first_name = blocks.CharBlock()
  3. surname = blocks.CharBlock()
  4. photo = ImageChooserBlock()
  5. biography = blocks.RichTextBlock()
  6. class Meta:
  7. icon = 'user'
  8. 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方法,来加入一些额外的变量:

  1. class PersonBlock(blocks.StructBlock):
  2. first_name = blocks.CharBlock()
  3. surname = blocks.CharBlock()
  4. photo = ImageChooserBlock()
  5. biography = blocks.RichTextBlock()
  6. def get_form_context(self, value, prefix='', errors=None):
  7. context = super().get_form_context(value, prefix=prefix, errors=errors)
  8. context['suggested_first_name'] = ['John', 'Paul', 'George', 'Ringo']
  9. return context
  10. class Meta:
  11. icon = 'user'
  12. 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')),访问到该子块的值。

比如:

  1. from wagtail.core.models import Page
  2. from wagtail.core.blocks import (
  3. CharBlock, PageChooserBlock, StructValue, StructBlock, TextBlock, URLBlock)
  4. class LinkStructValue(StructValue):
  5. def url(self):
  6. external_url = self.get('external_url')
  7. page = self.get('page')
  8. if external_url:
  9. return external_url
  10. elif page:
  11. return page.url
  12. class QuickLinkBlock(StructBlock):
  13. text = CharBlock(label='链接文本', required=True)
  14. page = PageChoooserBlock(label='页面', required=False)
  15. external_url = URLBlock(label='外部URL', required=False)
  16. class Meta:
  17. icon = 'site'
  18. value_class = LinkStructValue
  19. class MyPage(Page):
  20. quick_links = StreamField([('链接', QuickLinkBlock())], blank=True)
  21. quotations = StreamField([('引用' StructBlock([
  22. ('quote', TextBlock(required=True)),
  23. ('page', PageChooserBlock(required=False)),
  24. ('external_url', URLBlock(required=False)),
  25. ], icon='openquote', value_class=LinkStructValue))], blank=True)
  26. content_panels = Page.content_panels + [
  27. StreamFieldPanel('quick_links'),
  28. StreamFieldPanel('quotations'),
  29. ]

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

{% raw %}

  1. {% load watailcore_tags %}
  2. <ul>
  3. {% for link in page.quick_links %}
  4. <li><a href="{{ link.value.url }}">{{ link.value.text }}</a></li>
  5. {% endfor %}
  6. </ul>
  7. <div>
  8. {% for quotation in page.quotations %}
  9. <blockquote cite="{{ quotation.value.url }}">
  10. {{ quotation.value.quote }}
  11. </blockquote>
  12. {% endfor %}
  13. </div>

{% endraw %}

对块类型进行定制

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

对于那些简单地将既有Django表单字段进行封装的块类型,Wagtail提供了一个抽象类wagtail.core.blocks.FieldBlock作为助手类(a helper(class))。那些子类就只需设置一个返回该表单字段对象的field属性即可:

  1. class IPAddressBlock(FieldBlock):
  2. def __init__(self, required=True, help_text=None, **kwargs):
  3. self.field = forms.GenericIPAddressField(required=required, help_text=help_text)
  4. 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), 而得以转换了:

  1. # -*- coding: utf-8 -*-
  2. from django.db import models, migration
  3. from wagtail.core.rich_text import RichText
  4. def convert_to_streamfield(apps, schema_editor):
  5. BlogPage = apps.get_model("demo", "BlogPage")
  6. for page in BlogPage.objects.all():
  7. if page.body.raw_text and not page.body:
  8. page.body = [('rich_text', RichText(page.body.raw_text))]
  9. page.save()
  10. def convert_to_richtext(apps, schema_editor):
  11. BlogPage = apps.get_model("demo", "BlogPage")
  12. for page in BlogPage.objects.all()
  13. if page.body.raw_text is None:
  14. raw_text = ''.join([
  15. child.value.source or child in page.body
  16. if child.block_type == 'rich_text'
  17. ])
  18. page.body = raw_text
  19. page.save()
  20. class Migration(migrations.Migration):
  21. dependencies = [
  22. # 保持之前生成的数据库迁移的依赖完整!
  23. ('demo', '0001_initial'),
  24. ]
  25. operations = [
  26. migrations.RunPython(
  27. convert_to_streamfield,
  28. convert_to_richtext
  29. ),
  30. ]

请注意上面的数据库迁移将只在以发布的页面对象上工作。如需对草稿页面与页面修订进行迁移,就要像下面的示例那样,编辑新的数据迁移:

  1. # -*- coding: utf-8 -*-
  2. import json
  3. from django.core.serializers.json import DjangoJSONEncoder
  4. from django.db import migrations, models
  5. from wagtail.core.rich_text import RichText
  6. def page_to_streamfield(page):
  7. changed = False
  8. if page.body.raw_text and not page.body:
  9. page.body = [('rich_text', {'rich_text': RichText(page.body.raw_text)})]
  10. changed = True
  11. return page, changed
  12. def pagerevision_to_streamfield(revision_data):
  13. changed = False
  14. body = revision_data.get('body')
  15. if body:
  16. try:
  17. json.loads(body)
  18. except:
  19. ValueError:
  20. revision_data('body') = json.dumps(
  21. [{
  22. "value": {"rich_text": body},
  23. "type": "rich_text"
  24. }],
  25. cls=DjangoJSONEncoder)
  26. changed = True
  27. else:
  28. # 其已经是有效的JSON了,所以保留即可
  29. pass
  30. return revision_data, changed
  31. def page_to_richtext(page):
  32. changed = False
  33. if page.body.raw_text is None:
  34. raw_text = ''.join([
  35. child.value['rich_text'].source for child in page.body
  36. if child.block_type == 'rich_text'
  37. ])
  38. page.body = raw_text
  39. changed = True
  40. return page, changed
  41. def pagerevision_to_richtext(revision_data):
  42. changed = False
  43. body = revision_data.get('body', 'definition non-JSON string')
  44. if body:
  45. try:
  46. body_data = json.loads(body)
  47. except ValudeError:
  48. # 显然其不是一个 StreamField, 所以保留即可
  49. pass
  50. else:
  51. raw_text = ''.join([
  52. child['value']['rich_text'] for child in body_data
  53. if child['type'] == 'rich_text'
  54. ])
  55. revision_data['body'] = raw_text
  56. chaned = True
  57. return revision_data, changed
  58. def convert(apps, schema_editor, page_converter, pagerevision_converter):
  59. BlogPage = apps.get_model("demo", "BlogPage")
  60. for page in BlogPage.objects.all():
  61. page, changed = page_converter(page)
  62. if changed:
  63. page.save()
  64. for revision in page.revisions.all():
  65. revision_data = json.loads(revision.content_json)
  66. revision_data, changed = pagerevision_converter(revision_data)
  67. if changed:
  68. revision.content_json = json.dumps(revision_data, cls=DjangoJSONEncoder)
  69. revison.save()
  70. def convert_to_streamfield(apps, schema_editor):
  71. return convert(apps, schema_editor, page_to_streamfield, pagerevision_to_streamfield)
  72. def convert_to_richtext(apps, schema_editor):
  73. return convert(apps, schema_editor, page_to_richtext, pagerevision_to_richtext)
  74. class Migration(migrations.Migration):
  75. dependencies = [
  76. # 完整保留生成的数据库迁移的依赖行
  77. ('demo', '0001_initial'),
  78. ]
  79. operations = [
  80. migrations.RunPython(
  81. convert_to_streamfield,
  82. convert_to_richtext,
  83. ),
  84. ]