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

# はじめに

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

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

# ページクラスの追加

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

```python
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`を題材に，具体的な例を見てみよう．

```python
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`クラスにも，下のように，同様の拡張を施しておこう．

```python
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を用意した．

```jinja
{% 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_at`や`last_published_at`はデフォルトの`Page`クラスが持っているフィールドであり，それぞれ自動的に初公開日と最終更新日の値が入る．

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

## パンくずリストの実装

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

```jinja
...
{% 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`という便利なフィールドの使い方に慣れよう．

# リンク

- [Wagtailのすすめ(1) DjangoベースのCMSを比較・検討してWagtailを選択した話](https://qiita.com/j54854/items/96396f24fe8ce9b60d05)
- [Wagtailのすすめ(2) ページを定義し，編集し，表示してみよう](https://qiita.com/j54854/items/3dcc7f6ddf54daf1a6b6)
- [Wagtailのすすめ(3) ページのツリー構造を理解し，利用しよう](https://qiita.com/j54854/items/f8c2cd6fb02bbc36a43d)
- [Wagtailのすすめ(5) StreamFieldに独自ブロックを追加しよう](https://qiita.com/j54854/items/7e51a5f485c41d9c22ae)
- [Wagtailのすすめ(6) カテゴリとタグを追加しよう](https://qiita.com/j54854/items/111fb99eadfac6a26c29)