创建第一个 Wagtail 站点

注解

本节介绍如何搭建一个基于 Wagtail 的全新项目。 要将 Wagtail 加入到一个已有的项目,请参考 在 Django 项目中集成 Wagtail

安装并运行 Wagtail

安装环境需求

Wagtail 支持 Python 3.5, 3.6, 3.7 及 3.8 版本。如果需要在 Python 2.x 以及 Django 1.11.x 环境运行,请使用 Wagtail 1.13.4 版本,并参考相应版本帮助文档。 Wagtail 1.x 版本和 Wagtail 2.x 有较大差异,并且不能不加改动的进行升级。

查看 Python 3 版本命令:

$ python3 --version

如果版本低于 3.5, 请参考 安装 Python 3 操作说明 升级 Python 版本。

重要

在安装 Wagtail 之前, 需要先安装 libjpegzlib 操作系统库,这些库在处理 JPEG, PNG 及 GIF 图像时用到 (通过 Python Pillow 库)。 不同操作系统平台的安装方法参考 Pillow 的 不同操作系统平台安装命令.

创建并激活 Python 虚拟环境

和通常的 Python 项目一样,建议使用 Python 虚拟环境进行 Wagtail 项目开发,Python 虚拟环境的创建步骤参考:venv, Python 3 有 venv 子命令.

On Windows (cmd.exe):

$ python3 -m venv mysite\env
$ mysite\env\Scripts\activate.bat

On Unix or MacOS (bash):

$ python3 -m venv mysite/env
$ source mysite/env/bin/activate

For other shells 参考 venv 文档.

注解

如果使用版本管理工具,比如 Git,mysite 是项目目录,那么最好将 env 目录建在项目目录之外,系统库和第三方软件包不要纳入版本代码中,不同操作系统的系统库不同。

安装 Wagtail

使用 Python 自带 pip 工具来安装 Wagtail 及其依赖:

$ pip install wagtail

创建站点

Wagtail 提供 start 命令,类似于 django-admin startproject,在项目目录中运行 wagtail start mysite 将在 mysite 文件夹中创建 Wagtail 相关文件及配置:包括 一个 “home” 应用、空的 HomePage 模型及基础模板、以及一个示例 “search” (搜索)应用。

按前的操作, mysite 目录在运行 venv 步骤时已经创建, 执行 wagtail start 命令时带上目标目录的参数:

$ wagtail start mysite mysite

安装项目依赖包

$ cd mysite
$ pip install -r requirements.txt

安装 requirements.txt 中设定的依赖包,包括: Wagtail, Django, 以及其它项目运行中使用到的依赖包。

生成数据库

执行如下命令,生成数据库。在没修改项目配置的情况下,命令成功执行后会在项目目录创建一个 SQLite 数据库文件。

$ python manage.py migrate

创建超级用户

执行如下命令,创建超级用户。创建的超级用户用于登录 Wagtail 后台管理系统。

$ python manage.py createsuperuser

启动服务

$ python manage.py runserver

启用浏览器访问 http://127.0.0.1:8000 如显示网站的欢迎页,则表明前面安装步骤及配置内容正常:

Wagtail 网站欢迎界面

网站的后台管理地址为:http://127.0.0.1:8000/admin,可以使用上一步创建的超级用户登录。

后台管理界面

改进 HomePage 模型

项目工程初始创建后, 在 “home” 应用的 models.py 文件中定义了一个空白的 HomePage 模型, 生成了 homepage 模型的数据库迁移文件, 并配置 Wagtail 使用它做为网站主页。

网站开发一般在这些模板文件基础进行扩展,要增加字段内容一般经过模型扩展、数据库表更改、模板文件引用几个基础步骤。 下面介绍在 home/models.py 的 HomePage 模型中增加一个 body 字段的操作过程:

from django.db import models

from wagtail.core.models import Page
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel


class HomePage(Page):
    body = RichTextField(blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('body', classname="full"),
    ]

body 定义成 RichTextField 类型, 这个类型是 Wagtail 扩展的字段类型, 与网页上 Html 图文编辑框一致,不同之处是可以插入 Wagtail 的图片资源。 Page 类是从 Django 的 Models 继承的,所以字段可以使用 Django 核心字段类型content_panels 定义编辑界面中编辑内容时显示的字段, 可参考 创建 Page 页面更多说明。

