nikkie-ftnextの日記

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

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

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

はじめに

いつも心は虹色に! nikkieです。
Django Girls Tutorial ExtensionsのDjango2系動作確認の第3弾(最終回)です。
第1弾はこちら:Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 1/3) #モグモグDjango - nikkie-ftnextの日記
第2弾はこちら:Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 2/3) セキュリティ編 - nikkie-ftnextの日記
(#pyhack でもくもくして、このブログを作成しました)

コメント機能を実装したブログはこのような感じです。
f:id:nikkie-ftnext:20181229182225p:plain

背景

Django Girls Tutorial Extensionsでは、Django Girls Tutorialで作ったブログアプリに機能追加していきます。
ExtensionsのコードはDjango 1系で書かれており、Django Girls Tutorialの翻訳メンバーの間では「Django 2系に書き換えたいね」という話が出ていました。

Django 2系に書き換える対象は、3ページあります。

今回はコメント機能のチュートリアルを進める中で気づいたことを書きます。
Extensionsに取り組まれる方の参考になれば幸いです。

前提

  • macOS 10.13.6
  • Python 3.6.6
  • Django 2.0.9
  • venvで仮想環境を作って動かしています(スクショ中に「Dockerで動かしています」とありますが、今回は当てはまりません)

コメント機能追加の流れ

大きく分けると3ステップです。

  1. コメントを管理画面から入力できるようにし、画面に表示する
    • コメントのモデルを追加
    • コメントのモデル用のマイグレーション実施
    • 管理画面から操作できるように設定
    • ブログアプリにコメント表示(記事詳細画面、記事一覧画面)
  2. ブログからコメントを作成できるようにする
    • コメント用のフォーム追加
    • 記事詳細画面からコメント入力画面へ遷移するように設定
      • 記事詳細画面のテンプレートにボタンを追加
      • コメント入力画面のURL設定
      • コメント入力画面のビュー関数作成
    • コメント入力フォーム追加
  3. コメントの承認機能と削除機能をつける
    • 記事詳細画面のテンプレートにコメント承認ボタン・削除ボタンを追加
    • コメント承認機能、コメント削除機能用のURL設定
    • コメント承認用ビュー関数、コメント削除用ビュー関数作成
    • 記事一覧に表示されるコメント数を承認されたコメント数に変更する

それでは順に見ていきましょう。

コメントを管理画面から入力できるようにし、画面に表示する

コメント用のクラス追加

blog/models.pyCommentクラスを追加します。

# importの行とPostモデルの定義は省略

class Comment(models.Model):
    post = models.ForeignKey('blog.Post', on_delete=models.CASCADE, related_name='comments')
    author = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)
    approved_comment = models.BooleanField(default=False)

    def approve(self):
        self.approved_comment = True
        self.save()

    def __str__(self):
        return self.text

Post(記事)とComment(コメント)の関係は

  • 1つのコメントは1つの記事につく (A)
  • 1つの記事は複数のコメントを持つ (B)

です。
commentをCommentモデルのインスタンスとすると、comment.postとしてコメントの紐づく記事にアクセスできます。(A)
postをPostモデルのインスタンスとすると、post.comment_set1として記事に紐づくコメント全体にアクセスできます。2 (B)
(B)のケースで、ForeignKeyrelated_name引数を設定することで、post.<related_name引数の値>というアクセスが可能になります。
(今回のコードではpost.commentsと書けるということです)

管理画面からコメントを作成できるようにする

新しくコメントモデルを追加した後は、マイグレーションを実施します。

$ python manage.py makemigrations blog
$ python manage.py migrate blog

管理画面からコメントが作成できるように、blog/admin.pyを編集します。

from django.contrib import admin
from .models import Post, Comment # Commentを追加

admin.site.register(Post)
admin.site.register(Comment) # 追加

f:id:nikkie-ftnext:20181229182250p:plain

コメントを表示する

管理画面から作成したコメントを表示します。
記事詳細画面、記事一覧画面の順で表示するコードを追加していきます。

blog/templates/blog/post_detail.html
(ブログを書いている途中、詳細画面に<hr>タグ(記事本文とコメントを分けるための水平線を表示)を書き忘れていることに気づきました。
以下のスクショの詳細画面には水平線が表示されていませんが、本来は表示されるはずです。)

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

