フォーム処理とコードの削減

DjangoDjangoBeginner
オンラインで実践に進む

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

このチュートリアルは、パブリックインターフェイスビューの作成が終わったところから始まります。私たちはウェブ投票アプリケーションを続け、フォーム処理とコードの削減に焦点を当てます。

最小限のフォームを作成する

前回のチュートリアルで作成した投票詳細テンプレート(polls/detail.html)を更新しましょう。このテンプレートに HTML の<form>要素を含めます。

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
    <legend><h1>{{ question.question_text }}</h1></legend>
    {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
    {% for choice in question.choice_set.all %}
        <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
        <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
    {% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>

簡単な説明:

  • 上記のテンプレートは、各質問の選択肢に対してラジオボタンを表示します。各ラジオボタンのvalueは、関連する質問の選択肢の ID です。各ラジオボタンのname"choice"です。つまり、誰かがラジオボタンの 1 つを選択してフォームを送信すると、POST データchoice=#が送信されます。ここで#は選択された選択肢の ID です。これが HTML フォームの基本的な概念です。
  • フォームのaction{% url 'polls:vote' question.id %}に設定し、method="post"に設定しました。method="post"method="get"とは対照的)を使用することは非常に重要です。なぜなら、このフォームを送信するとサーバーサイドのデータが変更されるからです。サーバーサイドのデータを変更するフォームを作成する場合は常にmethod="post"を使用します。このヒントは Django に特有のものではありません。一般的な良いウェブ開発の慣習です。
  • forloop.counterは、forタグがループを何回行ったかを示します。
  • 私たちが作成しているのは POST フォーム(データを変更する可能性がある)なので、クロスサイトリクエスト偽装について心配する必要があります。幸いなことに、あまり苦労する必要はありません。なぜなら、Django には対策するための便利なシステムが備えているからです。要するに、内部 URL を対象とするすべての POST フォームは、{% csrf_token %}<csrf_token>テンプレートタグを使用する必要があります。

では、送信されたデータを処理して何かを行う Django のビューを作成しましょう。覚えておいてください。**パブリックインターフェイスビューの作成**では、投票アプリケーション用の URLconf を作成しました。この URLconf には次の行が含まれています。

path("<int:question_id>/vote/", views.vote, name="vote"),

また、vote()関数のダミー実装も作成しました。本物のバージョンを作成しましょう。polls/views.pyに次のコードを追加します。

from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from.models import Choice, Question


#...
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST["choice"])
    except (KeyError, Choice.DoesNotExist):
        ## 質問の投票フォームを再表示します。
        return render(
            request,
            "polls/detail.html",
            {
                "question": question,
                "error_message": "You didn't select a choice.",
            },
        )
    else:
        selected_choice.votes += 1
        selected_choice.save()
        ## POST データを正常に処理した後は常に HttpResponseRedirect を返します。
        ## これにより、ユーザーが戻るボタンを押した場合にデータが 2 回送信されるのを防ぎます。
        return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))

このコードには、このチュートリアルでまだ扱っていないいくつかの要素が含まれています。

  • request.POST <django.http.HttpRequest.POST>は、キー名で送信されたデータにアクセスできる辞書のようなオブジェクトです。この場合、request.POST['choice']は選択された選択肢の ID を文字列として返します。request.POST <django.http.HttpRequest.POST>の値は常に文字列です。

    ただし、Django は同じ方法で GET データにアクセスするためのrequest.GET <django.http.HttpRequest.GET>も提供しています。ただし、私たちはコードで明示的にrequest.POST <django.http.HttpRequest.POST>を使用しています。これにより、データが POST 呼び出しを介してのみ変更されることを確認します。

  • request.POST['choice']は、POST データにchoiceが提供されていない場合にKeyErrorを発生させます。上記のコードはKeyErrorをチェックし、choiceが指定されていない場合にエラーメッセージ付きで質問フォームを再表示します。

  • 選択肢のカウントを増やした後、コードは通常の~django.http.HttpResponseではなく~django.http.HttpResponseRedirectを返します。~django.http.HttpResponseRedirectは 1 つの引数を取ります。この引数は、ユーザーがリダイレクトされる URL です(この場合、URL を構築する方法については次の項を参照)。

    上の Python のコメントにもあるように、POST データを正常に処理した後は常に~django.http.HttpResponseRedirectを返す必要があります。このヒントは Django に特有のものではありません。一般的な良いウェブ開発の慣習です。

  • この例では、~django.http.HttpResponseRedirectコンストラクタで~django.urls.reverse関数を使用しています。この関数は、ビュー関数で URL をハードコードする必要を回避するのに役立ちます。これには、制御を渡したいビューの名前と、そのビューを指す URL パターンの可変部分が与えられます。この場合、**パブリックインターフェイスビューの作成**で設定した URLconf を使用すると、この~django.urls.reverse呼び出しは次のような文字列を返します。

    "/polls/3/results/"
    

    ここで3question.idの値です。このリダイレクトされた URL は、最終的なページを表示するために'results'ビューを呼び出します。

**パブリックインターフェイスビューの作成**で述べたように、request~django.http.HttpRequestオブジェクトです。~django.http.HttpRequestオブジェクトに関する詳細については、request and response documentation </ref/request-response>を参照してください。

誰かが質問に投票した後、vote()ビューは質問の結果ページにリダイレクトします。そのビューを書きましょう。

from django.shortcuts import get_object_or_404, render


def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/results.html", {"question": question})

これは、**パブリックインターフェイスビューの作成**detail()ビューとほぼ同じです。唯一の違いはテンプレート名です。後でこの冗長性を修正します。

では、polls/results.htmlテンプレートを作成しましょう。

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

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

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

