nikkie-ftnextの日記

イベントレポートや読書メモを発信

Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 3/3) コメント機能 <後編>

※1本の記事として書き上げましたが、記事が長くなりすぎたため、前編・後編に分けています。

ブログからコメントを作成できるようにする(承前)

コメント入力画面へ遷移するように設定(承前)

AttributeErrorへの対応として、blog/views.pyadd_comment_to_post関数を用意します。

from django.shortcuts import render, get_object_or_404
from django.utils import timezone
from .models import Post
# importの行は以下の1行のみ変更
from .forms import PostForm, CommentForm # CommentFormのimportを追加

# これまでに作成した関数は変更なし

# 以下の行を追加
def add_comment_to_post(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        form = CommentForm(request.POST)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.post = post
            comment.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = CommentForm()
    return render(request, 'blog/add_comment_to_post.html', {'form': form})

Djangoフォーム · Django Girls Tutorial でやったように、comment = form.save(commit=False)で、コメント用フォームに結びつけられたデータからデータベースに保存するためのコメントのオブジェクトを生成しています。1
ForeignKeyのpostについて、comment.post = postとして、コメントが紐づく記事を設定しています。2
(記事のidを渡すのでなく、記事そのものを渡すと、Django側で処理してくれるようです。)

add_comment_to_post関数が追加されたので、記事詳細画面が表示されるようになります。(URL例:http://localhost:8000/post/1/
f:id:nikkie-ftnext:20181229182441p:plain

コメント入力フォーム追加

ところが、追加した「Add comment」ボタンをクリックすると、TemplateDoesNotExistというエラーが表示されます。
f:id:nikkie-ftnext:20181229182510p:plain

「Add comment」ボタンをクリックすると、先ほど追加したadd_comment_to_post関数が呼び出されます。
関数の中で、render(request, 'blog/add_comment_to_post.html', {'form': form})と、blog/add_comment_to_post.htmlテンプレートを表示するように設定しているのですが、そのテンプレートが見つからずエラーとなっています。
blog/add_comment_to_post.htmlテンプレートを作りましょう。

blog/templates/blog/add_comment_to_post.html

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

{% block content %}
  <h1>New comment</h1>
  <form method="POST" class="post-form">{% csrf_token %}
    {{ form.as_p }}
    <button type="submit" class="save btn btn-default">Send</button>
  </form>
{% endblock %}

add_comment_to_post.htmlpost_edit.htmlテンプレートと非常に似ています。
テンプレートが追加されたので、「Add comment」ボタンをクリックした後のエラーが消え、コメントが書けるようになりました。

▼ログインしていなくてもコメントが書けます f:id:nikkie-ftnext:20181229182522p:plain

▼追加されました f:id:nikkie-ftnext:20181229182533p:plain

コメントの承認機能と削除機能をつける

コメント承認機能・コメント削除機能追加

ここまででブログの記事に誰もがコメントを書け、そのコメントは誰でも見られるようになりました。
ここでは、ブログにログインしているユーザが承認したコメントに限って、誰でも見られるように修正します。

記事詳細画面の変更内容

  • ログインしているユーザは、すべてのコメントが見られる:(1)
  • ログインしているユーザは、コメントの承認または削除ができる3:(2)
  • ログインしていないユーザは、承認されたコメントのみを見られる:(3)

(1)〜(3)ができるように、記事詳細画面のテンプレートを編集します。

blog/templates/blog/post_detail.html

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

{% block content %}
  <div class="post">
    <!-- 省略 -->
  </div>
  <hr>
  <a class="btn btn-default" href="{% url 'add_comment_to_post' pk=post.pk %}">Add comment</a>
  {% for comment in post.comments.all %}
    {% if user.is_authenticated or comment.approved_comment %} <!-- 追加 (1)と(3)に対応 -->
    <div class="comment">
      <!-- div class="date" を変更。(2)に対応 -->
      <div class="date">
        {{ comment.created_date }}
        {% if not comment.approved_comment %}
          <a class="btn btn-default" href="{% url 'comment_remove' pk=comment.pk %}"><span class="glyphicon glyphicon-remove"></span></a>
          <a class="btn btn-default" href="{% url 'comment_approve' pk=comment.pk %}"><span class="glyphicon glyphicon-ok"></span></a>
        {% endif %}
      </div>
      <!-- div class="date" の変更 終わり -->
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text|linebreaks }}</p>
    </div>
    {% endif %} <!-- 追加 -->
  {% empty %}
    <p>No comments here yet :(</p>
  {% endfor %}
{% endblock %}

ログインしているユーザの場合、user.is_authenticatedTrueなので、すべてのコメントが表示されます。
そのうち、承認されていない各コメントには削除ボタンと承認ボタンが表示されます。
ログインしていないユーザの場合、comment.approved_commentがTrueになる(すなわち、承認済みの)コメントが表示されます。
承認済みなので、コメントに削除ボタンと承認ボタンは表示されません。4

記事詳細画面を表示すると、NoReverseMatchエラーです。 f:id:nikkie-ftnext:20181229182552p:plain
comment_remove(とcomment_approve)というURLが設定されていないために発生しているので、blog/urls.pyを編集します。

from django.urls import path
from . import views

urlpatterns = [
    # これまで作成したpath()関数に変更はなし
    # リストの末尾の要素として以下の2行を追加する
    path('comment/<int:pk>/approve/', views.comment_approve, name='comment_approve'),
    path('comment/<int:pk>/remove/', views.comment_remove, name='comment_remove'),
]

AttributeErrorにより、サーバが起動しなくなりました。
f:id:nikkie-ftnext:20181229182610p:plain

blog/views.pycomment_approve関数(とcomment_remove関数)が見つからないために発生しているので、これらの関数を追加します。

from django.shortcuts import render, get_object_or_404
from django.utils import timezone
# importの行は以下の1行のみ変更
from .models import Post, Comment # Commentのimportを追加
from .forms import PostForm, CommentForm

# これまでに作成した関数は変更なし

# 以下の行を追加
@login_required
def comment_approve(request, pk):
    comment = get_object_or_404(Comment, pk=pk)
    comment.approve()
    return redirect('post_detail', pk=comment.post.pk)

@login_required
def comment_remove(request, pk):
    comment = get_object_or_404(Comment, pk=pk)
    comment.delete()
    return redirect('post_detail', pk=comment.post.pk)

comment.post.pkでコメントが紐づく記事のpk(ID)を取得できます。
Djangoシェルでの確認

>>> from blog.models import Comment
>>> comment = Comment.objects.get(id=1)
>>> comment.post
<Post: 初投稿:dockerで環境構築>
>>> comment.post.pk
1

これでエラーは解決し、ログインするとコメントの承認/削除を行うことができます。
f:id:nikkie-ftnext:20181229182627p:plain

ログインしていないユーザには、承認したコメントだけが表示されます。
f:id:nikkie-ftnext:20181229182640p:plain

記事一覧に表示されるコメント数を承認されたコメント数に変更する

最後に、記事一覧のコメント数を承認されたコメントの数に変更します。
(ログインの有無にかかわらず、承認されたコメント数の表示となります)

変更するのはテンプレートとモデルの2箇所です。
まず、blog/templates/blog/post_list.htmlを変更します。

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

{% block content %}
  {% for post in posts %}
    <div class="post">
      <div class="date">
        <p>published: {{ post.published_date }}</p>
      </div>
      <h1><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>
      <p>{{ post.text|linebreaksbr }}</p>
      <!-- 以下の1行だけを変更しています。変更前:<a href="{% url 'post_detail' pk=post.pk %}">Comments: {{ post.comments.count }}</a> -->
      <a href="{% url 'post_detail' pk=post.pk %}">Comments: {{ post.approved_comments.count }}</a>
    </div>
  {% endfor %}
{% endblock %}

コメントの数はPostモデルのapproved_commentsメソッドで取得した数としています。
しかし、approved_commentsはまだ実装されていないため、この時点ではコメント数には何も表示されません。
そこで、blog/models.pyを変更します。
Postモデルにapproved_commentsメソッドを追加します。

from django.db import models
from django.utils import timezone


class Post(models.Model):
    # プロパティには変更がないため省略

    def publish(self):
        self.published_date = timezone.now()
        self.save()

    # 以下のメソッドを追加するだけ
    def approved_comments(self):
        return self.comments.filter(approved_comment=True)
    
    def __str__(self):
        return self.title


class Comment(models.Model):
    # 変更がないため省略

approved_commentsメソッドでは、前編冒頭のCommentモデルのForeignKey設定で、post.commentsと書けるように設定したことを利用しています。
追加したメソッドをDjangoシェルで試してみます。

>>> from blog.models import Post
>>> post = Post.objects.get(id=1)
>>> post.approved_comments()
<QuerySet [<Comment: フォームからコメントを作りました!>, <Comment: 承認のテストです>]>
>>> post.approved_comments().count()
2

記事一覧画面で承認されたコメントの数が表示されるようになりました!
f:id:nikkie-ftnext:20181229182657p:plain

終わりに

コメント機能、手を動かす部分は長かったですが、やっていることはDjango Girls Tutorialでやってきた機能追加手順と変わらないと思います。
Djangoで機能追加するときの一連の手順の練習という印象です。

nikkie流Django機能追加手順5

  1. 新規ページへ遷移するリンクを追加
  2. 新規ページのURL設定
  3. 新規ページのビュー作成
  4. 新規ページのテンプレート作成

ExtensionsのDjango2系の動作確認はこれで完了です。
PyConJPのスプリントで協力いただいた翻訳があるので、そのレビューを進めていきます。
最後までお読みいただき、ありがとうございました。

脚注


  1. https://docs.djangoproject.com/ja/2.1/topics/forms/modelforms/#the-save-method

  2. ForeignKeyの同様の書き方として post.author = request.user がありました。ref: https://tutorial.djangogirls.org/ja/django_forms/

  3. ログインしているユーザであっても、一度承認されたコメントは削除できません。(ただし、Django Adminからは削除可能です)

  4. 一度承認されたコメントを削除できるようにする場合、この箇所を修正することになりそうです。(ログインしているユーザに限って、削除ボタンと承認ボタンを表示する)

  5. Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 1/3) #モグモグDjango - nikkie-ftnextの日記Djangoの機能追加手順」より