Djangoチュートリアル – その4

Django公式のチュートリアル「Writing your first Django app, part 4」を元に進んでいきます。

チュートリアル – その4では、以下の内容を学びます。

  • 基本的なフォームの作り方
  • データベースに投票数を保存
  • 汎用ビュー(クラスベースビュー)
WARN
必ず「仮想環境(myenv)」が有効化されているか確認しましょう。ここからは先頭の(myenv)を省略して書いています。
app1 $ source myenv/bin/activate
(myenv) app1 $ cd mysite
(myenv) mysite $

投票フォームを作る

前回、質問の詳細ページはできたので、ここからはその質問に対して投票する機能を実装していきます。

polls/detail.htmlを次のように修正して下さい。

<!-- 実行するviewをurlで指定し、保存するのでmethod="post"にします -->
<form action="{% url 'polls:vote' question.id %}" method="post">
    <!-- formの中に必須で、正しいトークンかが検証されます -->
    {% csrf_token %}
    <fieldset>
        <h1>{{ question.question_text }}</h1>
        <!-- エラーがあった時viewで定義したメッセージを表示します -->
        {% if error_message %}
            <p><strong>{{ error_message }}</strong></p>
        {% endif %}
        {% for choice in question.choice_set.all %}
            <!-- choiceという名前を設定し、view受け取れるようにします -->
            <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
            <!-- for文が繰り返された回数をカウントします -->
            <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
        {% endfor %}
    </fieldset>
    <input type="submit" value="Vote">
</form>

<input>の中身をもう少し詳しく説明します。

  • type=”radio”
    ラジオボタンを表示します。
  • name=”choice”
    inputの名前をchoiceにします。viewでrequest.POST[‘choice’]と書けば受け取れます。
  • id=”choice{{ forloop.counter }}”
    labelのforにも設定し、選択ボタンとテキストの表示を紐付けています。
  • value=”{{ choice.id }}
    choice.idの値がviewに渡されます。

actionの{% url ‘polls:vote’ question.id %}は、urls.pyで書いたこの部分です。

path('<int:id>/vote/', views.vote, name='vote'),

投票する関数を書く

polls/views.pyのvote関数を以下のようにしてください。

from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404, redirect

from .models import Question, Choice

# 他の関数