Page 是基于 Django Model 实现的,因此页面模型字段变化后,需要运行 python manage.py makemigrations 生成迁移文件, 然后运行 python manage.py migrate 命令将模型变化更新到数据库。每次模型字段内容发生变化都需要重复执行上述命令,以免发生数据库访问错误。

重启服务后,就可以在后台管理中编辑 homepage 页面 (点击【页面】菜单, Homepage, 然后进行编辑) 。此时在编辑页面可以看到新增的 body 字段。 尝试输入一些文字,然后【发布】页面。

为了能在前端页面中看到新增的 body 字段,需要修改页面模块。 Wagtail 使用 Django 模板语言来实现页面展示。缺省情况下,展示 Wagtail 页面时 会根据 app 及 model 名来查找模板文件。模板文件名是 app名_model名.html (例如,’home’应用的 HomePage 模型对应于 home/home_page.html 模板文件)。 模板文件存放和查找顺序采用 Django 的模板文件使用方法 Django’s template rules; 习惯上模板文件存放于应用的 templates 的文件夹中。

编辑 home/templates/home/home_page.html 文件,增加下面内容:

{% extends "base.html" %}

{% load wagtailcore_tags %}

{% block body_class %}template-homepage{% endblock %}

{% block content %}
    {{ page.body|richtext }}
{% endblock %}
更新后的 homepage

Wagtail 模板标签

Wagtail 扩展了一些模板标签,参见 template tags & filters。 在需要使用 Wagtail 扩展标签时,请在模板文件顶部增加加载 wagtailcore_tags 模块的定义,如 {% load wagtailcore_tags %}

下面我们使用 richtext 过滤器来处理 RichTextField 字段,richtext 过滤器会对字段内容中的 html 标签进行必要处理:

{% load wagtailcore_tags %}
{{ page.body|richtext }}

输出的 html 内容示例::

<p>
    <b>Welcome</b> to our new site!
</p>

Note: {% load wagtailcore_tags %} 需要在每个使用 Wagtail 标签模板文件的顶部进行定义。否则 Django 会抛出 TemplateSyntaxError 的异常。

简单博客页面

了解 Wagtail 模块的基础结构以后,我们一个简单的博客页面为例继续说明复杂一些的模块如何开发设计。首先创建博客应用,运行 python manage.py startapp blog 命令在网站目录中生成 blog 应用。

blog 应用增加到 mysite/settings/base.pyINSTALLED_APPS 列表中。

Blog 索引页及发布博客页

首先我们在 blog/models.py 文件中定义一个简单的索引页页面:

from wagtail.core.models import Page
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel


class BlogIndexPage(Page):
    intro = RichTextField(blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('intro', classname="full")
    ]

运行 python manage.py makemigrationspython manage.py migrate 命令,根据新建的模型更新数据库表。

模型 BlogIndexPage , 根据映射规则缺省的页面模板名为 ``blog/templates/blog/blog_index_page.html``(这个模板文件名如有需要也可以自定义)。 创建这个文件并增加如下内容:

{% extends "base.html" %}

{% load wagtailcore_tags %}

{% block body_class %}template-blogindexpage{% endblock %}

{% block content %}
    <h1>{{ page.title }}</h1>

    <div class="intro">{{ page.intro|richtext }}</div>

    {% for post in page.get_children %}
        <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
        {{ post.specific.intro }}
        {{ post.specific.body|richtext }}
    {% endfor %}

{% endblock %}

基于 Django 模板语言和前面的介绍,文件大部分内容都已熟悉。 get_children 用于获取页面的子页面列表,将在后面介绍。 这里的 pageurl 标签与 Django 的 url 标签类似,输出页面 URL,不同之外在于它使用 Wagtail Page 对象做为参数。

进入 Wagtail 后台管理界面,在 HomePage 页面下,创建一个``BlogIndexPage`` 子页面并进行编辑。编辑页面有三个 Tab 页, 在 Promote Tab 页中,缩写(slug)字段中填写 “blog” ,然后发布页面。这样在浏览器地址栏输入网站主页 URL 并加上 /blog 就能访问 新创建的博客索引页了。缩写(slug)字段中填写的内容就是 URL 上增加的路径访问地址。

接下来,在 blog/models.py 中定义博客文章页面的模型:

from django.db import models

from wagtail.core.models import Page
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.search import index


# Keep the definition of BlogIndexPage, and add:


class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)

    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
    ]

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('intro'),
        FieldPanel('body', classname="full"),
    ]

运行 python manage.py makemigrationspython manage.py migrate 命令,根据新建的 BlogPage 模型更新数据库表。

