Wagtailのすすめ(6) カテゴリとタグを追加しよう


はじめに

前回ブロクポストをイメージしたページクラスPostPageを導入したので,今回は,それにタグとカテゴリの機能を追加してみよう.これは頻出トピックで,例えば,公式ドキュメントにあるYour first Wagtail siteここのチュートリアルでも扱われている.

カテゴリモデルとタグモデルの追加

最初に,下記のように,カテゴリとタグのモデルの定義をcms/models.pyに追加し,それらをPostPageに紐付けする.

...
from modelcluster.fields import ..., ParentalManyToManyField
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase, Tag as TaggitTag

...

class PostPage(Page):
    ...
    categories = ParentalManyToManyField(
        'cms.PostCategory',
        blank=True
        )
    tags = ClusterTaggableManager(
        through='cms.PostTag',
        blank=True
        )
    ...
    content_panels = Page.content_panels + [
        ...,
        FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
        FieldPanel('tags'),
    ]
    ...

...
@register_snippet
class PostCategory(models.Model):
    name = models.CharField(max_length=255)
    panels = [
        FieldPanel('name'),
    ]
    def __str__(self):
        return self.name
    class Meta:
        verbose_name = "Category"
        verbose_name_plural = "Categories"

class PostTag(TaggedItemBase):
    content_object = ParentalKey(PostPage, related_name='tagged_posts')

@register_snippet
class Tag(TaggitTag):
    class Meta:
        proxy = True

PostCategoryクラスから見ていこう.これはカテゴリを表すためのもので,Djangoデフォルトのdjango.db.models.Modelクラスを継承し,nameという文字列だけをフィールドに保持したシンプルなクラスになっている.@register_snippetデコレータは,このクラスをスニペットとしてWagtailに登録してくれる.そして,その結果,管理サイトのスニペット編集画面でカテゴリの編集が行えるようになる.panelsの指定はその際に用いる編集パネルの設定である.

PostPageクラスに追加されたフィールドcategoriesを見ればわかるように,上で定義したカテゴリをページに多対多で紐付けする際にはParentalManyToManyFieldを用いればよい.また,content_panelsの箇所では,引数にwidget=を明示することで,チェックボックスでページとカテゴリの対応関係を指定できるようにしている.

次に,タグのモデルに目を移そう.Wagtailでは,django-taggitを用いてタグ付け機能を実装するのが標準になっている.PostTagクラスの定義やtagsフィールドのPostPageクラスへの追加の仕方がカテゴリの場合と異なっているのはそのためである(ここでは詳細は省略する).なお,ここまでの設定で,PostPageの編集画面でそのページに自由に新しいタグを追加することができるようになっているはずである .

TaggitTagという名称でインポートしたtaggit.models.TagクラスのproxyモデルとしてTagクラスを定義し,それに@register_snippetデコレータを適用しているのは,管理サイトのスニペット編集画面にカテゴリだけでなくタグも現れるようにしておくためである.これによって,スニペット編集画面を開くとそれまでに追加されたタグの一覧を確認することができるようになって便利だと思う.

この段階で管理サイトにログインし,スニペット編集画面からいくつかカテゴリを追加してみてほしい.そして,PostPageクラスのページの編集画面から,カテゴリを選択したり,タグを追加したりしてみよう.

タグとカテゴリのサイドバーへの表示

続いて,カテゴリが指定されたり,タグが追加されたりしたPostPageクラスのページを表示する際に,それらの情報がサイドバーに現れるようにしてみる.そのために,templates/cms/post_page.htmlを下のように修正した.


{% block side_bar %}
    {{ block.super }}

    <div class="card my-4">
        <h4 class="card-header">
            Categories
        </h4>
        <div class="card-body">
            {% for category in page.categories.all %}
                <a class="btn btn-outline-primary btn-sm m-1" href="{% pageurl top_page %}category/?category={{ category }}" role="button">
                    {{ category.name }}
                </a>
            {% endfor %}
        </div>
    </div>

    <div class="card my-4">
        <h4 class="card-header">
            Tags
        </h4>
        <div class="card-body">
            {% for tag in page.tags.all %}
                <a class="btn btn-outline-primary btn-sm m-1" href="{% pageurl top_page %}tag/?tag={{ tag }}" role="button">
                    {{ tag.name }}
                </a>
            {% endfor %}
        </div>
    </div>
{% endblock %}

カテゴリ,タグともにBbootstrapのカード内にボタンとして表示されるようにしてあることがわかる.なお,ボタンのリンク先は,カテゴリの場合だと,

top_pageのURL/category/?category=カテゴリの文字列

タグの場合だと,

top_pageのURL/tag/?tag=タグの文字列

となっていることがわかる.ここで,これらを引き受けるためのページとして,対応するTopPageのページの子要素としてListPageクラスのページをカテゴリ用とタグ用にそれぞれ1つずつ作成しておこう(編集画面のPROMOTEタブでSlug文字列をそれぞれcategoryおよびtagに設定しておくこと).

同一カテゴリ・タグを付与されたページリストの表示

続いて,上で作成した,ボタンのリンク先のListPageインスタンスが表示された際に,指定されたカテゴリやタグが付与されたPostPageのページが一覧表示されるように,ListPageの定義にちょとした細工を加えよう.具体的には,get_context()メソッドを下のように修正する.

class ListPage(Page):
    ...

    def get_context(self, request):
        context = super().get_context(request)
        context['top_page'] = self.get_top_page()
        context['breads'] = self.get_breads()
        if self.related_pages.count():
            context['page_list'] = [item.page for item in self.related_pages.all()]
        else:
            tag = request.GET.get('tag')
            category = request.GET.get('category')
            if category:
                context['category'] = category
                context['page_list'] = PostPage.objects.descendant_of(self.get_top_page()).filter(categories__name=category).live().order_by('-first_published_at')
            elif tag:
                context['tag'] = tag
                context['page_list'] = PostPage.objects.descendant_of(self.get_top_page()).filter(tags__name=tag).live().order_by('-first_published_at')
            else:
                context['page_list'] = self.get_children().live().order_by('-first_published_at')
        return context

if以下で,条件に応じてコンテキストに含めるpage_listを変更していることがわかる.詳細は上のコードで確認してもらえればと思うが,ポイントは,GETメソッドのクエリにcategorytagが指定されていた場合に,それが付与されているPostPageインスタンスのみを取得して投稿日の降順に並べ替えたものをpage_listとしている点である.また,指定されたカテゴリやタグの文字列自身もコンテキストに含めている.

したがって,上記のボタンを押してcategorytagのクエリ付きのURLに飛ばされると,対応するListPageがレンダリングされる前にこの修正されたget_context()メソッドが呼ばれ,page_listに,指定されたカテゴリやタグが付与されたPostPageインスタンスのみが入るので,結果としてそれらが一覧表示されることになる.

ついでに,templates/cms/list_page.htmlに下記の修正を加えて,どのようなカテゴリ,あるいはタグに対応するページが表示されているのかがわかるように,ジャンボトロンにそれらの情報を表示するようにしておこう.

{% block header_text %}
    {{ block.super }}
    {% if category %}
        <h5>Posts in category: {{ category|upper }}</h5>
    {% elif tag %}
        <h5>Posts given tag: {{ tag|upper }}</h5>
    {% endif %}
{% endblock %}

おわりに

今回はPostPageのページにカテゴリとタグの情報を追加する方法について簡単に見てきた,またその過程で,Wagtailのスニペットの扱い方にも触れた.

次回以降の予定は今のところ未定だが,興味深いトピックはまだまだ残っているので,機会があればいつかまた続編を書きたいと思う.

リンク