Wagtailのすすめ(4) テンプレートにコンテキストを渡そう


はじめに

Wagtailでもページをレンダリングする際にテンプレートにコンテキスト情報が渡されている.前回までに見てきたような単純な使い方では特にそれを意識する必要はなかったが,コンテキストに独自の情報を追加できれば便利なこともあるだろう.Wagtailでは,それを簡単に実現するための方法が用意されている.

今回は,ListPageクラスを追加するとともに,コンテキストに情報を追加してからページをレンダリングするための標準的な流れを押さえよう.

ページクラスの追加

今回も新しいページクラス(ListPageクラス)の定義を1つ追加する.そのため,最初に,cms/models.pyを下のように拡張しよう.

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

class TopPage(Page):
    ...

    subpage_types = ['cms.ListPage', 'cms.PlainPage']

class PlainPage(Page):
    ...

class ListPage(Page):
    cover_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )
    intro = models.CharField(max_length=255)
    main_body = RichTextField(blank=True)

    content_panels = Page.content_panels + [
        ImageChooserPanel('cover_image'),
        FieldPanel('intro'),
        FieldPanel('main_body', classname="full"),
        InlinePanel('related_pages', label="Related Pages"),
    ]

    parent_page_types = ['cms.TopPage', 'cms.ListPage']

    def get_top_page(self):
        pages = self.get_ancestors().type(TopPage)
        return pages[0]

class NavItems(Orderable):
    ...

class RelatedPages(Orderable):
    base_page = ParentalKey(Page, related_name='related_pages')
    page = models.ForeignKey(
        Page,
        on_delete=models.CASCADE,
        related_name='+'
    )
    panels = [
        PageChooserPanel('page'),
    ]

ListPageクラスとそこで用いる新しいOrderableのサブクラスRelatedPagesの定義が追加されているのがわかる.RelatedPagesは,ListPageクラスのページに表示する関連ページをリストアップしておくための道具立てである.前回取り上げたNavItemsとほぼ同じなので詳しい説明は不要だろう.

ListPageクラスの定義は,このRelatedPagesが紐付けられていることを除くとPlainPageとほぼ等しい.parent_page_typesの指定によると,このページはTopPageもしくはListPage自身の子要素として位置づけることができるようになっている.

なお,ListPageクラスの定義が追加されたことを受けて,TopPageクラスにも子要素の指定を追加した.

コンテキストへの独自情報の追加

続いて,今回の本題に入ろう.すなわち,テンプレートに渡すコンテキストに独自の情報を追加してみることにする.これはPageクラスのget_context()メソッドをオーバーライドすることによって簡単に実現することができる.ListPageを題材に,具体的な例を見てみよう.

class ListPage(Page):
    ...

    def get_breads(self):
        breads = self.get_ancestors().descendant_of(self.get_top_page(), True)
        return breads

    def get_context(self, request):
        context = super().get_context(request)
        context['top_page'] = self.get_top_page()
        context['breads'] = self.get_breads()
        context['page_list'] = [item.page for item in self.related_pages.all()]
        return context

get_context()メソッドについて説明する前に,先に簡単にget_breads()メソッドを見ておこう.これは,自分から親子関係を遡ってトップページにたどり着くまでのページをすべて取得し,逆順にトップページから並べたリストを返すメソッドになっている.これは,後で実装するパンくずリストのために利用する.

続いて,get_context()メソッドを見てほしい.スーパークラス(すなわちデフォルトのPageクラス)のget_context()メソッドを呼んだ後,それが返す辞書contextに新たな要素を追加していることがわかる.具体的には,get_top_page()メソッドで取得されるトップページ,get_breads()メソッドで得られるパンくずリスト情報,そして,このページに紐付けられているRelatedPagesのリストである.こうすることで,テンプレート側からそれらの情報に簡単にアクセスできるようになる.

