編寫你的第一個 Django 應(yīng)用,第 4 部分
本教程從 教程第 3 部分 結(jié)束的地方開始。我們將繼續(xù)網(wǎng)絡(luò)投票的應(yīng)用,并將重點放在表單處理和精簡我們的代碼上。
從哪里獲得幫助:
如果你在閱讀本教程的過程中有任何疑問,可以前往 FAQ 的 獲取幫助 的版塊。
編寫一個簡單的表單
讓我們更新一下在上一個教程中編寫的投票詳細(xì)頁面的模板 ("polls/detail.html") ,讓它包含一個 HTML <form> 元素:
polls/templates/polls/detail.html
<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>
簡要說明:
- 上面的模板在 Question 的每個 Choice 前添加一個單選按鈕。 每個單選按鈕的
value屬性是對應(yīng)的各個 Choice 的 ID。每個單選按鈕的name是"choice"。這意味著,當(dāng)有人選擇一個單選按鈕并提交表單提交時,它將發(fā)送一個 POST 數(shù)據(jù)choice=#,其中# 為選擇的 Choice 的 ID。這是 HTML 表單的基本概念。 - 我們將表單的
action設(shè)置為{% url 'polls:vote' question.id %},并設(shè)置method="post"。使用method="post"(而不是method="get")是非常重要的,因為提交這個表單的行為將改變服務(wù)器端的數(shù)據(jù)。當(dāng)你創(chuàng)建一個改變服務(wù)器端數(shù)據(jù)的表單時,使用method="post"。這不是 Django 的特定技巧;這是優(yōu)秀的網(wǎng)站開發(fā)技巧。 -
forloop.counter指示for標(biāo)簽已經(jīng)循環(huán)多少次。 - 由于我們創(chuàng)建一個 POST 表單(它具有修改數(shù)據(jù)的作用),所以我們需要小心跨站點請求偽造。 謝天謝地,你不必太過擔(dān)心,因為 Django 自帶了一個非常有用的防御系統(tǒng)。 簡而言之,所有針對內(nèi)部 URL 的 POST 表單都應(yīng)該使用
{% csrf_token %}模板標(biāo)簽。
現(xiàn)在,讓我們來創(chuàng)建一個 Django 視圖來處理提交的數(shù)據(jù)。記住,在 教程第 3 部分 中,我們?yōu)橥镀睉?yīng)用創(chuàng)建了一個 URLconf ,包含這一行:
polls/urls.py
path("<int:question_id>/vote/", views.vote, name="vote"),
我們還創(chuàng)建了一個 vote() 函數(shù)的虛擬實現(xiàn)。讓我們來創(chuàng)建一個真實的版本。 將下面的代碼添加到 polls/views.py :
polls/views.py
from django.db.models import F
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):
# Redisplay the question voting form.
return render(
request,
"polls/detail.html",
{
"question": question,
"error_message": "You didn't select a choice.",
},
)
else:
selected_choice.votes = F("votes") + 1
selected_choice.save()
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
以上代碼中有些內(nèi)容還未在本教程中提到過:
-
request.POST是一個類字典對象,讓你可以通過關(guān)鍵字的名字獲取提交的數(shù)據(jù)。 這個例子中,request.POST['choice']以字符串形式返回選擇的 Choice 的 ID。request.POST的值永遠(yuǎn)是字符串。注意,Django 還以同樣的方式提供
request.GET用于訪問 GET 數(shù)據(jù) —— 但我們在代碼中顯式地使用request.POST,以保證數(shù)據(jù)只能通過 POST 調(diào)用改動。 如果在
request.POST['choice']數(shù)據(jù)中沒有提供choice, POST 將引發(fā)一個KeyError。上面的代碼檢查KeyError,如果沒有給出choice將重新顯示 Question 表單和一個錯誤信息。F("votes") + 1instructs the database to increase the vote count by 1.-
在增加 Choice 的得票數(shù)之后,代碼返回一個
HttpResponseRedirect而不是常用的HttpResponse、HttpResponseRedirect只接收一個參數(shù):用戶將要被重定向的 URL(請繼續(xù)看下去,我們將會解釋如何構(gòu)造這個例子中的 URL)。正如上面的 Python 注釋指出的,在成功處理 POST 數(shù)據(jù)后,你應(yīng)該總是返回一個
HttpResponseRedirect。這不是 Django 的特殊要求,這是那些優(yōu)秀網(wǎng)站在開發(fā)實踐中形成的共識。 在這個例子中,我們在
HttpResponseRedirect的構(gòu)造函數(shù)中使用reverse()函數(shù)。這個函數(shù)避免了我們在視圖函數(shù)中硬編碼 URL。它需要我們給出我們想要跳轉(zhuǎn)的視圖的名字和該視圖所對應(yīng)的 URL 模式中需要給該視圖提供的參數(shù)。 在本例中,使用在 教程第 3 部分 中設(shè)定的 URLconf,reverse()調(diào)用將返回一個這樣的字符串:
>"/polls/3/results/"
其中 3 是 question.id 的值。重定向的 URL 將調(diào)用 'results' 視圖來顯示最終的頁面。
正如在 教程第 3 部分 中提到的,HttpRequest 是一個 HttpRequest 對象。更多關(guān)于 HttpRequest 對象的內(nèi)容,請參見 請求和響應(yīng)的文檔 。
當(dāng)有人對 Question 進行投票后, vote() 視圖將請求重定向到 Question 的結(jié)果界面。讓我們來編寫這個視圖:
polls/views.py
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})
這和 教程第 3 部分 中的 detail() 視圖幾乎一模一樣。唯一的不同是模板的名字。 我們將在稍后解決這個冗余問題。
現(xiàn)在,創(chuàng)建一個 polls/results.html 模板:
polls/templates/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>
現(xiàn)在,在你的瀏覽器中訪問 /polls/1/ 然后為 Question 投票。你應(yīng)該看到一個投票結(jié)果頁面,并且在你每次投票之后都會更新。 如果你提交時沒有選擇任何 Choice,你應(yīng)該看到錯誤信息。
使用通用視圖:代碼還是少點好
detail() (在 教程第 3 部分 中)和 results() 視圖都很精簡 —— 并且,像上面提到的那樣,存在冗余問題。用來顯示一個投票列表的 index() 視圖(也在 教程第 3 部分 中)和它們類似。
這些視圖反映基本的網(wǎng)絡(luò)開發(fā)中的一個常見情況:根據(jù) URL 中的參數(shù)從數(shù)據(jù)庫中獲取數(shù)據(jù)、載入模板文件然后返回渲染后的模板。 由于這種情況特別常見,Django 提供一種快捷方式,叫做 “通用視圖” 系統(tǒng)。
通用視圖將常見的模式抽象到了一個地步,以至于你甚至不需要編寫 Python 代碼來創(chuàng)建一個應(yīng)用程序。例如,ListView 和 DetailView 通用視圖分別抽象了 "顯示對象列表" 和 "顯示特定類型對象的詳細(xì)頁面" 的概念。
讓我們將我們的投票應(yīng)用轉(zhuǎn)換成使用通用視圖系統(tǒng),這樣我們可以刪除許多我們的代碼。我們僅僅需要做以下幾步來完成轉(zhuǎn)換,我們將:
- 轉(zhuǎn)換 URLconf。
- 刪除一些舊的、不再需要的視圖。
- 基于 Django 的通用視圖引入新的視圖。
請繼續(xù)閱讀來了解詳細(xì)信息。
為什么要重構(gòu)代碼?
一般來說,當(dāng)編寫一個 Django 應(yīng)用時,你應(yīng)該先評估一下通用視圖是否可以解決你的問題,你應(yīng)該在一開始使用它,而不是進行到一半時重構(gòu)代碼。本教程目前為止是有意將重點放在以“艱難的方式”編寫視圖,這是為將重點放在核心概念上。
就像在使用計算器之前你需要掌握基礎(chǔ)數(shù)學(xué)一樣。
改良 URLconf?
首先,打開 polls/urls.py 這個 URLconf 并將它修改成:
polls/urls.py
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"),
]
請注意,第二和第三個模式的路徑字符串中匹配的模式名稱已從 <question_id> 更改為 <pk>。這是因為我們將使用 DetailView 通用視圖來替換我們的 detail() 和 results() 視圖,它期望從 URL 中捕獲的主鍵值被稱為 "pk"。
改良視圖?
下一步,我們將刪除舊的 index, detail, 和 results 視圖,并用 Django 的通用視圖代替。打開 polls/views.py 文件,并將它修改成:
polls/views.py
from django.db.models import F
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):
# same as above, no changes needed.
...
每個通用視圖都需要知道它將要操作的模型。可以使用 model 屬性來提供這個信息(在這個示例中,對于 DetailView 和 ResultsView,是 model = Question),或者通過定義 get_queryset() 方法來實現(xiàn)(如 IndexView 中所示)。
默認(rèn)情況下,通用視圖 DetailView 使用一個叫做 <app name>/<model name>_detail.html 的模板。在我們的例子中,它將使用 "polls/question_detail.html" 模板。template_name 屬性是用來告訴 Django 使用一個指定的模板名字,而不是自動生成的默認(rèn)名字。 我們也為 results 列表視圖指定了 template_name —— 這確保 results 視圖和 detail 視圖在渲染時具有不同的外觀,即使它們在后臺都是同一個 DetailView 。
類似地,ListView 使用一個叫做 <app name>/<model name>_list.html 的默認(rèn)模板;我們使用 template_name 來告訴 ListView 使用我們創(chuàng)建的已經(jīng)存在的 "polls/index.html" 模板。
在之前的教程中,提供模板文件時都帶有一個包含 question 和 latest_question_list 變量的 context。對于 DetailView , question 變量會自動提供—— 因為我們使用 Django 的模型(Question), Django 能夠為 context 變量決定一個合適的名字。然而對于 ListView, 自動生成的 context 變量是 question_list。為了覆蓋這個行為,我們提供 context_object_name 屬性,表示我們想使用 latest_question_list。作為一種替換方案,你可以改變你的模板來匹配新的 context 變量 —— 這是一種更便捷的方法,告訴 Django 使用你想使用的變量名。
啟動服務(wù)器,使用一下基于通用視圖的新投票應(yīng)用。
更多關(guān)于通用視圖的詳細(xì)信息,請查看 通用視圖的文檔