【Django】コメント機能を実装する|簡単な掲示板アプリの作成

Djangoで作成した掲示板アプリにコメント機能を追加する方法を解説します。投稿とコメントを紐付けるモデルの設計、入力フォームの作成、データベースへのデータ保存と削除処理、そして自分のコメントだけ削除できる権限設定まで、Webアプリケーション開発における一連の基本的な流れを学べます。

作成日: 更新日:

開発環境

  • 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
4+from .models import Board, Comment
5 from django.contrib.auth.forms import UserCreationForm
6 from django.contrib.auth.models import User
7 
8         model = Board
9         fields = ['title', 'content']
10 
11+class CommentForm(forms.ModelForm):
12+    class Meta:
13+        model = Comment
14+        fields = ['content']
15+
16+
17 class SignUpForm(UserCreationForm):
18     email = forms.EmailField(max_length=254, help_text='Required. Inform a valid email address.')
19 
20

/app/models.py

/app/models.py
1 
2     def __str__(self):
3         return self.title
4+
5+class Comment(models.Model):
6+    board = models.ForeignKey(Board, on_delete=models.CASCADE, related_name='comments')
7+    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='comments')
8+    content = models.TextField()
9+    created_at = models.DateTimeField(auto_now_add=True)
10+    updated_at = models.DateTimeField(auto_now=True)
11+
12+    def __str__(self):
13+        return f'Comment by {self.user.username} on {self.board.title}'
14

/app/urls.py

/app/urls.py
1     path('<int:pk>/edit/', login_required(views.edit), name='edit'),
2     path('<int:pk>/update/', login_required(views.update), name='update'),
3     path('<int:pk>/delete/', login_required(views.delete), name='delete'),
4+    path('<int:pk>/comment/', login_required(views.comment_create), name='comment_create'),
5+    path('<int:board_pk>/comment/<int:comment_pk>/delete/', login_required(views.comment_delete), name='comment_delete'),
6     path('my_boards/', login_required(views.my_boards), name='my_boards'),
7 ]
8

/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
4-from .forms import BoardForm, SignUpForm
5+from .models import Board, Comment
6+from .forms import BoardForm, SignUpForm, CommentForm
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 @login_required
11 def show(request, pk):
12     board = Board.objects.get(pk=pk)
13-    return render(request, 'show.html', {'board': board})
14+    comments = Comment.objects.filter(board=pk).order_by('-created_at')
15+    comment_form = CommentForm()
16+    return render(request, 'show.html', {'board': board, 'comments': comments, 'comment_form': comment_form})
17 
18 @login_required
19 @user_owns_board
20     boards = user.boards.all()
21     return render(request, 'my_boards.html', {'boards': boards})
22 
23+@login_required
24+def comment_create(request, pk):
25+    if request.method == 'POST':
26+        comment_form = CommentForm(request.POST)
27+        if comment_form.is_valid():
28+            comment_form.instance.user = request.user
29+            comment_form.instance.board_id = pk
30+            comment_form.save()
31+    return redirect('show', pk=pk)
32+
33+@login_required
34+def comment_delete(request, board_pk, comment_pk):
35+    comment = get_object_or_404(Comment, pk=comment_pk)
36+
37+    if request.user == comment.user:
38+        comment.delete()
39+
40+    return redirect('show', pk=board_pk)
41+
42+
43 # ログインページのビュー
44 class CustomLoginView(LoginView):
45     template_name = 'registration/login.html'
46

/templates/show.html

