※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 でもくもくして、このブログを作成しました)
コメント機能を実装したブログはこのような感じです。
背景
Django Girls Tutorial Extensionsでは、Django Girls Tutorialで作ったブログアプリに機能追加していきます。
ExtensionsのコードはDjango 1系で書かれており、Django Girls Tutorialの翻訳メンバーの間では「Django 2系に書き換えたいね」という話が出ていました。
Django 2系に書き換える対象は、3ページあります。
- Homework: add more to your website! · Django Girls Tutorial: Extensions (機能追加)
- Homework: secure your website · Django Girls Tutorial: Extensions (セキュリティ)
- Homework: create comment model · Django Girls Tutorial: Extensions (コメント機能)
今回はコメント機能のチュートリアルを進める中で気づいたことを書きます。
Extensionsに取り組まれる方の参考になれば幸いです。
前提
- macOS 10.13.6
- Python 3.6.6
- Django 2.0.9
- venvで仮想環境を作って動かしています(スクショ中に「Dockerで動かしています」とありますが、今回は当てはまりません)
コメント機能追加の流れ
大きく分けると3ステップです。
- コメントを管理画面から入力できるようにし、画面に表示する
- コメントのモデルを追加
- コメントのモデル用のマイグレーション実施
- 管理画面から操作できるように設定
- ブログアプリにコメント表示(記事詳細画面、記事一覧画面)
- ブログからコメントを作成できるようにする
- コメント用のフォーム追加
- 記事詳細画面からコメント入力画面へ遷移するように設定
- 記事詳細画面のテンプレートにボタンを追加
- コメント入力画面のURL設定
- コメント入力画面のビュー関数作成
- コメント入力フォーム追加
- コメントの承認機能と削除機能をつける
- 記事詳細画面のテンプレートにコメント承認ボタン・削除ボタンを追加
- コメント承認機能、コメント削除機能用のURL設定
- コメント承認用ビュー関数、コメント削除用ビュー関数作成
- 記事一覧に表示されるコメント数を承認されたコメント数に変更する
それでは順に見ていきましょう。
コメントを管理画面から入力できるようにし、画面に表示する
コメント用のクラス追加
blog/models.py
にComment
クラスを追加します。
# 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_set
1として記事に紐づくコメント全体にアクセスできます。2 (B)
(B)のケースで、ForeignKey
のrelated_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) # 追加
コメントを表示する
管理画面から作成したコメントを表示します。
記事詳細画面、記事一覧画面の順で表示するコードを追加していきます。
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>
タグに置き換えます5Joel\nis a\n\nslug
を渡すと、Joel<br>is a<br><br>slug
となりました
- linebreaksタグは空行が続く改行を
<p>
タグに置き換え、改行を<br>
タグに置き換えます6Joel\nis a\n\nslug
を渡すと、<p>Joel<br>is a</p><p>slug</p>
となりました
記事詳細画面にコメントが表示されるようになりました!
コメントにスタイルを設定します。
blog/static/css/blog.css
/* これまでのスタイル設定の下に追加 */ .comment { margin: 20px 0px 20px 20px; }
スタイルを設定するとコメントが右に寄ります。
続いて、記事一覧画面に、各記事に紐づくコメントの数を表示します。
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
ブログからコメントを作成できるようにする
コメント用のフォームを追加
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
というエラーを解決し、先に進んだと考えましょう)
blog/urls.py
に追加した行で、http://localhost:8000/post/1/comment/
のようなURLに対して、blog/views.py
のadd_comment_to_post
関数を実行するように設定しています。
ところが、現時点ではblog/views.py
にadd_comment_to_post
関数が見つからないため、エラーとなっています。
では、blog/views.py
にadd_comment_to_post
関数を用意しましょう。
この解決は後編の記事に続きます。
脚注
-
「ForeignKey 定義内の related_name パラメータをセットして FOO_set の名前をオーバーライドできます」ref: https://docs.djangoproject.com/ja/2.1/topics/db/queries/#backwards-related-objects↩
-
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
を実行してください)↩ -
https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#for-empty↩
-
https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#linebreaksbr↩
-
https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#linebreaks↩