Page 页面模型

Wagtail 每个 Page 页面模型是基于 Django modal 模型实现的。 所有页面模型都要从 wagtail.core.models.Page 类继承。

在页面模型里定义字段可以使用 Django 提供的所有字段类型,参考 Model field reference 提供的字段类型。 在这此类型的基础上,Wagtail 提供了富文本、图片、文档、流等许多扩展字段类型。

Django models(模型)

如果您对 Django models(模型) 不熟悉,首先学习一下相关的知识:

Wagtail 模型示例

下面的例子展示了一个典型的博客页面模型:

from django.db import models

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, MultiFieldPanel, InlinePanel
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.search import index


class BlogPage(Page):

    # Database fields

    body = RichTextField()
    date = models.DateField("Post date")
    feed_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )


    # Search index configuration

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


    # Editor panels configuration

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('body', classname="full"),
        InlinePanel('related_links', label="Related links"),
    ]

    promote_panels = [
        MultiFieldPanel(Page.promote_panels, "Common page configuration"),
        ImageChooserPanel('feed_image'),
    ]


    # Parent page / subpage type rules

    parent_page_types = ['blog.BlogIndex']
    subpage_types = []


class BlogPageRelatedLink(Orderable):
    page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name='related_links')
    name = models.CharField(max_length=255)
    url = models.URLField()

    panels = [
        FieldPanel('name'),
        FieldPanel('url'),
    ]

重要

在定义字段名时,字段名不要和模型名同名。否则会导致处理关系时出错 (read more)。 做为开发规约,在编写程序时最好给页面模型加上 “Page” 后缀。

编写页面模型

下面将上节定义模型的各段程序逐一进行说明。

数据库字段

每个 Wagtail 页面类型是一个 Django model, 对于数据库一个独立的表(公共字段保存在页面公共表中)。

每个页面类型定了一套自有的字段集,例如博客有正文主体和发布日期, 而事件有内容和开始、结束时间。

Wagtail 中字段除了使用 Django 的字段类外,还可以使用许多第三方扩展的字段类。

Wagtail 的核心模块提供了富文本和流的字段类型:

对于标签,Wagtail 全完支持 django-taggit 插件。

查找

search_fields 属性定义哪些字段用于做内容查找使用,以及字段的索引方式。这个属性是 SearchField 以及 FilterField 对象列表。 SearchField 增加供全文检索使用的字段,FilterField 增加供过滤使用的字段。一个字段可以使用两种方式加入到查找序列中,但每种方式只能使用一次。

在上面的例子中,使用 body 做全文搜索查找,而 date 用来做为过滤条件。哪些类型可以做为参数使用,请查看详细说明:额外字段索引

编辑面板

在后台管理界面的内容编辑页面中,定义了下面三种 Tab 页:

  • content_panels - 内容编辑项目, 例如内容主体字段

  • promote_panels - 推广元数据(metadata), 例如标签、缩略图、页面推广标题

  • settings_panels - 配置项, 例如发布日期

这几个属性的内容是 EditHandler 对象列表, 定义了界面各个 Tab 页显示项目及组合顺序。

关于 Wagtail 提供的 EditHandler 类定义,请查看 Available panel types 详细介绍。简要说来提供下面三种编辑手段:

Basic

使用下面两个类定义最基础的编辑方式,FieldPanel 根据字段类型及属性选择合适的界面组件进行编辑。 StreamField 字段用于流布局方式的区块内容编辑。

Structural

使用如下类来定义界面中一组结构化数据编辑的方式:

Chooser

对于 ForeignKey 字段,如果链接 Wagtail 的基础类型,Wagtail 基于 ChooserPanel 类提供如下一些选择弹框来供用用户选择内容项。 在图形/文档(image/document)选择框中还提供了即时上传图片或文档的功能,不需要关闭选择器就可操作。

注解

这些选择器只能在 Wagtail 页面字段链接到其它页面、图片、文档或片段时使用。

链接到其它模型类型,应使用 FieldPanel,默认使用下拉框显示选择内容。

页面编辑器定制

页面编辑器可以深度定制,详细内容请参考: Customising the editing interface

父页面 / 子页面类型限制

有两个属性可以控制网站显示的页面类型。页面类型控制主要使用在诸如”博客索引页下面只能新建博客页面”之类的需求。

下面两个属性要赋予模型类或模型名的列表。模型名的格式一般为 app_label.ModelName,如果 app_label 省略,默认在同一个应用(app)中。

  • parent_page_types 限定当前页面类型可以创建在哪些页面类型下面。

  • subpage_types 限定当前页面类型可以创建哪些子页面。

缺省情况下是不加限制。而设置 parent_page_types 为一个空的列表,表示当前页面不能创建在任何页面下面。