{% block content %}
  <div class="post">
    <!-- 省略 -->
  </div>
  <!-- 追加 -->
  <hr>
  {% for comment in post.comments.all %}
    <div class="comment">
      <div class="date">{{ comment.created_date }}</div>
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text|linebreaks }}</p>
    </div>
  {% empty %}
    <p>No comments here yet :(</p>
  {% endfor %}
  <!-- 追加 終わり -->
{% endblock %}

{% empty %}というタグが使われていますが、これは{% for %}タグで「ループさせようとした配列が空、または存在しなかった場合に表示する文字列を指定」するものだそうです。3

コメントの本文の表示にlinebreaksというフィルタが使われています。
記事の本文の表示ではlinebreaksbrでした。<p>{{ post.text|linebreaksbr }}</p> 4
2つの挙動の違いは以下の通りです。

  • linebreaksbrは、改行を<br>タグに置き換えます5
    • Joel\nis a\n\nslugを渡すと、Joel<br>is a<br><br>slugとなりました
  • linebreaksタグは空行が続く改行を<p>タグに置き換え、改行を<br>タグに置き換えます6
    • Joel\nis a\n\nslugを渡すと、<p>Joel<br>is a</p><p>slug</p>となりました

記事詳細画面にコメントが表示されるようになりました!
f:id:nikkie-ftnext:20181229182308p:plain

コメントにスタイルを設定します。

blog/static/css/blog.css

/* これまでのスタイル設定の下に追加 */
.comment {
  margin: 20px 0px 20px 20px;
}

スタイルを設定するとコメントが右に寄ります。
f:id:nikkie-ftnext:20181229182337p:plain

続いて、記事一覧画面に、各記事に紐づくコメントの数を表示します。

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>
    </div>
  {% endfor %}
{% endblock %}

post.comments.countで各記事のコメント件数が取得されます。
Djangoシェルで試してみました。

>>> from blog.models import Post
>>> post = Post.objects.get(pk=1)
>>> post.comments.count()
2

f:id:nikkie-ftnext:20181229182405p:plain

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

コメント用のフォームを追加

blog/forms.pyにコメント用のフォーム(CommentForm)を追加します。

from django import forms

from .models import Post, Comment # Commentのimportを追加

class PostForm(forms.ModelForm):
    # 変更はないため省略

# 以下を追加する
class CommentForm(forms.ModelForm):

    class Meta:
        model = Comment
        fields = ('author', 'text',)

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

記事詳細画面からコメント入力画面へ遷移するように設定していきます。
Django URL · Django Girls Tutorial 以降で経験したように、エラーを出しながら1つ1つ解決していきます。

記事詳細画面のテンプレートに、コメント入力画面へ遷移するボタン(aタグ)を追加します。
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 %}
    <div class="comment">
      <div class="date">{{ comment.created_date }}</div>
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text|linebreaks }}</p>
    </div>
  {% empty %}
    <p>No comments here yet :(</p>
  {% endfor %}
{% endblock %}

記事詳細画面にアクセスすると(URL例:http://localhost:8000/post/1/)、NoReverseMatchというエラーが出ます。(図はExtensions参照)
これは、追加した箇所のhref="{% url 'add_comment_to_post' pk=post.pk %}が原因です。
add_comment_to_postというURLが未設定のために発生しています。
では、blog/urls.pyで設定しましょう。

from django.urls import path
from . import views

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

すると、別のエラーAttributeErrorが現れ、runserverに失敗します。(NoReverseMatchというエラーを解決し、先に進んだと考えましょう)

f:id:nikkie-ftnext:20181229182426p:plain

blog/urls.pyに追加した行で、http://localhost:8000/post/1/comment/のようなURLに対して、blog/views.pyadd_comment_to_post関数を実行するように設定しています。
ところが、現時点ではblog/views.pyadd_comment_to_post関数が見つからないため、エラーとなっています。
では、blog/views.pyadd_comment_to_post関数を用意しましょう。

この解決は後編の記事に続きます。

脚注


  1. 「ForeignKey 定義内の related_name パラメータをセットして FOO_set の名前をオーバーライドできます」ref: https://docs.djangoproject.com/ja/2.1/topics/db/queries/#backwards-related-objects

  2. Djangoモデル の章でPostモデルにauthor = models.ForeignKey('auth.User', on_delete=models.CASCADE)と定義しています。これにより、me = User.objects.get(username='ftnext')がauthorとなる記事はme.post_set.all()で取得できます。(Djangoシェルで確認する場合は、まずfrom django.contrib.auth.models import Userを実行してください)

  3. https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#for-empty

  4. https://tutorial.djangogirls.org/ja/template_extending/

  5. https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#linebreaksbr

  6. https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#linebreaks