レビュー機能の実装

はじめに

前回↓に引き続き、本棚アプリケーションの作成を行います。今回は新たに本のレビューをすることのできる機能を追加していきます。
urhayataro.hatenablog.com

レビューは、レビューする本に対して評価、詳細、星の数を投稿できるものを目指します。


urlpatternとviewの作成

レビュー機能の実装にあたってまずurls.pyの実装から行います。やることはCreateReviewViewを呼び出すためのurlpatternsを設定します。book/urls.pyに以下の記述を追加します。

path('book/<int:book_id>/review/', views.CreateReviewView.as_view(), name='review'),

次にモデルの作成をします。book/models.pyに以下の記述を追加します。

from .consts import MAX_RATE

RATE_CHOICES = [(x, str(x)) for x in range(0, MAX_RATE + 1)]

class Review(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    text = models.TextField()
    rate = models.IntegerField(choices=RATE_CHOICES)
    user = models.ForeignKey('auth.User', on_delete=models.CASCADE)

    def __str__(self):
        return self.title

ここで設定した'tittle'が本のレビューの評価、'text'が詳細、'rate'が星の数を表しています。

モデルについて見ていきます。まずクラスの1行目のForeignKeyは別のデータベーステーブルのデータを使うときに利用します。今回はどの本に対してのレビューかを明確にするために、同じbook/models.pyにあるBookモデルのデータを参照します。ReviewモデルがBookモデルのデータを使うのでForeignKeyを利用します。これで管理画面などでレビューを作成するときにBookモデルのデータを選択することができるようになります。

ForeignKeyの第一引数はBookモデルのBookとしていますが、第二引数について見ていきます。これは参照先のデータが削除されたときの処理方法を指定します。つまり、本のデータを削除されたときの処理方法のことです。models.CASCADEは本を削除したときにその本に関するレビューも削除されます。ForeignKeyを利用した場合は、on_deleteの設定が必須となっています。

rateを指定するときに利用したIntegerFieldは整数型の数字を扱うときに利用します。また、引数のRATE_CHOICESは予めfor文で定義している変数です。この変数を別で書いているのは将来的に数字を変更する際に便利なためです。今回は星5つまでとしますが、10こまでに変更するときに別で定義することでエラーのリスクを下げることができます。


ではRATE_CHOICESの数字を設定します。数字の設定にはMAX_RATEを指定する必要があります。新たにMAX_RATEを指定するファイルを作成します。

touch book/consts.py

作成したファイルに以下の記述をします。今回は星が5つまでとするのでMAX_RATEは5としています。

MAX_RATE = 5

これでモデルの作成ができました。次にviews.pyの実装をします。
book/views.pyに以下の記述を追加します。

from .models import Book, Review

class CreateReviewView(CreateView):
    model = Review
    fields = ('book', 'title', 'text', 'rate')
    template_name = 'book/review_form.html'

モデルはReviewモデル、フィールドは本の名前、評価、詳細、星の数を設定しています。

ここでReviewモデルを作成します。モデルの作成はターミナルで以下を実行します。

python3 manage.py makemigrations
python3 manage.py migrate

次にhtmlファイルを作成します。

touch book/templates/book/review_form.html

作成したファイルに以下の記述をします。

{% extends 'base.html' %}

{% block title %}レビュー投稿{% endblock %}
{% block h1 %}レビュー投稿{% endblock %}
{% block content %}
    <form method="post" class="p-4 m-4 bg-light border-success rounded form-group">
        {% csrf_token %}
        <label>
            対象書籍
        </label>
        <input class="form-control" value="{{ object.title }}" readonly>
        <label>
            タイトル
        </label>
        <input class="form-control" name="title">
        <label>
            本文
        </label>
        <textarea class="form-control" name="text" rows="3"></textarea>
        <label></label>
        <select class="form-control" name="rate">
            <option value="0">0 (最低)</option>
            <option value="0">1</option>
            <option value="0">2</option>
            <option value="0">3 (普通)</option>
            <option value="0">4</option>
            <option value="0">5 (最高)</option>
        </select>
        <button type="submit" class="btn btn-success mt-4">投稿する</button>
    </form>
{% endblock %}

ここまでできればサーバーを立ち上げて127.0.1:8000/book/1/reviewにアクセスします。すると以下の画面になっています。

今はまだReviewモデルの実装はしたものの、Bookモデルの実装はしていないので対象書籍の欄が空欄になっています。ここからBookモデルの実装をして対象書籍の欄に表示させるようにしていきます。


Bookモデルからデータを取得し書籍情報を表示させる

ここからレビューをする書籍情報を表示できるようにしていきます。書籍情報を取得するにはCreateViewに備わっているget_content_dataというメソッドを利用します。
book/views.pyに以下の記述を追加します。

class CreateReviewView(CreateView):
    model = Review
    fields = ('book', 'title', 'text', 'rate')
    template_name = 'book/review_form.html'

    def get_context_data(self, **kwargs):  #追加
        context = super().get_context_data(**kwargs)  #追加
        context['book'] = Book.objects.get(pk=self.kwargs['book_id'])  #追加
        print(context)  #追加
        return context  #追加

新たに追加した**kwargsについて説明します。これはキーワード引数というもので、urlに入力された数字がキーワード引数としてviewに渡されます。今回の場合はbook/urls.pyで追加した、path('book//review/', views.CreateReviewView.as_view(), name='review'),のがキーワード引数に当たります。

次にcontext['book']に付いて見ていきます。これは辞書型のデータにオブジェクトを追加する事をしています。今回はBookオブジェクトのpk=self.kwargs['book_id']で指定したものに対応したデータが取得されます。これをターミナルで表示させ、返り値として設定しています。


ではサーバーを立ち上げて127.0.1:8000/book/1/reviewにアクセスします。ターミナルを見てみると以下の表示がされています。

'book':という表示はBookモデルにあるidが1の本のことを指しています。

これでBookモデルの実装ができました。合わせてbook/templates/book/review_form.htmlを修正します。

{% csrf_token %}
        <label>
            対象書籍
        </label>
        <input class="form-control" value="{{ book.title }}" readonly>  #修正
        <label>
            タイトル
        </label>
        <input class="form-control" name="title">
        <label>
            本文
        </label>
        <textarea class="form-control" name="text" rows="3"></textarea>
        <label></label>

objectだったものをbookにすることでget_context_dataで追加したcontext[book]に対応し、bookオブジェクトのtittleフィールドの情報を取得できるようになります。ここには上で説明したとおりbook_idで指定したBookモデルのデータが入っています。

再度サーバーを立ち上げて127.0.1:8000/book/1/reviewにアクセスします。対象書籍の欄が書かれています。



レビュー内容のデータを追加し反映させる

これまでで本に対してレビューを入力することはできました。ここからは入力した内容を投稿すると反映させるようにしていきます。

まず管理画面からReviewモデルのデータを追加できるようにします。book/admin.pyに以下の記述をします。

from django.contrib import admin
from .models import Book, Review
# Register your models here.

admin.site.register(Book)
admin.site.register(Review)

次にBookオブジェクトの情報をフォームに渡すことをしなければなりません。book/templates/book/review_form.htmlに以下の記述を追加します。

{% extends 'base.html' %}

{% block title %}レビュー投稿{% endblock %}
{% block h1 %}レビュー投稿{% endblock %}
{% block content %}
    <form method="post" class="p-4 m-4 bg-light border-success rounded form-group">
        {% csrf_token %}
        <label>
            対象書籍
        </label>
        <input class="form-control" value="{{ book.title }}" readonly>
        <label>
            タイトル
        </label>
        <input class="form-control" name="title">
        <label>
            本文
        </label>
        <textarea class="form-control" name="text" rows="3"></textarea>
        <label></label>
        <select class="form-control" name="rate">
            <option value="0">0 (最低)</option>
            <option value="0">1</option>
            <option value="0">2</option>
            <option value="0">3 (普通)</option>
            <option value="0">4</option>
            <option value="0">5 (最高)</option>
        </select>
        <input type="hidden" name='book' value="{{ book.id }}">
        <button type="submit" class="btn btn-success mt-4">投稿する</button>  #追加
    </form>
{% endblock %}

htmlからフォームでデータを送るときにはnameを使います。また、どの本のデータを保存するのかがわかるようにidの指定も行う必要があります。htmlファイルでidの情報を表示する必要は無いのでhiddenとしています。


ここで一度CreateReviewViewのfieldsの項目を確認します。

class CreateReviewView(CreateView):
    model = Review
    fields = ('book', 'title', 'text', 'rate')
    template_name = 'book/review_form.html'

このように4つのフィールドが設定されています。一方Reviewモデルを確認すると5つのフィールドが設定されています。

class Review(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    text = models.TextField()
    rate = models.IntegerField(choices=RATE_CHOICES)
    user = models.ForeignKey('auth.User', on_delete=models.CASCADE)

これではuserの情報が抜け落ちているのでViewからuserの情報をおくる必要があります。ここでuserの情報はブラウザ上に表示させてはいけません。この解決には以下のようにします。book/views.pyに以下の記述を追加します。

class CreateReviewView(CreateView):
    model = Review
    fields = ('book', 'title', 'text', 'rate')
    template_name = 'book/review_form.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['book'] = Book.objects.get(pk=self.kwargs['book_id'])
        print(context)
        return context

    def form_valid(self, form):  #追加
        form.instance.user = self.request.user  #追加
        return super().form_valid(form)  #追加

まずform_vaidはフォームが送信され、入力に間違いが無い場合データが保存される前に呼び出されます。

追加した2行目の左側はformクラスのinstanceというデータにuserという属性でデータを追加する事を表しています。
そして右側は取得したuserのデータをどのように追加するのかを表しています。ここではuserがログインしているときrequestオブジェクトにあるuserの情報を表しています。

この記述でユーザーの情報をformのデータに送ることができます。


ここまでで、レビューの内容をデータに追加することができます。ただ、投稿するを押せばレビューの投稿ができるはずですが、押したあとの画面の遷移先を設定していません。

投稿するを押して、データ送信後の遷移先のurlを設定します。book/views.pyで以下の記述を追加します。

from django.urls import reverse, reverse_lazy  #追加

class CreateReviewView(CreateView):
    model = Review
    fields = ('book', 'title', 'text', 'rate')
    template_name = 'book/review_form.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['book'] = Book.objects.get(pk=self.kwargs['book_id'])
        print(context)
        return context

    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)

    def get_success_url(self):  #追加
        return reverse('detail-book', kwargs={'pk':self.object.book.id})  #追加

get_success_urlメソッドを新たに作成し画面の遷移先を指定します。今まではreverce_lazyを利用していましたが、メソッドの中に書く場合はreverceを使います。
返り値の第一引数でdetail-bookを指定することで、書籍詳細ページに遷移するようにしました。また、第二引数でキーワード引数に書籍のidの番号を渡しています。DetailBookViewでは、どののデータを表示するのかを指定する必要があるのでこの記述としています。


これでレビューのデータを追加し、投稿するボタンで送信すると書籍詳細ページに画面が遷移するようになりました。

サーバーを立ち上げて127.0.0.1:8000/book/1/review/にアクセスし投稿してみましょう。

投稿すると、書籍詳細ページに画面が遷移したはずです。


追加したレビューの確認

レビューが投稿できれば、きちんと送信され、データが追加されているか確認しましょう。

サーバーを立ち上げて127.0.0.1:8000/admin/book/review/にアクセスしましょう。
ログインすると以下の画面が表示されます。

また、この良かったというところを押すと以下のようにレビューの編集ができます。


これでレビューした内容が反映されていることもわかりました。ブラウザ上でレビューを表示するページは後ほど作成します。

最後に、書籍詳細から、レビューを書くためのページに遷移するリンクを付けます。book/templates/book/book_detail.htmlに以下の記述を追加します。

{% extends 'base.html' %}

{% block title %}{{ object.title }}{% endblock %}
{% block h1 %}書籍詳細{% endblock %}

{% block content %}
<div class="card">
    <div class="p-4 m-4 bg-light border border-success rounded">
        <h2 class="text-success">{{ object.title }}</h2>
            <p>{{ object.text }}</p>
            <a href="{% url 'list-book' %}" class="btn btn-primary">一覧へ</a>
            <a href="{% url 'review' object.pk %}" class="btn btn-primary">レビューする</a>  #追加
            <a href="{% url 'update-book' object.pk %}" class="btn btn-primary">編集する</a>
            <a href="{% url 'delete-book' object.pk %}" class="btn btn-primary">削除する</a>
            <h6 class="card-title">{{ object.category }}</h6>
    </div>
</div>
{% endblock content %}

これでサーバーを立ち上げて適当な本の詳細ページを見てみると以下のようにレビューするためのボタンが追加されています。

レビューするをおすと、レビューを書く画面に遷移しました。

これでレビュー機能の実装ができました。


おわりに

今回は本のレビューを投稿する機能の追加をしました。評価、詳細、星の数を設定し、投稿することができるようになりました。
また、本のレビューをデータに追加し、確認することができました。
次回からは本の名前だけで色々していたものを本の写真を付けて見栄え良くしていきます。