では、ブラウザで/polls/1/に移動して質問に投票してみましょう。投票するたびに結果ページが更新されるはずです。選択肢を選ばずにフォームを送信すると、エラーメッセージが表示されるはずです。

cd ~/project/mysite
python manage.py runserver 0.0.0.0:8080
投票フォームインターフェイス

注記

私たちのvote()ビューのコードには小さな問題があります。まず、データベースからselected_choiceオブジェクトを取得し、次にvotesの新しい値を計算し、そしてそれをデータベースに保存します。あなたのウェブサイトの 2 人のユーザーがまさに同時に投票しようとした場合、これがうまくいかないことがあります。同じ値、たとえば 42 がvotesとして取得されます。そして、両方のユーザーにとって新しい値 43 が計算されて保存されますが、期待される値は 44 です。

これは「競合条件」と呼ばれます。興味があれば、avoiding-race-conditions-using-fを読んで、この問題をどのように解決するか学ぶことができます。

汎用ビューを使う:コードが少ない方が良い

detail()**パブリックインターフェイスビューの作成**から)とresults()ビューは非常に短く、上記のように冗長です。投票一覧を表示するindex()ビューも同様です。

これらのビューは、基本的なウェブ開発の一般的なケースを表しています。URL に渡されたパラメータに基づいてデータベースからデータを取得し、テンプレートを読み込み、レンダリングされたテンプレートを返すというものです。これは非常に一般的なため、Django は「汎用ビュー」システムと呼ばれるショートカットを提供しています。

汎用ビューは、共通のパターンを抽象化しており、アプリを書くために Python コードさえ書かなくても良いほどまでです。

投票アプリを汎用ビューシステムを使うように変更しましょう。そうすることで、自分たちのコードをたくさん削除できます。変更にはいくつかの手順が必要です。

  1. URLconf を変更する。
  2. 古くて不要なビューの一部を削除する。
  3. Django の汎用ビューに基づいた新しいビューを導入する。

詳細は以下を読んでください。

なぜコードを入れ替えるのか?

一般的に、Django アプリを書く際は、汎用ビューが問題に適しているかどうかを評価し、最初から使うようにします。途中でコードをリファクタリングすることはありません。しかし、このチュートリアルではこれまでコアコンセプトに焦点を当てるため、「難しい方法」でビューを書くことに重点を置いてきました。

電卓を使い始める前に基本的な数学を知っておく必要があります。

URLconf を修正する

まず、polls/urls.pyの URLconf を開き、以下のように変更します。

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:question_id>/vote/", views.vote, name="vote"),
]

2 番目と 3 番目のパターンのパス文字列での一致するパターンの名前が、<question_id>から<pk>に変更されていることに注意してください。

ビューを修正する

次に、古いindexdetailresultsビューを削除し、代わりに Django の汎用ビューを使います。そのために、polls/views.pyファイルを開き、以下のように変更します。

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from.models import Choice, Question


class IndexView(generic.ListView):
    template_name = "polls/index.html"
    context_object_name = "latest_question_list"

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by("-pub_date")[:5]


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


class ResultsView(generic.DetailView):
    model = Question
    template_name = "polls/results.html"


def vote(request, question_id):
  ...  ## 上と同じで、変更は不要。

ここでは 2 つの汎用ビューを使っています。~django.views.generic.list.ListView~django.views.generic.detail.DetailViewです。それぞれのビューは、「オブジェクトの一覧を表示する」と「特定の種類のオブジェクトの詳細ページを表示する」という概念を抽象化しています。

  • 各汎用ビューは、どのモデルに対して動作するかを知る必要があります。これはmodel属性を使って提供されます。
  • ~django.views.generic.detail.DetailView汎用ビューは、URL からキャプチャされた主キー値が"pk"と呼ばれることを期待しています。そのため、汎用ビュー用にquestion_idpkに変更しました。

デフォルトでは、~django.views.generic.detail.DetailView汎用ビューは<アプリ名>/<モデル名>_detail.htmlと呼ばれるテンプレートを使用します。私たちの場合、"polls/question_detail.html"を使用します。template_name属性は、Django に自動生成されたデフォルトのテンプレート名の代わりに特定のテンプレート名を使用するように指示するために使用されます。また、results一覧ビューのtemplate_nameも指定しています。これにより、結果ビューと詳細ビューがレンダリングされる際に異なる外見を持つようになります。実際には、両方とも背後で~django.views.generic.detail.DetailViewです。

同様に、~django.views.generic.list.ListView汎用ビューは、<アプリ名>/<モデル名>_list.htmlと呼ばれるデフォルトのテンプレートを使用します。私たちはtemplate_nameを使って、~django.views.generic.list.ListViewに既存の"polls/index.html"テンプレートを使用するように指示しています。

このチュートリアルの前の部分では、テンプレートにはquestionlatest_question_listのコンテキスト変数が含まれるコンテキストが提供されていました。DetailViewの場合、question変数は自動的に提供されます。Django モデル(Question)を使用しているため、Django はコンテキスト変数に適切な名前を決定できます。一方、ListViewの場合、自動生成されるコンテキスト変数はquestion_listです。これをオーバーライドするために、context_object_name属性を提供して、latest_question_listを代わりに使用することを指定しています。別の方法として、新しいデフォルトのコンテキスト変数に合わせてテンプレートを変更することもできます。しかし、Django に使用したい変数を指定する方がはるかに簡単です。

サーバーを起動して、汎用ビューに基づく新しい投票アプリを使ってみましょう。

まとめ

おめでとうございます!あなたはフォーム処理とコードの削減の実験を完了しました。あなたのスキルを向上させるために、LabEx でさらに実験を行って練習することができます。