【Django】お気に入り機能を実装する|簡単な掲示板アプリの作成

Djangoで掲示板アプリにお気に入り機能を実装する手順を解説します。ユーザーが投稿をお気に入り登録できるように、モデルでユーザーと投稿を関連付けます。お気に入りの追加と削除を行う処理(ビュー)を作成し、画面(テンプレート)では状態に応じてボタン表示を切り替える方法を学びます。`annotate`を使った効率的なデータ取得もポイントです。

作成日: 更新日:

開発環境

  • OS: Windows10
  • Visual Studio Code: 1.73.0
  • Python: 3.10.11
  • Django: 5.0.3

サンプルコード

/app/forms.py

/app/forms.py
1 # app/forms.py
2 from django import forms
3-from .models import Board, Comment
4+from .models import Board, Comment, Favorite
5 from django.contrib.auth.forms import UserCreationForm
6 from django.contrib.auth.models import User
7 
8 
9     class Meta:
10         model = User
11-        fields = ['username', 'email', 'password1', 'password2']
12+        fields = ['username', 'email', 'password1', 'password2']
13+
14+class FavoriteForm(forms.ModelForm):
15+    class Meta:
16+        model = Favorite
17+        fields = ['board']
18

/app/models.py

/app/models.py
1     user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='boards', null=True)
2     created_at = models.DateTimeField(auto_now_add=True)
3     updated_at = models.DateTimeField(auto_now=True)
4+    favorited_by = models.ManyToManyField(User, related_name='favorite_boards', through='Favorite')
5 
6     def __str__(self):
7         return self.title
8     updated_at = models.DateTimeField(auto_now=True)
9 
10     def __str__(self):
11-        return f'Comment by {self.user.username} on {self.board.title}'
12+        return f'Comment by {self.user.username} on {self.board.title}'
13+
14+class Favorite(models.Model):
15+    user = models.ForeignKey(User, on_delete=models.CASCADE)
16+    board = models.ForeignKey(Board, on_delete=models.CASCADE)
17+    created_at = models.DateTimeField(auto_now_add=True)
18+    updated_at = models.DateTimeField(auto_now=True)
19+
20+    class Meta:
21+        unique_together = ('user', 'board')
22

/app/urls.py

/app/urls.py
1     path('<int:board_pk>/comment/<int:comment_pk>/delete/', login_required(views.comment_delete), name='comment_delete'),
2     path('my_boards/', login_required(views.my_boards), name='my_boards'),
3     path('search/', views.board_search, name='search'),
4-    path('sort/', views.board_sort, name='sort')
5+    path('sort/', views.board_sort, name='sort'),
6+    path('add_favorite/', views.add_favorite, name='add_favorite'),
7+    path('remove_favorite/', views.remove_favorite, name='remove_favorite'),
8 ]
9

/app/views.py

/app/views.py
1 # app/views.py
2 from django.shortcuts import render, redirect, get_object_or_404
3-from .models import Board, Comment
4-from .forms import BoardForm, SignUpForm, CommentForm
5+from .models import Board, Comment, Favorite
6+from .forms import BoardForm, SignUpForm, CommentForm, FavoriteForm
7 from django.contrib.auth import logout
8 from django.contrib.auth.views import LoginView
9 from django.contrib.auth.decorators import login_required
10 from functools import wraps
11+from django.http import JsonResponse
12+from django.db.models import Count
13+from django.db import models
14 
15 def user_owns_board(view_func):
16     @wraps(view_func)
17     return wrapper
18 
19 def index(request):
20-    boards = Board.objects.all().order_by('-updated_at')
21+    user = request.user
22+
23+    if user.is_authenticated:
24+        boards = Board.objects.annotate(is_favorite=Count('favorite', filter=models.Q(favorite__user=user))).order_by('-updated_at')
25+    else:
26+        boards = Board.objects.all().order_by('-updated_at')
27     return render(request, 'index.html', {'boards': boards})
28 
29 @login_required
30     }
31     return render(request, 'index.html', context)
32 
33+@login_required
34+def add_favorite(request):
35+    if request.method == 'POST':
36+        form = FavoriteForm(request.POST)
37+        if form.is_valid():
38+            form.instance.user = request.user
39+            form.save()
40+            return redirect('index')
41+    return redirect('index')
42+
43+@login_required
44+def remove_favorite(request):
45+    if request.method == 'POST':
46+        favorite = Favorite.objects.get(user=request.user, board=request.POST.get('board'))
47+        favorite.delete()
48+        return redirect('index')
49+    return redirect('index')
50+
51 # ログインページのビュー
52 class CustomLoginView(LoginView):
53     template_name = 'registration/login.html'
54

/templates/index.html