def vote(request, id):
    # 送信されたidから、該当するQuestionを取得
    question = get_object_or_404(Question, id=id)
    try:
        # name=choiceで送られてきた、choice.idに該当するデータがあれば実行
        selected_choice = question.choice_set.get(id=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # エラーが起きた時のメッセージとquestionオブジェクトを設定
        context = {
            'question': question,
            'error_message': "何も選択されていません。",
        }
        return render(request, 'polls/detail.html', context)
    else:
        # tryが成功したら、選択されたchoiceのvotesに1を足す。
        selected_choice.votes += 1
        # 保存
        selected_choice.save()
        # 投票が成功したので、投票結果にリダイレクトする。
        return redirect('polls:results', question.id)

今まで扱った事の無いコードについて解説します。

  • request.POSTは辞書のようなオブジェクトで、method=”post”で送信されたデータにアクセスできます。他に、request.GETがあり、method=”get”のデータを受け取る時に使います。POSTとGETは大文字です。
  • request.POSTのchoiceのidがデータベースに無ければ、KeyErrorを実行します。
  • 成功(投票)した場合は、Djangoがshortcutsで用意してくれている、redirect()で別のページにリダイレクトさせます。今回は、question.idのresultsのページにリダイレクトさせます。

投票結果のページを作る

views.pyのresults関数を以下のように修正しましょう。

def results(request, id):
    question = get_object_or_404(Question, id=id)
    context = {
        'question': question
        }
    return render(request, 'polls/results.html', context)

templatesのpolls内にresults.htmlを作成し、以下の内容を保存して下さい。

<h1>{{ question.question_text }}</h1>

<ul>
    {% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} 票</li>
    {% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">再投票しますか?</a>

選択肢をfor文で取り出し、選択肢とその投票数を出力しています。

競合回避

こういったカウントには一つ問題があります。

実は、同時に複数人が投票した時に正しく数字がカウントされないのです。

それを回避するためにF()式を使います。

from django.db.models import F

# 正しくカウントされない
selected_choice.votes += 1
selected_choice.save()

# 同時に投票しても正しくカウントされる
selected_choice.votes = F('votes') + 1
selected_choice.save()

不可欠なものではないで、競合を回避したシーンに出会ったら改めて調べてみると良いかもしれません。


汎用ビューについて

Djangoには汎用ビュー(Class-based views)というものがあります。

これは関数を沢山書かなくても簡単にviewを作れるようにしたものです。

よく使われるクラスベースビューをいくつかあげておきます。

  • TemplateView
    ホームページなど、単にHTMLを返す時に使われます。
  • ListView
    オブジェクトのリストを表示するページで使われます。
  • DetailView
    オブジェクトの詳細を表示するページで使われます。
  • CreateView
    オブジェクトを作成するページで使われます。
  • UpdateView
    オブジェクトを更新するページで使われます。
  • DeleteView
    オブジェクトを削除するページで使われます。

これらは非常に便利ではあるのですが、複雑な処理をしようとすると理解するまでに時間がかかるため、最初はチュートリアルでやってきた関数ベースビューでアプリを作っていくことをオススメします。

# 関数ベースビュー
def detail(request):
    context = {}
    return render(request, 'app/index.html', context)

# クラスベースビュー
from django.views.generic import DetailView

class DetailView(DetailView):
    model = Question
    template_name = 'polls/detail.html'

関数ベースビュー

def ????(request):の形で書きます。

クラスベースビュー

class ????(view_name):の形で書きます。

クラス名の頭文字は大文字で書く慣習があります。カッコの中は、Djangoが予め用意してくれているdjango.views.genericからimportして使います。コピペで使い回すので無理に覚えなくても大丈夫です。

さらに、クラスベースビューを使う時はurls.pyのpathの書き方が変わるので注意してください。

# 関数ベースビュー
path('<int:id>/', view.detail, name='detail'),

# クラスベースビュー
# idをpkに変更。クラス名の後に.as_view()
path('<int:pk>/', views.DetailView.as_view(), name='detail'),

汎用ビューを使ってみる

今まで書いたコードをクラスベースビューに変えていきます。

index、detail、resultsの3つを変更するので、とりあえずpathを変更しておきます。

<int:pk>とviewsの後が変わっています。クラスベースビューのデフォルトでは、pk(プライマリーキー)しか受け付けません。(ここではpkとidが別物として扱われますが数値は同じです)

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:id>/vote/', views.vote, name='vote'),
]

views.pyを変更します。

from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import ListView, DetailView

from .models import Choice, Question

# リストを表示したいのでListViewを使います。
class IndexView(ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        return Question.objects.order_by('-pub_date')[:5]

# オブジェクトの詳細を表示したいのでDetailViewを使います。
class DetailView(DetailView):
    model = Question
    template_name = 'polls/detail.html'

# オブジェクトの結果の詳細を知りたいのでDetailViewを使います。
class ResultsView(DetailView):
    model = Question
    template_name = 'polls/results.html'

def vote(request, id):
    # voteはそのまま

class クラス名(使いたいview)

クラスベースビューを使う時の基本的な形です。

Djangoが用意してくれているclassを()中に書いて使えるようにします。

他の場所からclassを持ってきて()内に書き、使えるようにする事を継承といいます。

class A():
    Aという機能

class B(A):
    Aの機能を継承したBという機能
Classとは?
設計図やレシピのようなイメージです。それに対し関数(def)は部品のようなclassより小さい規模のものです。

template_name

表示するhtmlファイルを、’アプリ名/ファイル名.html’で指定します。

context_object_name

テンプレート側で使うオブジェクトリストの変数になります。これを設定しない場合は、object_listがデフォルトの変数です。

def get_queryset(self)

リストの中身を設定します。classの中の関数(def)をメソッドと言います。クラスベースビューにはそれぞれメソッドがあるので、慣れるまではその都度調べて確認すると良いです。

model

DetailViewにはmodelが設定できます。urls.pyからpk等を受け取り、該当したオブジェクトを取得できます。


まとめ

  • <form>の中には必ず{% csrf_token %}を入れる
  • まずdef 関数名(request)の形を使いこなせるようにする
  • defの書き方に慣れるまで、classは深堀りしない。
  • クラスベースビューには独自のメソッドがある

チュートリアル – その4までが理解できれば、ある程度はアプリが作れるようになっているはずです。

次はステップは、自分が作りたいアプリを作ってみる事ですが、もし作りたいものが無ければSNSの作り方を紹介しているので参考にしてみて下さい。