撰寫你的第一個 Django 應用程式,第 4 部分¶
本教學從教學 3結束的地方開始。我們將繼續開發網路投票應用程式,並將重點放在表單處理和減少程式碼上。
在哪裡取得協助
如果你在學習本教學時遇到問題,請前往常見問題的取得協助章節。
撰寫一個最小的表單¶
讓我們更新上次教學中的投票詳細資訊模板(“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>
快速總結
上面的模板為每個問題選項顯示一個單選按鈕。每個單選按鈕的
value
是相關問題選項的 ID。每個單選按鈕的name
是"choice"
。這表示,當有人選擇一個單選按鈕並提交表單時,它會傳送 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 %}
模板標籤。
現在,讓我們建立一個 Django 檢視,該檢視會處理提交的資料並對其進行處理。請記住,在教學 3中,我們為投票應用程式建立了一個 URLconf,其中包含這一行
polls/urls.py
¶path("<int:question_id>/vote/", views.vote, name="vote"),
我們也建立了 vote()
函式的虛擬實作。讓我們建立一個真實的版本。將以下內容新增至 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,)))
此程式碼包含一些我們在本教學中尚未涵蓋的內容
request.POST
是一個類似字典的物件,可讓您依鍵名存取提交的資料。在此案例中,request.POST['choice']
會以字串形式傳回所選選項的 ID。request.POST
值一律為字串。請注意,Django 也提供了
request.GET
來以相同方式存取 GET 資料,但我們在程式碼中明確使用request.POST
,以確保資料僅透過 POST 呼叫來變更。如果 POST 資料中未提供
choice
,request.POST['choice']
將引發KeyError
。如果未給定choice
,上述程式碼會檢查KeyError
並重新顯示包含錯誤訊息的問題表單。F("votes") + 1
指示資料庫將投票計數增加 1。增加選項計數後,程式碼會傳回
HttpResponseRedirect
,而不是一般的HttpResponse
。HttpResponseRedirect
採用單一引數:使用者將重新導向的 URL(請參閱以下要點,瞭解我們如何在這種情況下建構 URL)。如同上面的 Python 註解所指出的,你應該在成功處理 POST 資料後一律傳回
HttpResponseRedirect
。此提示並非 Django 特有;它是一般良好的網頁開發實務。在此範例中,我們在
HttpResponseRedirect
建構函式中使用reverse()
函式。此函式有助於避免在檢視函式中硬式編碼 URL。它會收到我們想要傳遞控制權的檢視名稱,以及指向該檢視的 URL 模式的變數部分。在此案例中,使用我們在教學 3中設定的 URLconf,此reverse()
呼叫會傳回類似以下的字串"/polls/3/results/"
其中
3
是question.id
的值。此重新導向的 URL 接著會呼叫'results'
檢視以顯示最終頁面。
如同在教學 3中提到的,request
是一個 HttpRequest
物件。如需有關 HttpRequest
物件的詳細資訊,請參閱要求與回應文件。
當有人在問題中投票後,vote()
檢視會將使用者重新導向至該問題的結果頁面。讓我們撰寫該檢視
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()
檢視完全相同。唯一的差異是範本名稱。我們稍後會修正此多餘問題。
現在,建立 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>
現在,在瀏覽器中前往 /polls/1/
並在問題中投票。您應該會看到一個結果頁面,該頁面會在您每次投票時更新。如果你在未選擇選項的情況下提交表單,則應該會看到錯誤訊息。
使用通用檢視:程式碼越少越好¶
detail()
(來自教學 3)和 results()
檢視都非常簡短,而且如上所述,是多餘的。顯示投票清單的 index()
檢視也很類似。
這些檢視代表基本網頁開發的常見案例:根據在 URL 中傳遞的參數從資料庫取得資料、載入範本並傳回呈現的範本。由於這很常見,因此 Django 提供了一個稱為「通用檢視」系統的捷徑。
通用檢視會抽象化常見的模式,達到您甚至不需要撰寫 Python 程式碼即可撰寫應用程式的程度。例如,ListView
和 DetailView
通用檢視分別抽象化「顯示物件清單」和「顯示特定物件類型的詳細資訊頁面」的概念。
讓我們將投票應用程式轉換為使用通用視圖系統,這樣我們就可以刪除一堆自己的程式碼。 我們需要幾個步驟來進行轉換。 我們將會
轉換 URLconf。
刪除一些舊的、不需要的視圖。
引入基於 Django 通用視圖的新視圖。
請繼續閱讀以了解詳情。
為什麼要重新整理程式碼?
一般來說,在編寫 Django 應用程式時,你會評估通用視圖是否適合你的問題,並且你會從一開始就使用它們,而不是在程式碼進行到一半時重構程式碼。 但本教學課程刻意將重點放在「硬著頭皮」編寫視圖,直到現在才開始關注核心概念。
你應該在開始使用計算機之前了解基礎數學。
修改 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
屬性(在此範例中,model = Question
用於 DetailView
和 ResultsView
)或透過定義 get_queryset()
方法(如 IndexView
中所示)來提供。
預設情況下,DetailView
通用視圖使用名為 <app name>/<model name>_detail.html
的範本。 在我們的例子中,它將使用範本 "polls/question_detail.html"
。 template_name
屬性用於告訴 Django 使用特定的範本名稱,而不是自動產生的預設範本名稱。 我們也為 results
列表視圖指定 template_name
– 這確保在呈現時,結果視圖和詳細視圖具有不同的外觀,即使它們在幕後都是 DetailView
。
同樣地,ListView
通用視圖使用名為 <app name>/<model name>_list.html
的預設範本; 我們使用 template_name
來告訴 ListView
使用我們現有的 "polls/index.html"
範本。
在本教學課程的前幾個部分中,範本已提供包含 question
和 latest_question_list
上下文變數的內容。 對於 DetailView
,會自動提供 question
變數 – 因為我們正在使用 Django 模型 (Question
),Django 能夠決定上下文變數的適當名稱。 然而,對於 ListView,自動產生的上下文變數是 question_list
。 為了覆寫此設定,我們提供了 context_object_name
屬性,指定我們要改用 latest_question_list
。 作為替代方法,您可以變更範本以符合新的預設上下文變數 – 但告訴 Django 使用您想要的變數要容易得多。
執行伺服器,並使用基於通用視圖的新投票應用程式。
有關通用視圖的完整詳細資訊,請參閱通用視圖文件。
當您熟悉表單和通用視圖時,請閱讀本教學課程的第 5 部分,以了解如何測試我們的投票應用程式。