页面 URLs

获得页面 URL 的常用方式是使用模板的 {% pageurl %} 标签。因为是从模板中调用的,pageurl 会自动包含如下一些优化内容,更多的说明请参考 pageurl

页面模型(Page) 还提供如下几种底层的 URL 获取方式。

定制页面模型的 URL 模式

通常 Page.get_url_parts(request) 方法不会直接调用,一般在子类中复写此方法来返回页面的访问路径。此方法返回 (site_id, root_url, page_path) 格式的元组, get_urlget_full_url 都会调用此方法来获得给定页面的 URL。

get_url_parts() 的参数是 *args, **kwargs:

def get_url_parts(self, *args, **kwargs):

这些参数要传给父类的 superget_url_parts 方法,例如:

super().get_url_parts(*args, **kwargs)

也可以直传递 request 参数,但为了更好的与后来的版本兼容,还是要采用上述的方式。

方法的详细说明参见: wagtail.core.models.Page.get_url_parts().

获取页面实例的 URLs

任何时候需要获得一个页面实例的 url 时,可以调用 Page.get_url(request) 方法。当页面在访问请求站点(根据``request`` 主机名)范围内时,这个返回不带协议和域名的本地 URL。 get_url(request) 通常用在页面导航菜单等定制的模板标签中。在定制这些模板标签时,应使用 takes_context=True 参数,并使用 context.get('request') 获取请求,模板上下文没有 request 时应使用 None

方法的详细说明参见: wagtail.core.models.Page.get_url().

在需要包含协议及域名的 URL 时,请使用 Page.get_full_url(request) 方法。 request 一般不要省略,以便在一次请求中进行处理结果缓存使用。 方法的详细说明参见: wagtail.core.models.Page.get_full_url().

模板渲染

每个页面模型可以使用一个默认的页面模板。这是最简单和常用的方式。

创建页面模板

Wagtail 默认使用应用标签(app label)及模型名(model class name)来引用小写形式模板文件名,模型名中大写字母转换成下划线加小写字母。

模板格式: <app_label>/<model_name (snake cased)>.html

上例中的博客页面模型 blog.BlogPage 的模板对应于: blog/blog_page.html 文件。

这个文件根据 Django 的访问路径规则,放到可以访问的模板目录中(注意创建 blog 目录,模板目录一般不是 Python 代码的当前目录)。

模板的上下文

Wagtail 将当前页面实例赋值到页面上下文的 page 变量中。模板中可使用此变量来访问页面字段。 例如,使用 {{ page.title }} 获取页面的标题。 模板中可用的变量参考 context processors

定制模板上下文

所有页面都可以通过重写 get_context 方法将 Python 变量绑定到模板上下文的字典中:

class BlogIndexPage(Page):
    ...

    def get_context(self, request):
        context = super().get_context(request)

        # Add extra variables and return the updated context
        context['blog_entries'] = BlogPage.objects.child_of(self).live()
        return context

模板中通过 blog_entries 变量就能访问到子页面的列表了:

{{ page.title }}

{% for entry in blog_entries %}
    {{ entry.title }}
{% endfor %}

修改模板名

设置页面模型的 template 属性可以使用不同的模板文件:

class BlogPage(Page):
    ...

    template = 'other_template.html'

动态使用不同的模板文件

通过重写 get_template 方法,可在页面实例运行中动态选择不同的模板文件,这个方法在每次页面渲染时都会调用:

class BlogPage(Page):
    ...

    use_other_template = models.BooleanField()

    def get_template(self, request):
        if self.use_other_template:
            return 'blog/other_blog_page.html'

        return 'blog/blog_page.html'

在这个例子中,如果页面实例的 use_other_template 字段为真,则使用 blog/other_blog_page.html 文件做为页面模板,否则使用 blog/blog_page.html 文件做为页面模板。

Ajax 模板

通常 AJAX 调用方式只更新页面一小部分,会与网页请求方式返回不一样的格式。在这种情况下可以定义 ajax_template 属性为通过 AJAX 方式调用使用特定的模板。 (HTTP 请求中有 X-Requested-With: XMLHttpRequest 头变量):

class BlogPage(Page):
    ...

    ajax_template = 'other_template_fragment.html'
    template = 'other_template.html'

页面渲染更基础方法

在 Wagtail 页面渲染过程中,最基础的方法是 serve() ,这个方法在内部调用了 get_contextget_template 方法并完成页面渲染调用。 这个方法与 Django 视图函数类似,采用 Django Request 对象做参数,返回 Django Response 对象。

可以重写这个方法来完全控制页面的渲染过程:

For example, here’s a way to make a page respond with a JSON representation of itself:

from django.http import JsonResponse


