Freeform page content using StreamField
Wagtail的StreamField特性,提供了一个适合于那些并不遵循固定结构 -- 诸如博客文章或新闻报道 -- 的一类页面的内容编辑模型,在这样的页面中,文本可能穿插有子标题、图片、拉取引用及视频等元素。此种内容编辑模型也适合于那些更为专用的内容类型,比如地图或图表(或编程博客、代码片段等)等。在该模型中,这些各异的内容类型是以序列的“块”来表示的,这些块可以重复并以任意顺序进行安排。
有关StreamField特性的更多背景知识,以及为什么要在文章主体使用StreamField,而不使用富文本字段的原因,请参阅博客文章Rich text fields and faster horses。
StreamField还提供到一个丰富的,用于定义从简单的子块集合(比如由姓、名及相片组成的person
),到带有自己的编辑界面的、完全定制化组件的定制块类型的API。在数据库中,StreamField内容是作为JSON进行存储的,确保了该字段的全部信息内容都得以保留,而不仅是其HTML的表现形式。
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提供了以下一些基本块类型:
wagtail.core.blocks.CharBlock
一种单行的文本输入。接受一下关键字参数:
-
required
(默认值:True
)在为
True
时,该字段不能留空。 -
max_length
,min_length
确保字符串至多或至少有给定的长度。
-
help_text
显示于该字段旁边的帮助文本。
wagtail.core.blocks.TextBlock
一个多行的文本输入。与CharBlock
一样,接受关键字参数关键字required
(默认值:True
)、max_length
、min_length
与help_text
。
wagtail.core.blocks.EmailBlock
一个单行的email输入,会验证email字段是一个有效的Email地址。接受关键字参数required
(默认值:True
)与help_text
。
wagtail.core.blocks.IntegerBlock
一个单行的整数输入,会验证该整数是一个有效的整数。接受关键字参数required
(默认值:True
)、max_value
、min_value
与help_text
。
wagtail.core.blocks.FloatBlock
一个单行的浮点数输入,会验证该值是一个有效的浮点数。接受关键字参数required
(默认值:True
)、max_value
与min_value
。
wagtail.core.blocks.DecimalBlock
一个单行的小数输入,会验证该整数是一个有效的小数。接受关键字参数required
(默认值:True
)、help_text
、max_value
、min_value
、max_digits
与decimal_places
。
有关DecimalBlock
的用例,请参阅示例:PersonBlock
。
wagtail.core.blocks.RegexBlock
一个单行的文本输入,会将该字符串与一个正则表达式进行比对。用于验证的正则表达式,必须作为第一个参数,或一关键字参数regex
进行提供。为了对用于表示验证错误的消息文本进行定制,就要将一个包含了键required
(用于不显示消息)或invalid
(用于在不匹配值时显示的消息)的字典,作为关键字参数error_messages
加以传入。
blocks.RegexBlock(regex=r`^[0-9]{3}$`, error_messages={
'invalid': "不是一个有效的图书馆卡编号"
})
接受regex
、help_text
、required
(默认值:True
)、max_length
、min_length
与error_messages
关键字参数。
wagtail.core.blocks.URLBlock
一个单行的文本输入,会验证其字符串为一个有效的URL。接受关键字参数required
(默认值:True
)、max_length
、min_length
与help_text
。
wagtail.core.blocks.BooleanBlock
一个复选框。接受关键字参数required
与help_text
。与Django的BooleanField
一样,一个required=True
(默认的)值表明必须勾选该复选框才能继续。对于一个即可勾选也可不勾选的复选框,就必须显式的传入required=False
。
wagtail.core.blocks.DateBlock
一个日期选择器。接受required
(默认值:True
)、help_text
与format
关键字参数。
format
(默认值:None
)
日期格式。该参数必须是在DATE_INPUT_FORMATS
设置项中能识别的格式之一。在没有指定的该参数时,Wagtail将使用WAGTAIL_DATE_FORMAT
的设置,而回滚到%Y-%m-%d
的格式。
译者注 此块类型为何没有
start_date
、end_date
这样的关键字参数呢?
wagtail.core.blocks.TimeBlock
一个时间拾取器。接受关键字参数required
(默认值:True
)与help_text
。
wagtail.core.blocks.DateTimeBlock
一个结合了日期/时间的拾取器。接受关键字参数required
(默认值:True
)、help_text
与format
。
format
(默认值:None
)
日期格式。该参数必须是在DATETIME_INPUT_FORMATS
设置项中能识别的格式之一。在没有指定的该参数时,Wagtail将使用WAGTAIL_DATETIME_FORMAT
的设置,而回滚到%Y-%m-%d %H:%M
的格式。
wagtail.core.blocks.RichTextBlock
一个用于创建包含链接、粗体/斜体等内容的格式化文本的所见即所得的文本编辑器。接受关键字参数features
,用于制定所允许的特性集合(请参阅在富文本字段中对特性进行限制)。
wagtail.core.blocks.RawHTMLBlock
一个用于输入原始HTML的文本编辑区域,这些原始HTML将在页面输出中,进行转义渲染。接受关键字参数required
(默认值:True
)、max_length
、min_length
与help_text
。
警告 在使用此种块时,没有防止站点编辑将恶意脚本,包括那些可能在有另一名管理员查看该页面时,允许当前管理员寻求获取到管理员权限的脚本,插入到页面的机制。所以除非能够充分信任站点编辑,那么请不要使用此种块类型。
wagtail.core.blocks.BlockQuoteBlock
一个文本字段,其内容将以一个HTML的<blockquote>
标签对包围起来。接受关键字参数required
(默认值:True
)、max_length
、min_length
与help_text
。
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
是一个固定清单,而非可调用元素时,才能工作。
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 aPageChooserBlock
to select which subsection of the site articles will be taken from, with the root corresponding to 'everywhere')。
wagtail.core.block.DocumentChooserBlock
一个允许网站编辑选择某个既有文档对象,或上传新的文档对象的控件。接受关键字参数required
(默认:True
)。
wagtail.core.blocks.ImageChooserBlock
一个允许网站编辑选择某个既有图片,或上传新的图片的控件。接受关键字参数required
(默认:True
)。
一个允许站点编辑选取内容片段对象的控件。需要一个位置性参数:应从哪个内容片段类处选取。接受关键字参数required
(默认:True
)。
wagtail.core.blocks.EmbedBlock
用于站点编辑输入一个到媒体条目(比如Youtube视频)URL,而作为页面上嵌入的媒体的控件。接受关键字参数required
(默认:True
)、max_length
、min_length
与help_text
。
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
块。这类结构可以任意深度进行嵌套,从而令到某个结构包含块清单,或者结构的清单。
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(classname="full title")),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
('author', PersonBlock()),
])
更多有关对页面编辑器中的StrucBlock
的显示进行定制的选项,请参阅定制StructBlock
的编辑界面。
同时还可对如何将StructBlock
的值加以准备,以在模板中使用而进行定制 -- 请参阅定制StructBlock
的值类。
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)),
])
))
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
的顶级块使用时被忽略;在此情况下,该StreamField
的blank
属性优先。 -
min_num
该
StreamBlock
至少应有的子块数量。 -
max_num
该
StreamBlock
最多应有的子快数量。 -
block_counts
指定各个子块类型下最小与最大数量,是以子块名称到可选的
min_num
与max_num
字典的映射字典。
本示例对如何将上面讲到的基本块类型,结合到一个更为复杂的基于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_type
与value
属性:
{% 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_name
、surname
、photo
与biography
都是以其自己地位作为块进行定义的,所以这也可写为下面这样:
{% 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 %}
等价,但短的形式限制更多,因为其没有将来自所调用模板的变量,比如request
或page
,加以传递;因为这个原因,只建议在一些不会渲染其自己的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
关键字参数会是可用的,且他将是一个从调用该块的模板中传递过来的变量的字典。
所有块类型,而不仅是StructBlock
,都接受一个用于确定他们将如何在某个页面上进行渲染的template
参数。但对于那些处理基本Python数据类型的块,比如CharBlock
与IntegerBlock
,在于何处模板生效上有着一些局限,因为这些内建类型(str
、int
等等)无法就他们的模板渲染进行“干预”。作为此问题的一个示例,请思考一下的块定义:
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_type
与value
两个属性)
有经验的Django开发者可能会发现,将这个与Django的表单框架中,表示表单字段值与其相应的表单字段定义对的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
:
{% 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
子块的StructBlock
与StreamBlock
,因为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
拾取渲染模板。
总的来说,BoundBlock
s 与普通值之间的互动,遵循以下规则:
1
. 在对StreamField
或StreamBlock
的值进行迭代时(就像在
{% raw %}
{% for block in page.body %}
{% endraw %}
中那样),将获取到一系列的BoundBlock
s。
"注" 这里如写成一行,将导致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
不同,StructBlock
与StreamBlock
的值,总是知道如何去渲染他们自己的模板,就算仅有着普通值。
要对呈现在页面编辑器中的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-block
CSS类。
而对于那些需要修改HTML标记的更具扩展性的定制,则可在Meta
中覆写form_template
属性,以制定自己的模板路径。此种模板中支持以下这些变量:
-
children
所有构成该
StructBlock
的子块的一个BoundBlock
s的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'
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 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)。
为消除此问题,StructBlock
、StreamBlock
以及ChoiceBlock
都实现了额外的逻辑,以确保这些块的所有子类都被解构到StructBlock
、StreamBlock
以及ChoiceBlock
的普通实例 -- 通过这种方式,数据库迁移避免了有着对定制类定义的任何引用。这之所以能做到的原因,在于这些块类型都提供了继承的标准模式,且他们知悉如何对遵循此模式的全部子类的块定义,进行重构。
在多任何其他块类,比如FieldBlock
进行子类化时,都将需要在项目的生命周期保留子类的定义,或者实现一个就类而论可完全地表达块的定制结构方法,以确保子类存在。与此类似,在将某个StructBlock
、StreamBlock
、ChoiceBlock
的子类定制到其不再能作为基本块类型所能表达的时候 -- 比如将额外参数添加到了构造器 -- 那么就需要提供自己的deconstruct
方法了。
Migrating RichTextFields to StreamField
在将某个既有的RichTextField
修改为StreamField
,并如寻常那样创建并运行一个数据库迁移时,迁移将正确无误的完成,因为两种字段都在数据库中使用了一个文本列。但StreamField
使用的是一个JSON来表示他的数据,因此现有的文本就需要使用一个数据迁移来进行转换,以令到其再度可以访问。那么StreamField
就需要包含一个RichTextBlock
作为其一个可用的块类型,以完成这种转换。随后该字段就可以通过创建一个新的数据库迁移(./manage.py makemigration --empty myapp
),并将该迁移做如下编辑(在下面的示例中, demo.BlogPage
模型的body
字段,正被转换成一个带有名为rich_text
的RichTextBlock
的StreamField
), 而得以转换了:
# -*- 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,
),
]