/templates/show.html
1             </form>
2         {% endif %}
3     </section>
4+
5+    <section class="mt-3">
6+        <h3>コメントを追加する</h3>
7+        <form action="{% url 'comment_create' pk=board.pk %}" method="post">
8+            {% csrf_token %}
9+            <div class="form-group mb-3">
10+                <textarea name="content" rows="2"  class="form-control" id="id_content"></textarea>
11+            </div>
12+            <button type="submit" class="btn btn-primary">コメントする</button>
13+        </form>
14+    </section>
15+
16+    <section class="mt-3">
17+        <h3>コメント一覧</h3>
18+        <ul class="list-unstyled">
19+            {% for comment in comments %}
20+                <li class="mb-3">
21+                    <div class="card">
22+                        <div class="card-header bg-light">
23+                            <strong>{{ comment.user.username }}</strong> さん - {{ comment.created_at|date:"Y/m/d H:i" }}
24+                        </div>
25+                        <div class="card-body">
26+                            <p class="card-text">{{ comment.content }}</p>
27+                        </div>
28+                        <div class="card-footer">
29+                            {% if request.user == comment.user %}
30+                                <form action="{% url 'comment_delete' board.pk comment.pk %}" method="post">
31+                                    {% csrf_token %}
32+                                    <button type="submit" class="btn btn-danger">削除する</button>
33+                                </form>
34+                            {% endif %}
35+                        </div>
36+                    </div>
37+                </li>
38+            {% empty %}
39+                <li class="text-muted">コメントはまだありません。</li>
40+            {% endfor %}
41+        </ul>
42+    </section>
43+
44 {% endblock %}
45

コード解説

変更点: コメントを保存するためのCommentモデルの追加

/app/models.py
1+class Comment(models.Model):
2+    board = models.ForeignKey(Board, on_delete=models.CASCADE, related_name='comments')
3+    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='comments')
4+    content = models.TextField()
5+    created_at = models.DateTimeField(auto_now_add=True)
6+    updated_at = models.DateTimeField(auto_now=True)
7+
8+    def __str__(self):
9+        return f'Comment by {self.user.username} on {self.board.title}'

データベースにコメント情報を保存するための設計図として、Commentモデルを新しく作成しました。 models.Modelを継承することで、Djangoのモデルとしての機能を使えるようになります。

  • board = models.ForeignKey(Board, ...): このコメントがどの投稿に紐づくかを表すためのフィールドです。ForeignKeyは他のモデルとの関連付け(リレーションシップ)を定義するもので、ここではBoardモデル(投稿)と紐付けています。on_delete=models.CASCADEは、紐づいている投稿が削除されたら、このコメントも一緒に削除される設定です。
  • user = models.ForeignKey(User, ...): このコメントを誰が投稿したかを表すフィールドです。Django標準のUserモデルと紐付けています。
  • content = models.TextField(): コメントの本文を保存するフィールドです。長い文章も保存できます。
  • created_atupdated_at: コメントが作成された日時と更新された日時を自動で記録するためのフィールドです。

変更点: コメント入力フォームの作成

/app/forms.py
1-from .models import Board
2+from .models import Board, Comment
3 
4-class BoardForm(forms.ModelForm):
5-    ...
6+class CommentForm(forms.ModelForm):
7+    class Meta:
8+        model = Comment
9+        fields = ['content']

ユーザーがコメントを入力するためのHTMLフォームを定義するために、CommentFormというクラスを新しく作成しました。 forms.ModelFormを継承することで、先ほど定義したCommentモデルと連携したフォームを簡単に作成できます。

  • class Meta: フォームの設定を記述するための内部クラスです。
  • model = Comment: このフォームがCommentモデルに対応することを指定します。
  • fields = ['content']: フォームに表示する入力項目として、Commentモデルのcontentフィールドだけを使うように指定しています。

変更点: コメント作成・削除用のURLパターンの追加

/app/urls.py
1+    path('<int:pk>/comment/', login_required(views.comment_create), name='comment_create'),
2+    path('<int:board_pk>/comment/<int:comment_pk>/delete/', login_required(views.comment_delete), name='comment_delete'),

コメントを作成・削除する機能に対応するURLを2つ追加しました。

  • path('<int:pk>/comment/', ...): コメントを作成するためのURLです。例えば、1/comment/というURLにアクセスすると、IDが1の投稿に対してコメントを作成する処理を呼び出します。<int:pk>の部分が投稿のID番号に対応します。
  • path('<int:board_pk>/comment/<int:comment_pk>/delete/', ...): コメントを削除するためのURLです。例えば、1/comment/5/delete/というURLにアクセスすると、IDが1の投稿にあるIDが5のコメントを削除する処理を呼び出します。<int:board_pk>が投稿のID、<int:comment_pk>がコメントのIDに対応します。