创建模板文件 blog/templates/blog/blog_page.html:

{% extends "base.html" %}

{% load wagtailcore_tags %}

{% block body_class %}template-blogpage{% endblock %}

{% block content %}
    <h1>{{ page.title }}</h1>
    <p class="meta">{{ page.date }}</p>

    <div class="intro">{{ page.intro }}</div>

    {{ page.body|richtext }}

    <p><a href="{{ page.get_parent.url }}">Return to blog</a></p>

{% endblock %}

程序中使用了 Wagtail 的内置方法 get_parent() 获取文章所在的博客索引页,加上链接地址供返回索引页面使用。

在后台管理界面中,在 BlogIndexPage 页面下创建几个子页面,注意添加子页面时选择的页面类型为 BlogPage 页面。

创建  BlogIndex 的文章子页面
选择  BlogPost 类型

Wagtail 具备限定一个页面下可以创建子页面类型的方法,缺省时一个页面下可以增加任意类型的子页面,但这往往不太符合业务管理的需要。

Page edit screen

到此为止,我们已经完成了简单博客模块的基础程序,再次访问 /blog 网址将看到类似如下内容的界面:

Blog basics

索引页的标题列表链接到各个文章页面,而文章页面的底部有返回索引页的链接。

页面层级关系(父子页面)

简单博客模块展示了 Wagtail 的内容组织方法。在 Wagtail 页面管理时采用了树形层级关系,参见 Theory。 在简单博客的例子中,BlogIndexPage 是一个分类结点,这个结点下的各个 BlogPage 页面是叶结点。

再看一下 blog_index_page.html 页面的模板文件:

{% for post in page.get_children %}
    <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
    {{ post.specific.intro }}
    {{ post.specific.body|richtext }}
{% endfor %}

每个 Wagtail 的”页面”,可以查找父结点和子结点列表。这里我们使用 post.specific.intro 而不是 post.intro 来引用 intro 字段,原因是基于我们定义页面模型的过程,从类的声明来看:

class BlogPage(Page):

get_children() 方法返回一系列基于 Page 基类的页面列表。基类中包含”title” 之类的能家长字段,但不包括特定页面类型中定义的属性, 例如 BlogPage 中的 intro 字段。 Wagtail 提供了 specific 方法返回页面的 BlogPage 类实例。像 “title” 字段是基类 Page 模型中的字段 model 可直接引用, 而 “intro” 只存在于 BlogPage 模型,所以需要在 .specific 获取实例中引用。

如果要简化模板中引用复杂性,可以使用 Django 的 with 标签将 post.specific 赋予 post:

{% for post in page.get_children %}
    {% with post=post.specific %}
        <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
        <p>{{ post.intro }}</p>
        {{ post.body|richtext }}
    {% endwith %}
{% endfor %}

Wagtail 在 Django 的 QuerySet 基础上增强的如下方法来方便程序开发时快速处理访问页面间的层级关系:

# Given a page object 'somepage':
MyModel.objects.descendant_of(somepage) # 页面及下级结点
child_of(page) / not_child_of(somepage) # 页面直接子结点
ancestor_of(somepage) / not_ancestor_of(somepage) # 上级结点
parent_of(somepage) / not_parent_of(somepage) # 父结点 / 直接上级结点
sibling_of(somepage) / not_sibling_of(somepage) # 兄弟结点
# ... and ...
somepage.get_children()
somepage.get_ancestors()
somepage.get_descendants()
somepage.get_siblings()

详细说明可参考: Page QuerySet reference

重写模板的 Context

当前博客索引页面需要进一步实现下列需求:

  1. 博客一般是按发布时间的 倒序 来展示的

  2. 只展示 已发布 状态的内容

要解决这些问题,不是通过简单修改模板文件就能实现的,需要修改模型的查询结果。在 Wagtail 框架最佳的实现方法是重写 Page 类的 get_context() 方法。 在``BlogIndexPage`` 类中增加重写方法如下:

class BlogIndexPage(Page):
    intro = RichTextField(blank=True)

    def get_context(self, request):
        # Update context to include only published posts, ordered by reverse-chron
        context = super().get_context(request)
        blogpages = self.get_children().live().order_by('-first_published_at')
        context['blogpages'] = blogpages
        return context

方法中首先获取默认的模板上下文(Context)内容,然后根据需求创建一个定制的 QuerySet,并将其增加到已获取的 context 中。 接下来稍微改动一下 blog_index_page.html 模板文件,引用新的查询结果进行输出。