/templates/index.html
1         <table class="table">
2             <colgroup>
3                 <col style="width: 10%">
4-                <col style="width: 50%">
5+                <col style="width: 40%">
6+                <col style="width: 10%">
7                 <col style="width: 20%">
8                 <col style="width: 20%">
9             </colgroup>
10                             {% include 'direction.html' with sort_field='title' field=sort_by direction=direction %}
11                         </a>
12                     </th>
13+                    <th scope="col">お気に入り</th>
14                     <th scope="col">
15                         投稿日時
16                         <a href="{% url 'sort' %}?sort=created_at&direction={{ next_direction|default:'asc' }}">
17                     <tr>
18                         <td scope="row">{{ board.id }}</td>
19                         <td><a href="{% url 'show' pk=board.id %}">{{ board.title }}</a></td>
20+                        <td>
21+                            {% if user.is_authenticated %}
22+                                {% if board.is_favorite %}
23+                                    <form action="{% url 'remove_favorite' %}" method="POST">
24+                                        {% csrf_token %}
25+                                        <input type="hidden" name="board" value="{{ board.id }}">
26+                                        <button type="submit" class="btn btn-primary"><i class="bi bi-clipboard-heart"></i></button>
27+                                    </form>
28+                                {% else %}
29+                                    <form action="{% url 'add_favorite' %}" method="POST">
30+                                        {% csrf_token %}
31+                                        <input type="hidden" name="board" value="{{ board.id }}">
32+                                        <button type="submit" class="btn btn-outline-primary"><i class="bi bi-clipboard-heart-fill"></i></button>
33+                                    </form>
34+                                {% endif %}
35+                            {% endif %}
36+                        </td>
37                         <td>{{ board.created_at }}</td>
38                         <td>{{ board.updated_at }}</td>
39                     </tr>
40

コード解説

変更点: お気に入り情報を管理するモデルの追加

/app/models.py
1     user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='boards', null=True)
2     created_at = models.DateTimeField(auto_now_add=True)
3     updated_at = models.DateTimeField(auto_now=True)
4+    favorited_by = models.ManyToManyField(User, related_name='favorite_boards', through='Favorite')
5 
6     def __str__(self):
7         return self.title
8     updated_at = models.DateTimeField(auto_now=True)
9 
10     def __str__(self):
11-        return f'Comment by {self.user.username} on {self.board.title}'
12+        return f'Comment by {self.user.username} on {self.board.title}'
13+
14+class Favorite(models.Model):
15+    user = models.ForeignKey(User, on_delete=models.CASCADE)
16+    board = models.ForeignKey(Board, on_delete=models.CASCADE)
17+    created_at = models.DateTimeField(auto_now_add=True)
18+    updated_at = models.DateTimeField(auto_now=True)
19+
20+    class Meta:
21+        unique_together = ('user', 'board')

お気に入り機能を実装するために、データベースの設計を変更しました。

まず、Favoriteモデルを新しく作成しました。これは、「どのユーザー(user)が、どの投稿(board)を」お気に入りしたかを記録するためのテーブルです。ForeignKeyを使って、UserモデルとBoardモデルにそれぞれ関連付けています。Metaクラス内のunique_together = ('user', 'board')という設定は、同じユーザーが同じ投稿を複数回お気に入り登録できないようにするための制約です。

次に、Boardモデルにfavorited_byというフィールドを追加しました。これはManyToManyField(多対多リレーション)で、1つの投稿が複数のユーザーにお気に入りされ、1人のユーザーが複数の投稿をお気に入りできるようにします。through='Favorite'というオプションで、この多対多の関連を先ほど作成したFavoriteモデル(中間テーブル)を使って管理することをDjangoに伝えています。これにより、board.favorited_by.all()のように、投稿をお気に入りしたユーザーの一覧を簡単に取得できるようになります。

変更点: お気に入り登録用のフォーム作成

/app/forms.py
1 # app/forms.py
2 from django import forms
3-from .models import Board, Comment
4+from .models import Board, Comment, Favorite
5 from django.contrib.auth.forms import UserCreationForm
6 from django.contrib.auth.models import User
7 
8 
9     class Meta:
10         model = User
11-        fields = ['username', 'email', 'password1', 'password2']
12+        fields = ['username', 'email', 'password1', 'password2']
13+
14+class FavoriteForm(forms.ModelForm):
15+    class Meta:
16+        model = Favorite
17+        fields = ['board']

お気に入り登録処理で使うFavoriteFormを新しく作成しました。

これはDjangoのModelFormを継承しており、先ほど作成したFavoriteモデルを元にフォームを自動生成します。fields = ['board']と指定することで、このフォームが受け取るデータは「お気に入り対象の投稿ID (board)」のみであることを示しています。お気に入りしたユーザーの情報は、後述するビューでリクエスト情報から取得するため、フォームに含める必要はありません。このフォームを使うことで、ビューでのデータバリデーション(検証)や保存処理を簡単に行うことができます。