変更点: 投稿詳細ページでコメントフォームと一覧を表示する処理の追加

/app/views.py
1 @login_required
2 def show(request, pk):
3     board = Board.objects.get(pk=pk)
4-    return render(request, 'show.html', {'board': board})
5+    comments = Comment.objects.filter(board=pk).order_by('-created_at')
6+    comment_form = CommentForm()
7+    return render(request, 'show.html', {'board': board, 'comments': comments, 'comment_form': comment_form})

投稿詳細ページ (show.html) を表示するshowビューに、コメント機能のための処理を追加しました。

  • comments = Comment.objects.filter(board=pk).order_by('-created_at'): 表示している投稿(pkで指定)に紐づく全てのコメントをデータベースから取得しています。filter(board=pk)で特定の投稿のコメントのみを絞り込み、order_by('-created_at')で新しいコメントが上に表示されるように作成日時順で並び替えています。
  • comment_form = CommentForm(): 新規コメントを投稿するための空のフォームを生成しています。
  • return render(...): 取得した投稿情報(board)に加えて、コメントの一覧(comments)と空のコメントフォーム(comment_form)をHTMLテンプレートに渡しています。これにより、テンプレート側でこれらの情報を表示できるようになります。

変更点: コメントをデータベースに保存する処理の追加

/app/views.py
1+@login_required
2+def comment_create(request, pk):
3+    if request.method == 'POST':
4+        comment_form = CommentForm(request.POST)
5+        if comment_form.is_valid():
6+            comment_form.instance.user = request.user
7+            comment_form.instance.board_id = pk
8+            comment_form.save()
9+    return redirect('show', pk=pk)

ユーザーが入力したコメントをデータベースに保存するためのcomment_createビューを新しく作成しました。

  • if request.method == 'POST': フォームが送信されたとき(POSTリクエストのとき)のみ、中の処理を実行します。
  • comment_form = CommentForm(request.POST): 送信されたデータ(request.POST)を使ってCommentFormを生成します。
  • if comment_form.is_valid(): 入力内容に問題がないか(バリデーション)をチェックします。
  • comment_form.instance.user = request.user: コメントの投稿者情報として、現在ログインしているユーザー(request.user)を設定しています。
  • comment_form.instance.board_id = pk: このコメントがどの投稿に紐づくか、URLから受け取った投稿ID(pk)を設定しています。
  • comment_form.save(): 設定した情報を含めて、コメントをデータベースに保存します。
  • return redirect('show', pk=pk): コメント保存後、元の投稿詳細ページにリダイレクト(再表示)させています。

変更点: コメントを削除する処理の追加と権限設定

/app/views.py
1+@login_required
2+def comment_delete(request, board_pk, comment_pk):
3+    comment = get_object_or_404(Comment, pk=comment_pk)
4+
5+    if request.user == comment.user:
6+        comment.delete()
7+
8+    return redirect('show', pk=board_pk)

指定されたコメントを削除するためのcomment_deleteビューを新しく作成しました。

  • comment = get_object_or_404(Comment, pk=comment_pk): URLで指定されたID(comment_pk)を持つコメントをデータベースから取得します。もし存在しない場合は、404エラー(ページが見つかりません)を返します。
  • if request.user == comment.user: 削除しようとしているユーザー(request.user)が、そのコメントの投稿者(comment.user)と同一人物であるかを確認しています。これは、自分のコメント以外は削除できないようにするための重要なセキュリティチェックです。
  • comment.delete(): 条件が一致した場合にのみ、コメントをデータベースから削除します。
  • return redirect('show', pk=board_pk): 削除処理後、元の投稿詳細ページにリダイレクトさせています。

変更点: コメント入力フォームを画面に表示

