ブログのコメント・返信ページを作る

Python Django

概要

Djangoで、ブログを作るシリーズ①の1つです。ブログのコメント・返信ページを作っていきます。

コメント投稿機能

urls.pyに、コメント投稿ページの定義を追加します。

path('detail/<int:pk>/', views.PostDetailView.as_view(), name='post_detail')

'forms.py'を開き、コメント作成用のモデルフォームを作ります。

from .models import Tag, Comment  # 追加
# 略

class CommentCreateForm(forms.ModelForm):
    """コメント投稿フォーム"""

    class Meta:
        model = Comment
        exclude = ('target', 'created_at')
        widgets = {
            'text': forms.Textarea(
                attrs={'placeholder': 'マークダウンに対応しています。\n\n```python\nprint("コードはこのような感じで書く")\n```\n\n[リンクテキスト](https://narito.ninja/)\n\n![画像alt](画像URL)'}
            )
        }

どの記事に紐づくかのtargetはビュー側で設定するので、excludeに指定して除外しておきましょう。また、作成日(created_at)も自動で現在時間が入るようにしているので、除外です。

widgets属性で何をしているかというと、入力欄のプレースホルダに次のように表示しているのです。

マークダウンに対応しています。

```python
print("コードはこのような感じで書く")
```

[リンクテキスト](https://narito.ninja/)

[画像alt](画像URL)

マークダウンの書き方を簡単にユーザーに知らせているだけですね。

このフォームを使うように、コメントのビューを作ります。

# 追加
from django.shortcuts import redirect, get_object_or_404
from .forms import PostSearchForm, CommentCreateForm
from .models import Post, Comment

# 略

class CommentCreate(generic.CreateView):
    """記事へのコメント作成ビュー。"""
    model = Comment
    form_class = CommentCreateForm

    def form_valid(self, form):
        post_pk = self.kwargs['pk']
        post = get_object_or_404(Post, pk=post_pk)
        comment = form.save(commit=False)
        comment.target = post
        comment.save()
        return redirect('nblog1:post_detail', pk=post_pk)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['post'] = get_object_or_404(Post, pk=self.kwargs['pk'])
        return context

コメントを作成したいので、CreateViewを基に作成します。ちょっと上書きして、テンプレートファイルに記事モデルインスタンスを渡すようにしたり、コメントのtargetフィールドをビュー側で設定していたりします。よくよく見ると、やっていることは単純です。

コメント投稿ページとなる、comment_form.htmlを作ります。

{% extends 'nblog1/base.html' %}

{% block content %}
    <form action="" method="POST" id="comment-form">
        {{ form.non_field_errors }}
        {% for field in form %}
            <div class="field">
                {{ field.label_tag }}
                {{ field }}
                {% if field.help_text %}
                    <span class="helptext">※{{ field.help_text }}</span>
                {% endif %}
                {{ field.errors }}
            </div>
        {% endfor %}
        {% csrf_token %}

        <button type="submit" class="btn-link">送信</button>
        <a class="button btn-link" href="{% url 'nblog1:post_detail' post.pk %}">送信せずに戻る</a>
    </form>

{% endblock %}

記事詳細ページのリンクも修正しましょう。post_detail.htmlです。

<p><a href="{% url 'nblog1:comment_create' post.pk %}" target="_blank" rel="nofollow">記事にコメントする</a></p>

style.cssを開き、コメントフォーム関連の見た目を整えます。


/* フォームユーティリティ */
.helptext {
    font-size: 14px;
}

.field {
    margin-bottom: 24px;
}

.field > * {
    display: block;
    width: 100%;
}


すると、無事にコメントが投稿できるようになります。

コメント投稿の様子

コメントへの返信機能

コメントへの返信機能も作っていきましょう。urls.pyを開いて追加します。

path('reply/create/<int:pk>/', views.ReplyCreate.as_view(), name='reply_create'),

リプライ用のもモデルフォームも作成します。forms.pyです。

from .models import Tag, Comment, Reply  # 追加

# 略

class ReplyCreateForm(forms.ModelForm):
    """返信コメント投稿フォーム"""

    class Meta:
        model = Reply
        exclude = ('target', 'created_at')
        widgets = {
            'text': forms.Textarea(
                attrs={'placeholder': 'マークダウンに対応しています。\n\n```python\nprint("コードはこのような感じで書く")\n```\n\n[リンクテキスト](https://narito.ninja/)\n\n![画像alt](画像URL)'}
            )
        }

CommentFormと、殆ど同じです。

リプライ用のビューを作ります。

from .forms import PostSearchForm, CommentCreateForm, ReplyCreateForm  # 追加
from .models import Post, Comment, Reply  # 追加

# 略

class ReplyCreate(generic.CreateView):
    """コメントへの返信作成ビュー。"""
    model = Reply
    form_class = ReplyCreateForm
    template_name = 'nblog1/comment_form.html'

    def form_valid(self, form):
        comment_pk = self.kwargs['pk']
        comment = get_object_or_404(Comment, pk=comment_pk)
        reply = form.save(commit=False)
        reply.target = comment
        reply.save()
        return redirect('nblog1:post_detail', pk=comment.target.pk)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        comment_pk = self.kwargs['pk']
        comment = get_object_or_404(Comment, pk=comment_pk)
        context['post'] = comment.target
        return context

これも先ほどと殆ど同様ですが、self.kwargs['pk']がコメントのpkということ、Replyのtargetには対象となるコメントを指定する必要があることに注意です。

post_detail.htmlを開き、返信リンクを修正します。

<a href="{% url 'nblog1:reply_create' comment.pk %}" target="_blank" rel="nofollow">返信する</a>

コメントのメールお知らせ機能

コメントが来たときに、メールで通知してくれると親切ですね。そんなわけで、メール通知機能を実装していきます。まずですが、settings.pyに次の記述をしておきます。

# メールをコンソールに表示する。
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# あなたのメールアドレス
DEFAULT_FROM_EMAIL = 'toritoritorina@gmail.com'

これはメールをコンソールに表示します。開発中の確認には便利です。実際にメールを送信してみたい方は、Djangoで、メールを送信も参考にしてください。

今回はせっかくなので、シグナルを使って通知機能を実装しましょう。まず、apps.pyを変更します。

from django.apps import AppConfig


class Nblog1Config(AppConfig):
    name = 'nblog1'

    def ready(self):
        # シグナルのロードをする。signals.pyを読み込むだけでOK
        from . import signals

signals.pyを作成します。

from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Comment


@receiver(post_save, sender=Comment)
def send_mail_to_author(sender, instance, created, **kwargs):
    """コメントがあったことを管理者に伝える"""
    pass

これで、Commentモデルがpost_save...保存された後のことですが、その際この関数が呼ばれるようになります。

後はメールを自分に送信したいのですが、その前に少し仕込みをします。views.pyを編集します。

class CommentCreate(generic.CreateView):
    """記事へのコメント作成ビュー。"""

    def form_valid(self, form):
        post_pk = self.kwargs['pk']
        post = get_object_or_404(Post, pk=post_pk)
        comment = form.save(commit=False)
        comment.target = post
        comment.request = self.request  # 追加
        comment.save()
        return redirect('nblog1:post_detail', pk=post_pk)

何を変更したかというと、CommentCreateビューのform_valid内です。.request = self.requestというコードを追加しています。つまり、モデルインスタンスにrequest属性を設定しています。

何故こんなことをしているかというと、send_mail_to_author関数内でrequestオブジェクトを使いたいのですが、シグナルにはrequestオブジェクトが自動で渡されないのです。なので、send_mail_to_author関数が呼ばれる前、save()の手前でrequestオブジェクトをモデルインスタンスに持たせます。モデルインスタンスはシグナルに渡されるので、間接的にrequestオブジェクトをシグナルに渡すことができます。

では、singnals.pyを編集します。

from django.conf import settings
from django.core.mail import send_mail
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.template.loader import render_to_string
from .models import Comment


@receiver(post_save, sender=Comment)
def send_mail_to_author(sender, instance, created, **kwargs):
    """コメントがあったことを管理者に伝える"""
    if created:
        # views.py側で、requestオブジェクトをインスタンスに格納しています。
        request = instance.request

        context = {
            'post': instance.target,
        }
        subject = render_to_string('nblog1/mail/comment_notify_subject.txt', context, request)
        message = render_to_string('nblog1/mail/comment_notify_message.txt', context, request)
        from_email = settings.DEFAULT_FROM_EMAIL
        recipient_list = [settings.DEFAULT_FROM_EMAIL]
        send_mail(subject, message, from_email, recipient_list)

createdは、更新ではなく新規作成の場合にTrueです。新規作成のときだけメールですね。instanceは、コメントのモデルインスタンスです。

メールを送信するのに、テンプレートファイルを使っています。templates/nblogmailディレクトリを作ります。その中に、まずは題名のcomment_notify_subject.txtを作ります。改行が混じらないように注意しましょう。エラーになります。

ブログ コメントが届きました。

comment_notify_message.txtを作ります。

記事「{{ post.title }}」 にコメントが投稿されました。
以下のURLからご確認ください。
{{ request.scheme }}://{{ request.get_host }}{% url 'nblog1:post_detail' post.pk %}

requestオブジェクトが使えるので、URLを柔軟に作成できます。

コメントを作成したときに、次のようなメールがコンソールが表示されれば成功です。

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
Subject:
 =?utf-8?b?44OW44Ot44KwIOOCs+ODoeODs+ODiOOBjOWxiuOBjeOBvuOBl+OBn+OAgg==?=
From: toritoritorina@gmail.com
To: toritoritorina@gmail.com
Date: Fri, 07 Feb 2020 06:40:48 -0000
Message-ID:
 <158105764867.1012.5417095305340019861@DESKTOP-HPQF560.flets-east.jp>

記事「Python、venvで仮想環境を作る」 にコメントが投稿されました。
以下のURLからご確認ください。
http://127.0.0.1:8000/detail/171/

コメントへの返信お知らせ機能

返信があったときに、コメント者にその旨伝えるようにしたいと思います。私のブログだとプログラミングの質問が多いので、解答したときにそれを伝える必要があったのです。

まずは、Commentモデルにメールアドレスのフィールドを持たせます。

class Comment(models.Model):
    """記事に紐づくコメント"""

    # 追加
    email = models.EmailField('メールアドレス', blank=True, help_text='入力しておくと、返信があった際に通知します。コメント欄には表示されません。')

makemigrationsmigrateをしておきます。あとは、Replyが作成されたときにシグナルを使い、このemail;フィールド宛にメールを送信すれば良さそうですね。

views.pyのReplyCreateViewに少し追加をします。

class ReplyCreate(generic.CreateView):
    """コメントへの返信作成ビュー。"""

    def form_valid(self, form):
        comment_pk = self.kwargs['pk']
        comment = get_object_or_404(Comment, pk=comment_pk)
        reply = form.save(commit=False)
        reply.target = comment
        reply.request = self.request  # 追加
        reply.save()
        return redirect('nblog1:post_detail', pk=comment.target.pk)

コメント同様に、リプライにもrequestオブジェクトを持たせておきます。

signals.pyで、リプライ作成時のメール送信処理を作りましょう。

from django.core.mail import send_mail, EmailMessage  # 追加
from .models import Comment, Reply  # 追加

# 略

@receiver(post_save, sender=Reply)
def send_mail_to_comment_user(sender, instance, created, **kwargs):
    """コメントに返信があったことを、管理者とコメント者に伝える"""
    if created:
        # views.py側で、requestオブジェクトをインスタンスに格納しています。
        request = instance.request
        comment = instance.target
        post = comment.target

        context = {
            'post': post,
        }
        subject = render_to_string('nblog1/mail/reply_notify_subject.txt', context, request)
        message = render_to_string('nblog1/mail/reply_notify_message.txt', context, request)

        from_email = settings.DEFAULT_FROM_EMAIL
        recipient_list = []
        bcc = [settings.DEFAULT_FROM_EMAIL]

        # コメントした人がメールアドレスを入力してれば、返信があったことを知らせる
        if comment.email:
            recipient_list.append(comment.email)
        email = EmailMessage(subject, message, from_email, recipient_list, bcc)
        email.send()

コメントのお知らせメール送信処理をと殆ど同じです。今回はbccを使って管理者にもお知らせするようにしたいので、EmailMessageを使ってメールを送信します。

reply_notify_subject.txt

ブログ あなたのコメントに返信が届きました。

reply_notify_message.txt

記事「{{ post.title }}」 のコメントに返信がありました。
以下のURLからご確認ください。
{{ request.scheme }}://{{ request.get_host }}{% url 'nblog1:post_detail' post.pk %}

コメントした人が、自分のコメントに自分で返信した場合はどうでしょう。その場合はおそらく、返信のお知らせメールはしなくて良いでしょう。これはセッションを使って管理できます。

send_mail_to_author関数のrequest = instance.requestの下に、追加します。

        request = instance.request

        # コメントの投稿者を識別するため、投稿者のセッションにコメントのpkを入れておく
        request.session[str(instance.pk)] = True

send_mail_to_comment_user関数の、if comment.email部分を書き換えます。

        if comment.email and not request.session.get(str(comment.pk)):
            recipient_list.append(comment.email)

コメントにCaptcha機能

コメント欄は何も対策しないと、全く関係のないスパムコメントで溢れかえります。私のブログでは、「いぬを漢字で書いてください」といったフィールドを用意して、これに正解しないとコメントができません。日本人からすると非常に簡単ですが、海外からのスパムコメント対策としては、めちゃくちゃ有効に働いています。

海外の一般のユーザーが恐らくコメントできなくなるので、最近はちょっと見直そうかなとも思ってますが、とりあえずこれは実装しましょう。Djangoで、シンプルなCaptchaフィールドの作成 と殆ど同じ内容です。

アプリケーション内に、fields.pyを作ります。中身は次のようにしておきましょう。

from django import forms


class SimpleCaptchaField(forms.CharField):

    def __init__(self, label='人かどうかの確認', **kwargs):
        super().__init__(label=label, required=True, **kwargs)
        self.widget.attrs['placeholder'] = '「いぬ」を漢字で書いてください'

    def clean(self, value):
        value = super().clean(value)
        if value == '犬':
            return value
        else:
            raise forms.ValidationError('答えが違います!')

forms.pyを開き、コメントとリプライの投稿では、このSimpleCaptchaFieldを使うようにします。

# 追加
from .fields import SimpleCaptchaField


class CommentCreateForm(forms.ModelForm):
    """コメント投稿フォーム"""
    captcha = SimpleCaptchaField()  # 追加


class ReplyCreateForm(forms.ModelForm):
    """返信コメント投稿フォーム"""
    captcha = SimpleCaptchaField()  # 追加

Relation Posts

Djangoで、シンプルなCaptchaフィールドの作成

コメント欄等のスパム対策として、Djangoのフォームフィールドを自分で作り、シンプルなCaptchaフィールドを実装します。

Python Django Djangoカスタムウィジェット

Djangoで、メールを送信

Djangoでメールを送信する、基本的なやり方を説明していきます。

Python Django Email

Comment

記事にコメントする

まだコメントはありません。