【Django】自作ブログサイトの記事にコメント機能を実装してみた

今回はコメント機能の実装について備忘録を残していきます。
当方初心者エンジニアですが、自分のため、そしてこれを見てくれた方のお役に立てればと思います。  

これから一部コードを提示しながら説明していきますが、全体のコードをご覧になりたい場合は[Github](https://github.com/Yoshida-Programmer/Yoshida-Blog)にありますので、そちらを参照して頂けますと幸いです。

# 完成イメージ
実装結果はこちらになります。一般的なコメント欄となります。
トップの記事リスト(list.html)から選択した記事詳細(detail.html)の下に付けてみました。
![無題①.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1608896/cab7fc9e-cebc-6c6c-f9e9-b2a10c57945f.png)

コメントする場所(comment_form.html)は記事詳細(detail.html)に『コメントする』というボタンを追加しましたので、そこから遷移できるようにしました。
名前(デフォルトは名無し)、本文を書いて『投稿する』をポチっと押せば投稿完了です。
『キャンセル』を押せば記事詳細(detail.html)に戻ります。
![無題②.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1608896/51605565-40bd-0637-eea1-fbc128bd09aa.png)

CSSは神フレームワーク『BootStrap』を使用しました。
いつかオシャレデザインを自分で実装できるようにしたいよ(遠い目)  

……ま、まあとにかく。どう実装していったかを下記に記していきます。

# 参考サイト  
下記サイトを放浪しながら構築していきました。いつもお世話になっております。
ほぼ参考サイトのロジックで実装させて頂きました。
[Narito Blog 様](https://blog.narito.ninja/detail/179)
[知識の枝 様](https://chuna.tech/detail/39/)

# ①モデル  
まずはモデルを定義していきます。
当然ですが、コメント機能はブログ記事があってこそ成立します。なのでまずはブログ記事のモデルを用意します。

```models.py
#ブログ記事モデル
#モデル名が『BlogModel』となっておりますが、『Blog』とか、『Article』とかにした方が良いですね（汗）
class BlogModel(models.Model):
    title = models.CharField(max_length=100, verbose_name='タイトル')
    content = MDTextField(verbose_name='内容')
    postdate = models.DateField(auto_now_add=True, verbose_name='投稿日')
    category = models.CharField(
        max_length= 50,
        choices= CATEGORY,
        verbose_name= 'ジャンル'
    )
    tag = models.ManyToManyField(Tag, blank=True)
```

なんか色々とフィールドを設定してますが、とりあえずtitile, content, postdateフィールドさえあれば問題ないかと。
このブログ記事モデルが出来ましたら、次はお待ちかねのコメントモデルを定義していきます。  

```models.py
#コメントモデル
class Comment(models.Model):
    user_name = models.CharField('名前', max_length=255, default='名無し')
    message = models.TextField('本文')
    target = models.ForeignKey(BlogModel, on_delete=models.CASCADE, verbose_name='対象記事')
    created_at = models.DateTimeField('作成日', default=timezone.now)
 
    def __str__(self):
        return self.message[:20]
```
さて、色々とフィールドが設定されておりますね。
user_nameは名前、messageはコメント本文,created_atはコメント投稿日時となります。
そして大事なのはtargetフィールドです。
該当するブログ記事は一つだけですが、それに対するコメントはもちろん複数ありますよね。
それを実現するにはForeignKeyを使用して、「1対多」の関係を構築する必要があります。
なので引数には先ほど定義したBlogModelを設定します。  

これでモデルの定義は完了です。あとは忘れないよう、マイグレーションです。

```
python manage.py makemigrations
python manage.py migrate
```

# ②フォーム  
ふう、ようやくモデルが完成しましたね。
というわけで、お次はフォームの設定をしていきます。  

モデルの定義はOKですが、これだけでなくフォームも定義する必要があります。
まずはアプリフォルダ直下（models.pyやviews.pyと同じ階層）にforms.pyファイルを作成します。
作成後、以下のように記述していきます。

```forms.py
from django import forms
from .models import Comment

#コメント投稿フォーム
class CommentCreateForm(forms.ModelForm):
    class Meta:
        model = Comment
        exclude = ('target', 'created_at')
```

まずは必要なライブラリをインポートしていきます。
コメントフォームを作成しますので、先ほど定義したコメントモデルもインポートします。  

メタデータに exclude = ('target', 'created_at')を記述していますが、これはフォーム画面に表示させないよう除外しております。
どの記事に紐づくかのtarget、および作成日であるcreated_atです。  

# ③ビュー  
ここまでモデルとフォームの定義が完了しました。
次はビューを作っていきます。  

```views.py
from django.shortcuts import redirect, get_object_or_404
from django.views.generic import DetailView
from django.views.generic.edit import CreateView
from .models import BlogModel, Comment
from .forms import CommentCreateForm
from django import forms

#ブログ記事詳細ページのビュー
class BlogDetail(DetailView):
    template_name = 'detail.html'
    model = BlogModel

#コメント投稿ページのビュー
class CommentView(generic.CreateView):
    template_name = 'comment_form.html'
    model = Comment
    form_class = CommentCreateForm

 　 #フォームに入力された情報が正しい場合の処理
    def form_valid(self, form):
        post_pk = self.kwargs['pk']
        post = get_object_or_404(BlogModel, pk=post_pk)
        comment = form.save(commit=False)
        comment.target = post
        comment.save()
        return redirect('blogapp:detail', pk=post_pk)

 　 #htmlテンプレートに渡すデータを定義
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['post'] = get_object_or_404(BlogModel, pk=self.kwargs['pk'])
        return context
```

クラスベースビューとなります。こちらはほぼ参考サイトと同じですが、一部書き換えている部分もあります。
CommentViewのtemplate_name = 'comment_form.html'ですが、こちらのテンプレートは後ほど作成します。それとビューの作成に必要なライブラリのインポートも忘れないようにします。  

# ④ルーティング  

urls.pyの設定も欠かさず行っていきます。アプリ直下にurls.pyを作成し、以下の通り記述します。

```urls.py
from django.urls import path
from .views import BlogDetail, CommentView

app_name = 'blogapp'

urlpatterns = [
    path('detail/<int:pk>', BlogDetail.as_view(), name='detail'),
    path('comment/create/<int:pk>/', views.CommentView.as_view(), name='comment_create'),
]

```

# ⑤テンプレート  

では、いよいよ表示するためのテンプレートを作っていきます。
今回はブログ記事詳細ページであるdetail.htmlの一番下部分にコメント欄を配置するため、以下のように記述していきます。  

```detail.html
<h3 class="comment-title">コメント</h3>

<!--コメント-->
{% for comment in object.comment_set.all %}
<div class="comment-list">
    <div class='border-bottom'>{{ comment.user_name }}　{{ comment.created_at }}</div>
    <!--改行しないようlinebreaksを設定,URL要素に<a>要素を設定-->
    <div class='mt-2'>{{ comment.message | linebreaks | urlize }}</div>
</div>
{% empty %}
<p>コメントはありません</p>
{% endfor %}
```
上記を記述することにより、冒頭の完成イメージで見せたコメント欄が表示されます。
次は肝心なコメント投稿ページを作成します。  

```comment_form.html
<div class="container col-lg-6 offset-lg-3">

    <!---記事タイトルを表示-->
    <h2 class="titleline"><span>{{ post.title }}</span></h2>
    <form action="" method="POST" id="comment-form">

　　　　 <!--特定のフィールドに紐づかないエラーを表示する-->
        {{ form.non_field_errors }}

        <!--フォームの各フィールドを取り出す-->
        {% for field in form %}
        <div class="field">
            {{ field.label_tag }}
            {% render_field field class="form-control" %}

            {{ field.errors }}
        </div>
        {% endfor %}
        {% csrf_token %}
        <div class="row my-3">
            <button type="submit" class="btn btn-success col-3 offset-2">投稿する</button>
            <a class="btn btn-danger col-3 offset-2" href="{% url 'blogapp:detail' post.pk %}">キャンセル</a>
        </div>
    </form>
</div>
```
{% for field in form %}はフォームの各フィールドを取り出します。今回の場合はuser_name(名前)、message(本文)を入力するフォームが表示されるようになります。

# さいごに

こんな感じでコメント機能を実装しました。
本当は返信機能も実装したかったところですが、それはまた別の機会に。。。今はブログ記事詳細のページに目次機能を実装するため目下努力中であります。
結構ざっくばらんに書き綴りましたが、何か不備がございましたらご連絡ください。
それでは、また何かありましたら備忘録を書こうと思います。