{% for post in page.get_children %} 改成 {% for post in blogpages %}

测试时从后台管理界面将一个博客文章改为未发布,然后刷新博客索引界面,改为未发布的文章将从界面中消失。并且界面的文章列表顺序也是改成最后发布的文章显示列表最前面。

图片

下面将为博客文章增加图片及图册方式展示的功能。通常图片可以插入到富文本中来展示,但为了简化向 body 之类的富文本编辑框中插入图片的复杂操作步骤,在 Wagtail 中 采用将图片或图册单独处理,利用数据库独立管理。这样做的好处是,图片的展示可以在模板文件中使用各种样式进行灵活控制,避免了图片在富文本框中不易定义样式的问题。另外, 图片独立管理可以还能方便的将图片应用到不同的场合,例如索引页中可能会使用到图片的缩图,而正文则是原图。 为了提高图片处理展示速度,Wagtail 专为图片提供了丰富的转换、缓存功能, 减化前端处理图片的复杂性。

首先将 BlogPageGalleryImage 模型增加到 models.py 文件:

from django.db import models

# New imports added for ParentalKey, Orderable, InlinePanel, ImageChooserPanel

from modelcluster.fields import ParentalKey

from wagtail.core.models import Page, Orderable
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.search import index


# ... (Keep the definition of BlogIndexPage, and update BlogPage:)


class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)

    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
    ]

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('intro'),
        FieldPanel('body', classname="full"),
        InlinePanel('gallery_images', label="Gallery images"),
    ]


class BlogPageGalleryImage(Orderable):
    page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name='gallery_images')
    image = models.ForeignKey(
        'wagtailimages.Image', on_delete=models.CASCADE, related_name='+'
    )
    caption = models.CharField(blank=True, max_length=250)

    panels = [
        ImageChooserPanel('image'),
        FieldPanel('caption'),
    ]

运行 python manage.py makemigrationspython manage.py migrate 创建更新数据库表。

下面解释程序中出现的几个新概念:

Orderable 继承将向模型中添加 sort_order 字段,这个字段用来管理图片的展示顺序。

使用 ParentalKey 关联 BlogPage 是将图片与指定的页面建立关系。 ParentalKey 类似于 Django Models 中的 ForeignKey, 但它也同时定义了 BlogPageGalleryImageBlogPage 模型 的”孩子”,是页面的一部分,也是提交、审核和历史跟踪中的一部分。

image 通过 ForeignKey 引用 Wagtail 内置模型 Image。 内置模型 Image 真正保存及管理图片,并提供 ImageChooserPanel 图片选择面板 用来在界面上上传、查询、选择图片。 通过这种方法,一个图片可以方便的供多个图册使用,在页面和图片间形成多对多的关系。

在外键上设定 on_delete=models.CASCADE 意味着图片从系统删除时,也将从图册中删除。(在某些情况下,删除图片时也可以清空图片字段的内容, 例如在团队成员页面上的成员头像字段,如果成员头像照片被删除,并不意味着要删除成员,只是清除头像字段对图片的引用,这时可以将外键参数设为: blank=True, null=True, on_delete=models.SET_NULL)。

最后,将 InlinePanel 添加到 BlogPage.content_panels 列表中,让图册可以在 BlogPage 页面上进行编辑。

完成上面程序的修改后,继续调整页面模板文件内容来显示页面中引用的图片列表:

{% extends "base.html" %}

{% load wagtailcore_tags wagtailimages_tags %}

{% block body_class %}template-blogpage{% endblock %}

{% block content %}
    <h1>{{ page.title }}</h1>
    <p class="meta">{{ page.date }}</p>

    <div class="intro">{{ page.intro }}</div>

    {{ page.body|richtext }}

    {% for item in page.gallery_images.all %}
        <div style="float: left; margin: 10px">
            {% image item.image fill-320x240 %}
            <p>{{ item.caption }}</p>
        </div>
    {% endfor %}

    <p><a href="{{ page.get_parent.url }}">Return to blog</a></p>

{% endblock %}

模板中使用 {% image %} 标签 (在 wagtailimages_tags 标签库中实现,需要在模板文件顶部导入) 插入 <img> 元素, 参数 fill-320x240 表示将图片缩放填充并剪切成 320x240 矩形。更多图片处理参数可以参考 docs。 在调用这个标签时,Wagtail 会升成 320x240 分辨率的图片文件, 并缓存到 media 以加快网络传送及显示速度。

博客示例