変更点: お気に入り機能のURLを追加

/app/urls.py
1     path('<int:board_pk>/comment/<int:comment_pk>/delete/', login_required(views.comment_delete), name='comment_delete'),
2     path('my_boards/', login_required(views.my_boards), name='my_boards'),
3     path('search/', views.board_search, name='search'),
4-    path('sort/', views.board_sort, name='sort')
5+    path('sort/', views.board_sort, name='sort'),
6+    path('add_favorite/', views.add_favorite, name='add_favorite'),
7+    path('remove_favorite/', views.remove_favorite, name='remove_favorite'),
8 ]

お気に入りの「追加」と「削除」を行うためのURLを2つ追加しました。

path('add_favorite/', ...)は、お気に入り登録処理を行うビューviews.add_favoriteに接続されます。 path('remove_favorite/', ...)は、お気に入り削除処理を行うビューviews.remove_favoriteに接続されます。

それぞれにname='add_favorite'name='remove_favorite'と名前を付けているため、テンプレート(HTMLファイル)側から{% url 'add_favorite' %}のようにして、これらのURLを簡単に呼び出すことができます。

変更点: お気に入り状態を効率的に取得するビューの修正

/app/views.py
1 # app/views.py
2 from django.shortcuts import render, redirect, get_object_or_404
3-from .models import Board, Comment
4-from .forms import BoardForm, SignUpForm, CommentForm
5+from .models import Board, Comment, Favorite
6+from .forms import BoardForm, SignUpForm, CommentForm, FavoriteForm
7 from django.contrib.auth import logout
8 from django.contrib.auth.views import LoginView
9 from django.contrib.auth.decorators import login_required
10 from functools import wraps
11+from django.http import JsonResponse
12+from django.db.models import Count
13+from django.db import models
14 
15 def user_owns_board(view_func):
16     @wraps(view_func)
17     return wrapper
18 
19 def index(request):
20-    boards = Board.objects.all().order_by('-updated_at')
21+    user = request.user
22+
23+    if user.is_authenticated:
24+        boards = Board.objects.annotate(is_favorite=Count('favorite', filter=models.Q(favorite__user=user))).order_by('-updated_at')
25+    else:
26+        boards = Board.objects.all().order_by('-updated_at')
27     return render(request, 'index.html', {'boards': boards})

投稿一覧ページ(indexビュー)で、各投稿がログイン中のユーザーにお気に入りされているかどうかを判定する処理を追加しました。

ユーザーがログインしている場合(user.is_authenticated)、annotateという機能を使ってデータベースからのデータ取得を効率化しています。annotate(is_favorite=Count('favorite', filter=models.Q(favorite__user=user)))の部分が重要です。

  • annotate: データベースから取得する各投稿データに、新しい情報(ここではis_favorite)を付け加えます。
  • Count(...): 関連するFavoriteモデルのレコード数を数えます。
  • filter=models.Q(favorite__user=user): ただ数えるだけでなく、「ログイン中のユーザーによるお気に入り」に絞り込んでいます。

これにより、各投稿オブジェクトにはis_favoriteという属性が追加され、ログインユーザーがお気に入り登録していればその値は1、していなければ0になります。この方法を使うと、投稿ごとにデータベースへ問い合わせる必要がなくなり、一度のクエリで全ての情報を取得できるため、パフォーマンスが向上します。

変更点: お気に入り追加処理のビューを作成

/app/views.py
1 @login_required
2+def add_favorite(request):
3+    if request.method == 'POST':
4+        form = FavoriteForm(request.POST)
5+        if form.is_valid():
6+            form.instance.user = request.user
7+            form.save()
8+            return redirect('index')
9+    return redirect('index')

お気に入り登録を実行するためのadd_favoriteビューを新しく作成しました。この関数はログインしているユーザーのみが実行できるように@login_requiredデコレータが付いています。

処理の流れは以下の通りです。

  1. HTTPメソッドがPOST(フォームが送信されたとき)かを確認します。
  2. 送信されたデータ(request.POST)を使って、先ほど作成したFavoriteFormを初期化します。
  3. form.is_valid()で、送られてきたデータが正しいか(有効な投稿IDかなど)を検証します。
  4. 検証が通ったら、form.instance.user = request.userで、お気に入りしたユーザーが誰なのかをリクエスト情報から取得して設定します。
  5. form.save()で、Favoriteモデルの新しいレコードとしてデータベースに保存します。
  6. 最後にredirect('index')で、投稿一覧ページにリダイレクトします。

