前回に記事はこちら。

モデル

最初にモデルを編集します。
models.pyを下記のようにします。

Tagクラスを作成して、Postクラスに新たにTagと紐づけるカラムを追加します。

posts/models.py

class Tag(models.Model):
    title = models.CharField(max_length=255, verbose_name='タイトル')
    created_at = models.DateTimeField(auto_now_add=True, verbose_name='登録日')
    updated_at = models.DateTimeField(auto_now=True, verbose_name='更新日')
    
    class Meta:
        verbose_name = 'タグ'
        verbose_name_plural = 'タグ'

    def __str__(self):
        return self.title
    

class Post(models.Model):
    title = models.CharField(max_length=255, verbose_name='タイトル')
    body = models.TextField(verbose_name='内容')
    is_public = models.BooleanField(default=True, verbose_name='公開')
    published_at = models.DateTimeField(verbose_name='公開日')
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, verbose_name ='カテゴリ')
    tags = models.ManyToManyField(Tag, related_name='tags', blank=True, verbose_name ='タグ')

    class Meta:
        verbose_name = '投稿'
        verbose_name_plural = '投稿'

    def __str__(self):
        return self.title

下記コマンドを実行しマイグレーションファイルを作成します。

$ python manage.py makemigrations posts

posts/migrations/0003_tag_alter_category_options_post_tags.pyというファイルが生成されます。

migrateコマンドを実行しましょう。

$ python manage.py migrate

これでテーブルが作成されます。

データベースの確認

テーブルが作られているか確認してみましょう。

$ python manage.py dbshell
sqlite> .table

タグを保存するposts_tagと、関連付けするposts_post_tagsテーブルが作成されています。

sqliteを終了します。

sqlite> .quit

管理画面にタグ項目を追加

タグを登録できるように管理画面に追加します。
投稿編集画面でもタグが選択できるように修正しましょう。

posts/admin.py

from django.contrib import admin

from .models import Post, Category, Tag

# ...

class TagAdmin(admin.ModelAdmin):
    list_display  = ['title', 'created_at', 'updated_at']

admin.site.register(Tag, TagAdmin)

管理画面にタグ項目が追加されました。

管理画面にタグ項目を追加

一覧画面にも何のタグが紐付けされているかわかるように表示してみましょう。

posts/admin.py

from django.contrib import admin

    from .models import Post, Category, Tag
    
    class PostAdmin(admin.ModelAdmin):
        list_display  = ['title', 'category', 'get_tags', 'published_at']
        search_fields = ['title', 'body']

        def get_queryset(self, request):
            query = super().get_queryset(request)
            return query.prefetch_related('category', 'tags')
        
        def get_tags(self, obj):
            return ','.join([i.title for i in obj.tags.all()])
        
        get_tags.short_description = 'タグ'

タグはカテゴリーと違く複数存在するのでget_tagsというメソッドを新しくを作成して,区切りで表示するようにします。
get_tags.short_descriptionは一覧のカラム名などに使用されます。

管理画面一覧ページにタグ項目を追加

投稿の編集画面からもタグが選択できるようになっているので、タグの登録と投稿を編集してタグを紐づけてみましょう。

これで管理画面側の設定は完了です。

詳細画面にタグ名表示

次は管理画面で登録した情報をフロント画面に表示してみましょう。
タグは複数割り当てられるのでpost.tags.allを展開する形になります。

posts/templates/posts/post_detail.html

<ul>
    {% for tag in post.tags.all %}
        <li>{{ tag }}</li>
    {% endfor %}
</ul>

一覧画面にタグ名表示

管理画面と同じようにモデルに一覧表示用のメソッドを作成します。

posts/models.py

class Post(models.Model):
    # ...
    
    def tag_list(self):
        return ','.join([i.title for i in self.tags.all()])

prefetch_relatedにも忘れず追加しておきましょう。

posts/views.py

class IndexView(generic.ListView):
    paginate_by = 10

    def get_queryset(self):
        # ...
        posts = posts.order_by('-published_at').prefetch_related('category', 'tags')

        return posts

テンプレートではモデルで作成したtag_listを呼び出します。

posts/templates/posts/post_list.html

<ul>
    {% for post in post_list %}
        <li>
            <a href="{% url 'posts:detail' post.id %}">{{ post.title }}</a>
            {{ post.category }}
            {{ post.tag_list }}
        </li>
    {% endfor %}
</ul>

タグで絞り込む

タグ毎に表示できるようにしてみます。
/posts/tag/1/にアクセスするとタグIDが1の投稿一覧を表示するようにします。

ルーティング

タグ用のルーティングを設定します。

posts/views.py

urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('category/<int:category>/', views.IndexView.as_view(), name='index_category'),
    path('tag/<int:tag>/', views.IndexView.as_view(), name='index_tag'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
]

ビュー

ビューはカテゴリーと同じように増やすだけですね。

posts/views.py

from .models import Post, Category, Tag


class IndexView(generic.ListView):
    paginate_by = 10

    def get_queryset(self):
        
        posts = Post.objects.filter(is_public=True)

        # カテゴリーが指定されていたら条件追加
        if ('category' in self.kwargs):
            posts = posts.filter(category=self.kwargs['category'])

        # タグが指定されていたら条件追加
        if ('tag' in self.kwargs):
            posts = posts.filter(tags=self.kwargs['tag'])

        posts = posts.order_by('-published_at').prefetch_related('category', 'tags')

        return posts


    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        # ナビゲーション用カテゴリー
        context['categories'] = Category.objects.values('id', 'title')

        # ナビゲーション用タグ
        context['tags'] = Tag.objects.values('id', 'title')

        # タイトルの制御
        context['title'] = 'お知らせ'
        
        if ('category' in self.kwargs):
            result = next(
                item for item in context['categories']
                if item['id'] == self.kwargs['category']
            )
            context['title'] = result['title']

        if ('tag' in self.kwargs):
            result = next(
                item for item in context['tags']
                if item['id'] == self.kwargs['tag']
            )
            context['title'] = result['title']

        return context

テンプレート

ナビゲーション用にtagsを渡しているのでカテゴリーと同じように展開して完成。

posts/templates/posts/post_list.html

<ul>
    {% for tag in tags %}
        <li><a href="{% url 'posts:index_tag' tag.id %}">{{ tag.title }}</a></li>
    {% endfor %}
</ul>

関連するデータが複数あるのでその部分さえ気をつければ表示部分は一対多とほとんど同じでできますね。

GitHubはこちら。