Wagtailでタグを実装する方法[Django]

Wagtailでタグ機能を実装する実用的な方法を解説します。

Wagtailでタグを実装する方法[Django]

ブログやメディアの記事数が多くなると、それぞれの記事のトピックに合わせてタグをつけて整理したくなります。この記事では、Wagtailでタグを実装する実践的な方法を紹介します。

Wagtailでタグを実装する基本的な方法

まずはWagtailにおける最も基本的なタグの実装方法について解説します。

Wagtailでは、タグの機能を二つのDjango向けのモジュールを利用することで実現しています。一つは、django-taggitで、Djangoにおけるタグ機能実装の最もポピュラーなライブラリになります。もう一つは、django-modelclusterで、WagtailのAdmin上でのタグの管理に必要なParentalKeyというモデルを提供しています。

WagtailのPageモデルを継承したモデルにタグをつけるには、django-taggitTagモデルとPageモデルを紐づけるためのモデルを作成する必要があります。taggit.models.TaggedItemBaseというモデルを継承した、リレーション用のモデルを作成し、django-modelclusterClusterTaggableManagerPageモデルのフィールドで紐付けします。

下記のサンプルでは、ArticlePageモデル(メディアの記事ページ)にArticleTag(記事のタグ)を紐付けしています。

# models.py

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


class ArticleTag(TaggedItemBase):
    # articlesアプリのArticlePageモデルにタグをつける
    content_object = ParentalKey('articles.ArticlePage', on_delete=models.CASCADE, related_name='tagged_items')


class ArticlePage(Page):
    ...
    tags = ClusterTaggableManager(through=ArticleTag, blank=True)

    # promote_panelsにセットすることも可能
    content_panels = Page.content_panels + [
        ...
        FieldPanel('tags'),
    ]

これで、ArticlePageのAdminにタグを入力するフィールドが表示されます。,区切りで単語を入力するとそれぞれの単語がタグとして登録できます。

wagtail管理画面でのタグの入力フォーム

タグとPageモデルはMany-to-Many(多対多)の関係性で紐づいているため、テンプレートからは以下のような形で呼び出すことができます。

# article.html

<!-- この記事ページに紐づく全てのタグを表示し、タグページへのリンクを作成する -->
{% for tag in article.tags.all %}
    <a href="/article/?tag={{ tag }}">{{ tag }}</a>
{% endfor %}

上記のリンク先に対応する、記事一覧ページでタグで絞り込みをできるようにしましょう。記事の一覧を表示するArticleIndexPageを作成し、tagパラメータがある場合にはそのタグがついた記事でフィルターをかけるようにします。

# models.py

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

        # すべてのArticlePageを取得
        articles = ArticlePage.objects.live().public()

        # tagパラメータがURLに含まれていれば、それで記事を絞り込む
        tag = request.GET.get('tag')
        if tag:
            articles = articles.filter(tags__name=tag)

        context['articles'] = articles
        return context

また、Djangoのクエリからはrelated_nameで紐づけたtagged_itemsという名前経由で、そのタグがついているArticlePageインスタンスの一覧を取得することができます。

>>> from articles.models import ArticleTag
>>> tag = ArticleTag.objects.first()
>>> tag # Wagtailという名前のタグ
<ArticleTag: Wagtail>

>>> tag.tagged_items.first() # Wagtailというタグがついた記事の最初のインスタンスを取得
<TaggedArticle: The Zen of Wagtail(日本語訳) tagged with Wagtail>

Tagモデルをカスタマイズする

上記の基本的な方法で作成したタグは、django-taggitTagモデルで管理されます。このTagモデルは、同じ方法で作成された別のモデルのTagと共通して管理されてしまうため、別のモデル用に作成したタグが、他のモデルのAdminでも入力候補として表示されてしまいます。

そのため実運用を考えると、Tagモデルをカスタマイズして利用するのがおすすめです。

独自のタグを作成するためには、taggit.models.TagBaseを継承したTagモデルを作成し、taggit.models.ItemBaseを継承したリレーション用のモデルを作成してタグ付けしたいモデルと紐付けを行います。

以下の例では、articlesアプリ内の記事モデル(ArticlePage)用にArticleTagというタグを実装しています。

# models.py

from django.db import models
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from taggit.models import TagBase, ItemBase


# ArticlePageに紐づけるタグモデル
# TagBaseを継承する
class ArticleTag(TagBase):
    class Meta:
        verbose_name = "article tag"
        verbose_name_plural = "article tags"


# リレーション用のモデル
# ItemBaseを継承する
class TaggedArticle(ItemBase):
    tag = models.ForeignKey(ArticleTag, related_name="tagged_articles", on_delete=models.CASCADE)
    content_object = ParentalKey(
        to='articles.ArticleDetailPage',
        on_delete=models.CASCADE,
        related_name='tagged_items',
    )


class ArticlePage(Page):
    ...
    # リレーション用モデルに紐付けする
    tags = ClusterTaggableManager(through='articles.TaggedArticle', blank=True)

TagをSnippetとして登録する

作成したTagはSnippetとして登録しておくと便利です。利点は大きく二つあります。

一つは、Tagの追加・削除・制限ができるようになることです。TagはAdminのフォームからデータベースに追加すると、あとからそのタグを編集・削除できないようになっています。そのため、一度記事につけたタグを削除しても、そのタグはデータベースに残ったままになり、タグ一覧の情報を取得すると削除したタグも取得してしまいます。TagをSnippetとして登録することで、Snippetの管理画面からTagを追加・削除・編集することができるようになります。

もう一つの利点は、slugをカスタマイズできるようになることです。タグ名が英語である場合には問題ないのですが、日本語のタグを作ろうとした場合にはslugがローマ字読みとして自動変換されてしまいます。例えば、『プログラミング』というタグを登録すると『puroguramingu』というslugが自動で登録されます。TagをSnippetとして登録しておくことで、Snippetの管理画面からslugも変更できるようになり、日本語のタグであっても自由なslugにすることができます。

Snippetの登録は通常の方法と同じで、register_snippetを利用します。先ほど実装したArticleTagをSnippetにしてみます。

# models.py

from taggit.models import TagBase
from wagtail.snippets.models import register_snippet


# Snippetとして登録する
@register_snippet
class ArticleTag(TagBase):
    class Meta:
        verbose_name = "article tag"
        verbose_name_plural = "article tags"

これで管理画面からタグを追加・削除・編集することができます。

タグをSnippetにする

また、デフォルトの実装だと、タグは記事ページのAdminフォームから自由につけることができますが、意図しないタグをつけてしまうことがあります。記事ページから登録できるタグを制限したい場合には、Tagモデルにfree_tagging = Falseを指定しましょう。

# models.py

from taggit.models import TagBase
from wagtail.snippets.models import register_snippet


@register_snippet
class ArticleTag(TagBase):
    free_tagging = False # 追加

    class Meta:
        verbose_name = "article tag"
        verbose_name_plural = "article tags"

こうすることで、記事ページからタグを付ける場合には、Snippetの方から登録したタグの一覧から選択しなければいけなくなります。