由于图册及图片在数据库中使用独立表或字段管理,图片可以独立于博客正文来重用。下面定义一个 main_image 方法,来返回图片中第一个图片做为主图(如果不存在将返回``None``):

class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)

    def main_image(self):
        gallery_item = self.gallery_images.first()
        if gallery_item:
            return gallery_item.image
        else:
            return None

    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
    ]

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('intro'),
        FieldPanel('body', classname="full"),
        InlinePanel('gallery_images', label="Gallery images"),
    ]

在模板中使用刚刚定义的这个方法,修改 blog_index_page.html 文件,在每个文章前显示主图的缩图:

{% load wagtailcore_tags wagtailimages_tags %}

...

{% for post in blogpages %}
    {% with post=post.specific %}
        <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>

        {% with post.main_image as main_image %}
            {% if main_image %}{% image main_image fill-160x100 %}{% endif %}
        {% endwith %}

        <p>{{ post.intro }}</p>
        {{ post.body|richtext }}
    {% endwith %}
{% endfor %}

给博客文章增加标签

为了方便读者查找、阅读某类文章,通常文章撰稿人会对文章打标签, Wagtail 可以启用标签系统,建立标签与博客文章页面的关系。 并在模板中提供标签编辑、过滤功能。最后系统还提供了按标签访问的 URL,访问后显示符合标签的文章列表。

再次修改 models.py 文件增加处理标签的相关代码:

from django.db import models

# New imports added for ClusterTaggableManager, TaggedItemBase, MultiFieldPanel

from modelcluster.fields import ParentalKey
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase

from wagtail.core.models import Page, Orderable
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, MultiFieldPanel
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.search import index


# ... (Keep the definition of BlogIndexPage)


class BlogPageTag(TaggedItemBase):
    content_object = ParentalKey(
        'BlogPage',
        related_name='tagged_items',
        on_delete=models.CASCADE
    )


class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)
    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)

    # ... (Keep the main_image method and search_fields definition)

    content_panels = Page.content_panels + [
        MultiFieldPanel([
            FieldPanel('date'),
            FieldPanel('tags'),
        ], heading="Blog information"),
        FieldPanel('intro'),
        FieldPanel('body'),
        InlinePanel('gallery_images', label="Gallery images"),
    ]

运行 python manage.py makemigrationspython manage.py migrate 创建或更新数据库表。

注意程序中新增导入 modelclustertaggit 类, 新建了 BlogPageTag 模型,并在 BlogPage 模型中增加 tags 字段。 最后我们会在 content_panels 使用 MultiFieldPanel 方法将日期、标签等字段进行集中显示。

重新启用服务后,进入后台管理界面中编辑任意一篇文章( BlogPage 类型),现在就可以在界面上为文章打标签了:

文章打标签

要在 BlogPage 页面上展示标签, 可将如下内容添加到 blog_page.html 文件中:

{% if page.tags.all.count %}
    <div class="tags">
        <h3>Tags</h3>
        {% for tag in page.tags.all %}
            <a href="{% slugurl 'tags' %}?tag={{ tag }}"><button type="button">{{ tag }}</button></a>
        {% endfor %}
    </div>
{% endif %}

注意这里使用内置的 slugurl 链接到相关页面,而不是前用到的 pageurl。不同之处在于 slugurl 使用 Page 的 slug 做为参数。 pageurl 是更常用的方法,无二义性并且无须额外的数据库查询开销。但在此例中,由于 Page 对象并未生成,所以我们使用开销更小的 slugurl 标签来升成 URL。

访问带标签的文章界面,这时底部将显示相关的标签按钮列表。这时点击按钮会显示 404 错误,因为尚未增加 “tags” 的访问视图。要解决这个问题,需要修改 models.py 文件:

class BlogTagIndexPage(Page):

    def get_context(self, request):

        # Filter by tag
        tag = request.GET.get('tag')
        blogpages = BlogPage.objects.filter(tags__name=tag)

        # Update template context
        context = super().get_context(request)
        context['blogpages'] = blogpages
        return context

注意这个页面模型并没有自已的字段,只是从 Page 继承使得请求可以使用 Wagtail 的架构进行处理。 在 get_context() 访问中可以自定义按标签查询的结果集 (QuerySet)。

在模型迁移后,在后台管理界面的 HomePage 页面下生成一个新的 BlogTagIndexPage 页面,并将 slug 定义为 “tags”。

访问 /tags 时 Django 会提示找不到 blog/blog_tag_index_page.html 文件,需要创建这个文件并增加如下代码:

{% extends "base.html" %}
{% load wagtailcore_tags %}

{% block content %}

    {% if request.GET.tag|length %}
        <h4>Showing pages tagged "{{ request.GET.tag }}"</h4>
    {% endif %}

    {% for blogpage in blogpages %}

          <p>
              <strong><a href="{% pageurl blogpage %}">{{ blogpage.title }}</a></strong><br />
              <small>Revised: {{ blogpage.latest_revision_created_at }}</small><br />
              {% if blogpage.author %}
                <p>By {{ blogpage.author.profile }}</p>
              {% endif %}
          </p>

    {% empty %}
        No pages found with that tag.
    {% endfor %}

{% endblock %}

模板中使用 Page 内置的 latest_revision_created_at 字段显示页面版本的发布时间。

文章页面还可以增加作者字段,为每个作者创建一个介绍页或者按作者过滤文章列表,这些内容类似增加标签的过程,留给读者做练习吧,在此不在多述了。

再点击文章页面的标签按钮,将看到类似如下页面:

A simple tag view

分类

本节为博客系统增加文章分类功能。和标签不一样,文章的作者可以在编辑页面上定义新标签或使用已有标签,非常灵活。而文章分类是固定的列表,列表内容由网站管理员在网站后台 界面预先定义。

首先,定义 BlogCategory 模型。分类模型不是页面模型,它有自身的管理权限,所以使用 Django 的标准 models.Model 做为基类。 Wagtail 使用”片段”概念来使用后台管理界面定义的一些参数及变量。”片段”和”页面”不同,”页面”是采用树形结构进行组织的。一个模型可以使用 @register_snippet 修饰符注册成为”片段”,页面模型中可以使用的字段类型都可以用在”片段”的模型定义中。注册为”片段”的模型通过后台管理的”片段”菜单进行交互式操作管理。 程序中我们为分类模型增加图片及名称两个字段,将如下代码添加到 blog/models.py 文件中:

from wagtail.snippets.models import register_snippet


@register_snippet
class BlogCategory(models.Model):
    name = models.CharField(max_length=255)
    icon = models.ForeignKey(
        'wagtailimages.Image', null=True, blank=True,
        on_delete=models.SET_NULL, related_name='+'
    )

    panels = [
        FieldPanel('name'),
        ImageChooserPanel('icon'),
    ]

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = 'blog categories'

注解

注意这里使用 panels 来代替页面的 content_panels 是因为”片段”模型通常不需要 URL 缩写或发布日期,所以编辑页面不使用多个 Tab 页的布局方式。 模型定义完成后,使用迁移命令同步数据库表,然后在后台管理界面的【片段】中增加一些分类记录。

下面将分类字段添加到 BlogPage 模型里, 使用 many-to-many 方式,这里使用 ParentalManyToManyField 字段类型。这种类型是 Django ManyToManyField 类型 的扩展,就像在一对多情况下 Wagtail 使用 ParentalKey 代替 ForeignKey 一样,它确保选择的对象与页面历史记录能正确匹配。

# New imports added for forms and ParentalManyToManyField
from django import forms
from django.db import models

from modelcluster.fields import ParentalKey, ParentalManyToManyField
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase

# ...

class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)
    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
    categories = ParentalManyToManyField('blog.BlogCategory', blank=True)

    # ... (Keep the main_image method and search_fields definition)

    content_panels = Page.content_panels + [
        MultiFieldPanel([
            FieldPanel('date'),
            FieldPanel('tags'),
            FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
        ], heading="Blog information"),
        FieldPanel('intro'),
        FieldPanel('body'),
        InlinePanel('gallery_images', label="Gallery images"),
    ]

程序中 categories 字段的 FieldPanel 里使用 widget 关键字定义界面展示时使用基于 checkbox 的多选模式来显示 (缺省是 select 的多选框), 在分类不多的情况下更方便用户操作。

最后修改 blog_page.html 模板以显示分类:

<h1>{{ page.title }}</h1>
<p class="meta">{{ page.date }}</p>

{% with categories=page.categories.all %}
    {% if categories %}
        <h3>Posted in:</h3>
        <ul>
            {% for category in categories %}
                <li style="display: inline">
                    {% image category.icon fill-32x32 style="vertical-align: middle" %}
                    {{ category.name }}
                </li>
            {% endfor %}
        </ul>
    {% endif %}
{% endwith %}
A blog post with categories

后续参考