采用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 modelsfrom wagtail.core.models import Pagefrom wagtail.core.fields import StreamFieldfrom wagtail.core import blocksfrom wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanelfrom wagtail.images.blocks import ImageChooserPanelclass 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_length、min_length与help_text。
EmailBlock
wagtail.core.blocks.EmailBlock
一个单行的email输入,会验证email字段是一个有效的Email地址。接受关键字参数required(默认值:True)与help_text。
IntegerBlock
wagtail.core.blocks.IntegerBlock
一个单行的整数输入,会验证该整数是一个有效的整数。接受关键字参数required(默认值:True)、max_value、min_value与help_text。
FloatBlock
wagtail.core.blocks.FloatBlock
一个单行的浮点数输入,会验证该值是一个有效的浮点数。接受关键字参数required(默认值:True)、max_value与min_value。
DecimalBlock
wagtail.core.blocks.DecimalBlock
一个单行的小数输入,会验证该整数是一个有效的小数。接受关键字参数required(默认值:True)、help_text、max_value、min_value、max_digits与decimal_places。
有关DecimalBlock的用例,请参阅示例:PersonBlock。
RegexBlock
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关键字参数。
URLBlock
wagtail.core.blocks.URLBlock
一个单行的文本输入,会验证其字符串为一个有效的URL。接受关键字参数required(默认值:True)、max_length、min_length与help_text。
Boolean_Block
wagtail.core.blocks.BooleanBlock
一个复选框。接受关键字参数required与help_text。与Django的BooleanField一样,一个required=True(默认的)值表明必须勾选该复选框才能继续。对于一个即可勾选也可不勾选的复选框,就必须显式的传入required=False。
DateBlock
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这样的关键字参数呢?
TimeBlock
wagtail.core.blocks.TimeBlock
一个时间拾取器。接受关键字参数required(默认值:True)与help_text。
DateTimeBlock
wagtail.core.blocks.DateTimeBlock
一个结合了日期/时间的拾取器。接受关键字参数required(默认值:True)、help_text与format。
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_length、min_length与help_text。
警告 在使用此种块时,没有防止站点编辑将恶意脚本,包括那些可能在有另一名管理员查看该页面时,允许当前管理员寻求获取到管理员权限的脚本,插入到页面的机制。所以除非能够充分信任站点编辑,那么请不要使用此种块类型。
BlockQuoteBlock
wagtail.core.blocks.BlockQuoteBlock
一个文本字段,其内容将以一个HTML的<blockquote>标签对包围起来。接受关键字参数required(默认值:True)、max_length、min_length与help_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 aPageChooserBlockto 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_length、min_length与help_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类支持属性default、label、icon与template,这些属性与将他们传递给该块的构造器时,有着同样的意义。
上面的代码将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的顶级块使用时被忽略;在此情况下,该StreamField的blank属性优先。min_num该
StreamBlock至少应有的子块数量。max_num该
StreamBlock最多应有的子快数量。block_counts指定各个子块类型下最小与最大数量,是以子块名称到可选的
min_num与max_num字典的映射字典。
示例PersonBlock
本示例对如何将上面讲到的基本块类型,结合到一个更为复杂的基于StructBlock的块类型中:
from wagtail.core import blocksclass 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 datetimeclass 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 contextclass Meta:template = 'myapp/blocks/event.html'
在此示例中:变量is_happening_today将在该块的模板中成为可用。在该块是经由某个{% include_block%}标签进行渲染时,parent_context关键字参数会是可用的,且他将是一个从调用该块的模板中传递过来的变量的字典。
BoundBlocks与值
所有块类型,而不仅是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拾取渲染模板。
总的来说,BoundBlocks 与普通值之间的互动,遵循以下规则:
1. 在对StreamField或StreamBlock的值进行迭代时(就像在
{% 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不同,StructBlock与StreamBlock的值,总是知道如何去渲染他们自己的模板,就算仅有着普通值。
定制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 contextclass 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 Pagefrom 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_urlelif page:return page.urlclass 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 = LinkStructValueclass 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)。
为消除此问题,StructBlock、StreamBlock以及ChoiceBlock都实现了额外的逻辑,以确保这些块的所有子类都被解构到StructBlock、StreamBlock以及ChoiceBlock的普通实例 — 通过这种方式,数据库迁移避免了有着对定制类定义的任何引用。这之所以能做到的原因,在于这些块类型都提供了继承的标准模式,且他们知悉如何对遵循此模式的全部子类的块定义,进行重构。
在多任何其他块类,比如FieldBlock进行子类化时,都将需要在项目的生命周期保留子类的定义,或者实现一个就类而论可完全地表达块的定制结构方法,以确保子类存在。与此类似,在将某个StructBlock、StreamBlock、ChoiceBlock的子类定制到其不再能作为基本块类型所能表达的时候 — 比如将额外参数添加到了构造器 — 那么就需要提供自己的deconstruct方法了。
将富文本字段迁移到StreamField
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, migrationfrom wagtail.core.rich_text import RichTextdef 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.bodyif child.block_type == 'rich_text'])page.body = raw_textpage.save()class Migration(migrations.Migration):dependencies = [# 保持之前生成的数据库迁移的依赖完整!('demo', '0001_initial'),]operations = [migrations.RunPython(convert_to_streamfield,convert_to_richtext),]
请注意上面的数据库迁移将只在以发布的页面对象上工作。如需对草稿页面与页面修订进行迁移,就要像下面的示例那样,编辑新的数据迁移:
# -*- coding: utf-8 -*-import jsonfrom django.core.serializers.json import DjangoJSONEncoderfrom django.db import migrations, modelsfrom wagtail.core.rich_text import RichTextdef page_to_streamfield(page):changed = Falseif page.body.raw_text and not page.body:page.body = [('rich_text', {'rich_text': RichText(page.body.raw_text)})]changed = Truereturn page, changeddef pagerevision_to_streamfield(revision_data):changed = Falsebody = 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 = Trueelse:# 其已经是有效的JSON了,所以保留即可passreturn revision_data, changeddef page_to_richtext(page):changed = Falseif page.body.raw_text is None:raw_text = ''.join([child.value['rich_text'].source for child in page.bodyif child.block_type == 'rich_text'])page.body = raw_textchanged = Truereturn page, changeddef pagerevision_to_richtext(revision_data):changed = Falsebody = revision_data.get('body', 'definition non-JSON string')if body:try:body_data = json.loads(body)except ValudeError:# 显然其不是一个 StreamField, 所以保留即可passelse:raw_text = ''.join([child['value']['rich_text'] for child in body_dataif child['type'] == 'rich_text'])revision_data['body'] = raw_textchaned = Truereturn revision_data, changeddef 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,),]