実は,これらの情報は,前回のナビゲーションバーの実装で見たように,あえてコンテキストに含めなくともテンプレートからアクセスできるものである.そのため,get_context()メソッドの威力を実感しにくいかもしれない.しかし,このメソッドは,requestを引数にとっていることからもわかるように,テンプレートから直接アクセスするのが難しい情報をコンテキストに含めることもできる(次回以降にそのような例にも触れてみたいと思う).

なお,テンプレートの統一のため,TopPageクラスとPlainPageクラスにも,下のように,同様の拡張を施しておこう.

class TopPage(Page):
    ...

    def get_context(self, request):
        context = super().get_context(request)
        context['top_page'] = self.get_top_page()
        return context

class PlainPage(Page):
    ...

    def get_breads(self):
        breads = self.get_ancestors().descendant_of(self.get_top_page(), True)
        return breads

    def get_context(self, request):
        context = super().get_context(request)
        context['top_page'] = self.get_top_page()
        context['breads'] = self.get_breads()
        return context

リストページのテンプレート

続いて,新たに追加したListPageクラスのページを表示できるようにしよう.そのために,次のようなテンプレート,templates/cms/list_page.htmlを用意した.

{% extends 'cms/plain_page.html' %}
{% load wagtailcore_tags %}

{% block main_body %}
    {{ block.super }}

    {% for item_page in page_list %}
        <hr>
        <div class="my-4">
            <a href="{% pageurl item_page %}">
                <h4>{{ item_page.title }}</h4>
                {{ item_page.specific.intro }}
            </a>
            {% if item_page.last_published_at <= item_page. %}
                <p>Posted on {{ item_page.first_published_at|date }}</p>
            {% else %}
                <p>Last modified on {{ item_page.last_published_at|date }}</p>
            {% endif %}
        </div>
    {% endfor %}
    <hr>
{% endblock %}

templates/cms/plain_page.htmlを継承した上で,main_bodyブロック内でリッチテキストを表示した後にコンテキストのpage_listに含まれるページの情報を1つずつ順に表示していることがわかる.なお,first_published_atlast_published_atはデフォルトのPageクラスが持っているフィールドであり,それぞれ自動的に初公開日と最終更新日の値が入る.

この例のように,コンテキストに追加した情報の値にはそのキーを用いて直接アクセスできるようになる.したがって,templates/cms/plain_page.htmlの記述も少し単純化することができる.具体的には,page.get_top_pageとなっている箇所をすべてtop_pageに変更しておこう.

パンくずリストの実装

次に,コンテキストに追加した情報breadsを用いて,パンくずリストを実装しよう.すべてのページに表示されるように,templates/cms/plain_page.htmlの中に表示用のコードを追加する.具体的には,mainブロック内のmain_bodyブロックの直前に新たにbreadcrumbブロックを下記のように挿入した.

...
{% block main %}
    ...
    {% block breadcrumb %}
        <nav class="my-2">
            <ol class="breadcrumb">
                {% for bread in breads %}
                    <li class="breadcrumb-item">
                        <a href="{% pageurl bread %}">
                            {{ bread.title }}
                        </a>
                    </li>
                {% endfor %}
                <li class="breadcrumb-item active">
                    {{ page.title }}
                </li>
            </ol>
        </nav>
    {% endblock %}
    {% block main_body %}
        ....
    {% endblock %}
    ....
{% endblock %}
....

コンテキストからbreadsというキーでパンくずリストに表示するページのリストにアスセスし,そこから1つずつページを取り出して,そのタイトルをラベルにしたリンクを表示していることがわかる.

おわりに

今回は,ページをレンダリングする際にテンプレートに渡されるコンテキストに独自の情報を追加するための簡単な方法を紹介し,それを利用してパンくずリストを実装してみた.

次回は,最後のページクラスPostPageを追加するとともに,その中で用いるStraemFieldという便利なフィールドの使い方に慣れよう.

リンク