class BlogPage(Page):
    ...

    def serve(self, request):
        return JsonResponse({
            'title': self.title,
            'body': self.body,
            'date': self.date,

            # Resizes the image to 300px width and gets a URL to it
            'feed_image': self.feed_image.get_rendition('width-300').url,
        })

内联(inline)模型

Wagtail 可以在页面中引用其它模型。这对于页面包含多个重复条目时是极其有用的,例如定义一个页面中的多个轮播图片、多个图片或附件、参考连接等。内联模型与页面内容一起纳入版本管理。

每个内联模型需要满足如下要求:

注解

django-modelcluster 和 ParentalKey

内联模型的特点请参考 django-modelclusterParentalKey 字段类型增加 import 语句如下:

from modelcluster.fields import ParentalKey

ParentalKey 是 Django ForeignKey 的子类,使用相同的调用参数。

例如,定义一个博客页面的相关链接 (名称与 URL 的列表) 内容:

from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.core.models import Orderable


class BlogPageRelatedLink(Orderable):
    page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name='related_links')
    name = models.CharField(max_length=255)
    url = models.URLField()

    panels = [
        FieldPanel('name'),
        FieldPanel('url'),
    ]

将这个模型增加到后台管理编辑页面中,在 panel 类中使用 InlinePanel 方法引用:

content_panels = [
    ...

    InlinePanel('related_links', label="Related links"),
]

方法的第一个参数应是内联模型中 ParentalKey 字段 related_name 属性定义的名称。

使用多页面模型

Wagtail 使用 Django’s multi-table inheritance 特征来实现在内容树中使用多个页面模型。

每个页面在创建时都升成一个 Wagtail 内置的 Page 模型以及一个用户自定义的模型 (例如前面创建的 BlogPage 模型)。

在 Python 代码中页面也是由两个类的实例来实现的,一个 Page 及一个自定义的页面模型实例。

在多个页面模型通用处理的场合,一般使用 Wagtail’s Page 模型实例,这种情况下,不能访问特定页面内容中的字段内容。

# Get all pages in the database
>>> from wagtail.core.models import Page
>>> Page.objects.all()
[<Page: Homepage>, <Page: About us>, <Page: Blog>, <Page: A Blog post>, <Page: Another Blog post>]

在使用指定单一页面类型的场合,可以使用 Page 类字段以及指定页面模型类的字段。

# Get all blog entries in the database
>>> BlogPage.objects.all()
[<BlogPage: A Blog post>, <BlogPage: Another Blog post>]

通过 Page 对象实例的 .specific 属性可以获得特定页面模型的实例,获取时会增加额外的数据库查询开销。

>>> page = Page.objects.get(title="A Blog post")
>>> page
<Page: A Blog post>

# Note: the blog post is an instance of Page so we cannot access body, date or feed_image

>>> page.specific
<BlogPage: A Blog post>

其他说明

使用友好的模型名

Wagtail 使用 Django 内部 Meta 类的 verbose_name 变量定义易读的页面模型名称,例如:

class HomePage(Page):
    ...

    class Meta:
        verbose_name = "主页"

这些在后台管理界面等使用页面模型名的地方会显示自定义名。例如在创建子页面时,界面会显示 verbose_name 定义的”主页”供用户选择,而不是原先的 “Home Page”。

Page 查询结果集(QuerySet)排序

基于 Page 的模型不能使用标准 Django 模型中内部 Meta 的 ordering 属性来进行排序,因为 Wagtail 是基于内容树来组织的,强制使用了 path 来排序。

class NewsItemPage(Page):
    publication_date = models.DateField()
    ...

    class Meta:
        ordering = ('-publication_date', )  # will not work

要实现页面的排序,应构造明确的查询语句来实现排序功能:

news_items = NewsItemPage.objects.live().order_by('-publication_date')

定制 Page 查询管理器

Page 类可以使用定制的 Manager。 通常定制的管理从 wagtail.core.models.PageManager 继承:

from django.db import models
from wagtail.core.models import Page, PageManager

class EventPageManager(PageManager):
    """ Custom manager for Event pages """

class EventPage(Page):
    start_date = models.DateField()

    objects = EventPageManager()

另外一个简单的创建方法是只增加一个从 wagtail.core.models.PageQuerySet 继承的``QuerySet`` 类, 然后使用 from_queryset() 引用这个类,这样就创建了一个定制的 Manager :

from django.db import models
from django.utils import timezone
from wagtail.core.models import Page, PageManager, PageQuerySet

class EventPageQuerySet(PageQuerySet):
    def future(self):
        today = timezone.localtime(timezone.now()).date()
        return self.filter(start_date__gte=today)

EventPageManager = PageManager.from_queryset(EventPageQuerySet)

class EventPage(Page):
    start_date = models.DateField()

    objects = EventPageManager()