変更点: お気に入り削除処理のビューを作成

/app/views.py
1     }
2     return render(request, 'index.html', context)
3 
4+@login_required
5+def remove_favorite(request):
6+    if request.method == 'POST':
7+        favorite = Favorite.objects.get(user=request.user, board=request.POST.get('board'))
8+        favorite.delete()
9+        return redirect('index')
10+    return redirect('index')

お気に入り解除を実行するためのremove_favoriteビューを新しく作成しました。こちらも@login_requiredでログイン必須となっています。

処理の流れは以下の通りです。

  1. HTTPメソッドがPOSTかを確認します。
  2. Favorite.objects.get(...)を使って、削除対象のお気に入りレコードをデータベースから取得します。このとき、条件として「ログイン中のユーザー(user=request.user)」と「フォームから送信された投稿ID(board=request.POST.get('board'))」の両方を指定します。これにより、他人のいいねを誤って削除することを防ぎます。
  3. 取得したお気に入りオブジェクト(favorite)に対して.delete()メソッドを呼び出し、データベースからレコードを削除します。
  4. 最後にredirect('index')で、投稿一覧ページに戻ります。

変更点: テンプレートでお気に入りボタンを実装

/templates/index.html
1         <table class="table">
2             <colgroup>
3                 <col style="width: 10%">
4-                <col style="width: 50%">
5+                <col style="width: 40%">
6+                <col style="width: 10%">
7                 <col style="width: 20%">
8                 <col style="width: 20%">
9             </colgroup>
10                             {% include 'direction.html' with sort_field='title' field=sort_by direction=direction %}
11                         </a>
12                     </th>
13+                    <th scope="col">お気に入り</th>
14                     <th scope="col">
15                         投稿日時
16                         <a href="{% url 'sort' %}?sort=created_at&direction={{ next_direction|default:'asc' }}">
17                     <tr>
18                         <td scope="row">{{ board.id }}</td>
19                         <td><a href="{% url 'show' pk=board.id %}">{{ board.title }}</a></td>
20+                        <td>
21+                            {% if user.is_authenticated %}
22+                                {% if board.is_favorite %}
23+                                    <form action="{% url 'remove_favorite' %}" method="POST">
24+                                        {% csrf_token %}
25+                                        <input type="hidden" name="board" value="{{ board.id }}">
26+                                        <button type="submit" class="btn btn-primary"><i class="bi bi-clipboard-heart"></i></button>
27+                                    </form>
28+                                {% else %}
29+                                    <form action="{% url 'add_favorite' %}" method="POST">
30+                                        {% csrf_token %}
31+                                        <input type="hidden" name="board" value="{{ board.id }}">
32+                                        <button type="submit" class="btn btn-outline-primary"><i class="bi bi-clipboard-heart-fill"></i></button>
33+                                    </form>
34+                                {% endif %}
35+                            {% endif %}
36+                        </td>
37                         <td>{{ board.created_at }}</td>
38                         <td>{{ board.updated_at }}</td>
39                     </tr>

投稿一覧ページ(index.html)に、お気に入りボタンを表示する機能を追加しました。

まず、テーブルに見出しとして「お気に入り」列を追加しています。

次に、各投稿の行で、Djangoのテンプレートタグを使って条件分岐を行っています。

  • {% if user.is_authenticated %}: ユーザーがログインしている場合にのみ、ボタンを表示します。
  • {% if board.is_favorite %}: indexビューでannotateによって追加されたis_favoriteフィールドの値(1か0)をチェックします。もし1(お気に入り済み)であれば、お気に入り解除用のフォームとボタンを表示します。このフォームは/remove_favorite/にPOSTリクエストを送信します。
  • {% else %}: もし0(お気に入り未登録)であれば、お気に入り登録用のフォームとボタンを表示します。このフォームは/add_favorite/にPOSTリクエストを送信します。
  • <input type="hidden" name="board" value="{{ board.id }}">: どちらのフォームにも、どの投稿に対する操作なのかをサーバーに伝えるために、投稿IDを隠しフィールドとして含めています。
  • {% csrf_token %}: Djangoのフォームで必須の、セキュリティ対策のためのタグです。

おわりに

お疲れ様でした。今回はお気に入り機能の実装を通して、モデル・ビュー・テンプレートを連携させる一連の流れを学びました。Favoriteモデルを中間テーブルとして定義し、ManyToManyFieldでユーザーと投稿を関連付けるデータベース設計が基本となります。ビューではannotateを活用し、各投稿がお気に入り済みかの情報を効率的に取得する方法が重要なポイントでした。テンプレートではその情報をもとにifタグでボタン表示を切り替え、ユーザーの操作に応じた動的な画面を作成しました。

関連コンテンツ