/templates/show.html
1+    <section class="mt-3">
2+        <h3>コメントを追加する</h3>
3+        <form action="{% url 'comment_create' pk=board.pk %}" method="post">
4+            {% csrf_token %}
5+            <div class="form-group mb-3">
6+                <textarea name="content" rows="2"  class="form-control" id="id_content"></textarea>
7+            </div>
8+            <button type="submit" class="btn btn-primary">コメントする</button>
9+        </form>
10+    </section>

投稿詳細ページに、ユーザーがコメントを入力・送信するためのフォームをHTMLで追加しました。

  • <form action="{% url 'comment_create' pk=board.pk %}" method="post">: フォームの送信先(action)と送信方法(method)を指定しています。{% url 'comment_create' pk=board.pk %}は、urls.pyで定義したcomment_createという名前のURLを動的に生成します。board.pkは表示中の投稿IDです。これにより、正しい投稿にコメントが紐づけられます。
  • {% csrf_token %}: Djangoのフォームで必須のセキュリティ対策タグです。不正な送信からアプリケーションを保護します。
  • <textarea name="content" ...>: コメント本文を入力するエリアです。name="content"CommentFormcontentフィールドに対応します。

変更点: コメント一覧を画面に表示

/templates/show.html
1+    <section class="mt-3">
2+        <h3>コメント一覧</h3>
3+        <ul class="list-unstyled">
4+            {% for comment in comments %}
5+                <li class="mb-3">
6+                   ...
7+                </li>
8+            {% empty %}
9+                <li class="text-muted">コメントはまだありません。</li>
10+            {% endfor %}
11+        </ul>
12+    </section>

views.pyから渡されたコメントの一覧を表示する処理を追加しました。

  • {% for comment in comments %}: views.pyshowビューから渡されたcomments(コメントのリスト)を、forループを使って1件ずつ取り出しています。
  • {{ comment.user.username }}{{ comment.content }}: ループで取り出したcommentオブジェクトの各プロパティ(投稿者名や本文)を表示しています。
  • {% empty %}: forループの対象であるcommentsが空(コメントが1件もない)の場合に、このブロック内の「コメントはまだありません。」というメッセージを表示します。
  • {% endfor %}: forループの終わりを示します。

変更点: 自分のコメントにだけ削除ボタンを表示

/templates/show.html
1+                        <div class="card-footer">
2+                            {% if request.user == comment.user %}
3+                                <form action="{% url 'comment_delete' board.pk comment.pk %}" method="post">
4+                                    {% csrf_token %}
5+                                    <button type="submit" class="btn btn-danger">削除する</button>
6+                                </form>
7+                            {% endif %}
8+                        </div>

コメント一覧の中で、自分のコメントにだけ削除ボタンを表示するための条件分岐を追加しました。

  • {% if request.user == comment.user %}: テンプレート内で使える条件分岐タグです。現在ログインしているユーザー(request.user)と、表示しているコメントの投稿者(comment.user)が一致するかを判定します。
  • 一致した場合のみ、ifendifで囲まれた削除ボタンのフォームが表示されます。これにより、他のユーザーのコメントには削除ボタンが表示されなくなり、誤操作を防げます。
  • フォームのactionには{% url 'comment_delete' board.pk comment.pk %}が指定されており、クリックすると対応するコメントの削除処理が呼び出されます。

おわりに

おわりに 今回は、掲示板アプリにコメント機能を追加する一連の流れを学びました。ForeignKeyを使って投稿とコメントのモデルを関連付けることで、データ同士に繋がりを持たせる方法が重要なポイントです。ビューではフォームから送られたデータをデータベースに保存したり、特定のコメントを削除したりする処理を作成しました。さらに、テンプレートのif文でログイン中のユーザーとコメントの投稿者を比較し、自分のコメントにだけ削除ボタンを表示する基本的な権限設定も実装しました。このようにモデル、ビュー、テンプレートを連携させて一つの機能を完成させる流れは、Webアプリケーション開発の基本となります。

関連コンテンツ