编写你的第一个 Django 应用,第 4 部分
Hello,我是 Alex 007,一個(gè)熱愛計(jì)算機(jī)編程和硬件設(shè)計(jì)的小白,為啥是007呢?因?yàn)榻?Alex 的人太多了,再加上每天007的生活,Alex 007就誕生了。
Django4.表單和通用圖視圖
我們將繼續(xù)編寫投票應(yīng)用,專注于表單處理并且精簡我們的代碼。
編寫一個(gè)簡單的表單
讓我們更新一下在上一個(gè)教程中編寫的投票詳細(xì)頁面的模板 (“polls/detail.html”) ,讓它包含一個(gè) HTML <form> 元素:
<h1>{{ question.question_text }}</h1>{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}<form action="{% url 'polls:vote' question.id %}" method="post"> {% csrf_token %} {% 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 %} <input type="submit" value="Vote"> </form>簡要說明:
- 上面的模板在 Question 的每個(gè) Choice 前添加一個(gè)單選按鈕。 每個(gè)單選按鈕的 value 屬性是對應(yīng)的各個(gè) Choice 的 ID。每個(gè)單選按鈕的 name 是 “choice” 。這意味著,當(dāng)有人選擇一個(gè)單選按鈕并提交表單提交時(shí),它將發(fā)送一個(gè) POST 數(shù)據(jù) choice=# ,其中# 為選擇的 Choice 的 ID。這是 HTML 表單的基本概念。
- 我們設(shè)置表單的 action 為 {% url ‘polls:vote’ question.id %} ,并設(shè)置 method=“post” 。使用 method=“post”(與其相對的是 method=“get”)是非常重要的,因?yàn)檫@個(gè)提交表單的行為會(huì)改變服務(wù)器端的數(shù)據(jù)。 無論何時(shí),當(dāng)你需要?jiǎng)?chuàng)建一個(gè)改變服務(wù)器端數(shù)據(jù)的表單時(shí),請使用 method=“post” 。這不是 Django 的特定技巧;這是優(yōu)秀的網(wǎng)站開發(fā)技巧。
- forloop.counter 指示 for 標(biāo)簽已經(jīng)循環(huán)多少次。
- 由于我們創(chuàng)建一個(gè) POST 表單(它具有修改數(shù)據(jù)的作用),所以我們需要小心跨站點(diǎn)請求偽造。 謝天謝地,你不必太過擔(dān)心,因?yàn)?Django 自帶了一個(gè)非常有用的防御系統(tǒng)。 簡而言之,所有針對內(nèi)部 URL 的 POST 表單都應(yīng)該使用 {% csrf_token %} 模板標(biāo)簽。
現(xiàn)在,讓我們來創(chuàng)建一個(gè) Django 視圖來處理提交的數(shù)據(jù)。在 第 3 部分 中,我們?yōu)橥镀睉?yīng)用創(chuàng)建了一個(gè) URLconf ,包含這一行:
path('<int:question_id>/vote/', views.vote, name='vote'),我們還創(chuàng)建了一個(gè) vote() 函數(shù)的虛擬實(shí)現(xiàn)。讓我們來創(chuàng)建一個(gè)真實(shí)的版本。 將下面的代碼添加到 polls/views.py :
from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reversefrom .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 += 1selected_choice.save()# 在成功處理POST數(shù)據(jù)后,始終返回HttpResponseRedirect。這可以防止在用戶點(diǎn)擊后退按鈕時(shí)兩次發(fā)布數(shù)據(jù)。return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))以上代碼中有些內(nèi)容還未在本教程中提到過:
- request.POST 是一個(gè)類字典對象,讓你可以通過關(guān)鍵字的名字獲取提交的數(shù)據(jù)。 這個(gè)例子中, request.POST[‘choice’] 以字符串形式返回選擇的 Choice 的 ID。 request.POST 的值永遠(yuǎn)是字符串。
注意,Django 還以同樣的方式提供 request.GET 用于訪問 GET 數(shù)據(jù) —— 但我們在代碼中顯式地使用 request.POST ,以保證數(shù)據(jù)只能通過 POST 調(diào)用改動(dòng)。 - 如果在 request.POST[‘choice’] 數(shù)據(jù)中沒有提供 choice , POST 將引發(fā)一個(gè) KeyError 。上面的代碼檢查 KeyError ,如果沒有給出 choice 將重新顯示 Question 表單和一個(gè)錯(cuò)誤信息。
- 在增加 Choice 的得票數(shù)之后,代碼返回一個(gè) HttpResponseRedirect 而不是常用的 HttpResponse 、 HttpResponseRedirect 只接收一個(gè)參數(shù):用戶將要被重定向的 URL。
- 在這個(gè)例子中,我們在 HttpResponseRedirect 的構(gòu)造函數(shù)中使用 reverse() 函數(shù)。這個(gè)函數(shù)避免了我們在視圖函數(shù)中硬編碼 URL。它需要我們給出我們想要跳轉(zhuǎn)的視圖的名字和該視圖所對應(yīng)的 URL 模式中需要給該視圖提供的參數(shù)。 在本例中,使用在 第 3 部分 中設(shè)定的 URLconf, reverse() 調(diào)用將返回一個(gè)這樣的字符串:'/polls/3/results/'
其中 3 是 question.id 的值。重定向的 URL 將調(diào)用 ‘results’ 視圖來顯示最終的頁面。
當(dāng)有人對 Question 進(jìn)行投票后, vote() 視圖將請求重定向到 Question 的結(jié)果界面。讓我們來編寫這個(gè)視圖:
from django.shortcuts import get_object_or_404, renderdef results(request, question_id):question = get_object_or_404(Question, pk=question_id)return render(request, 'polls/results.html', {'question': question})這和 第 3 部分 中的 detail() 視圖幾乎一模一樣,唯一的不同是模板的名字。 我們將在稍后解決這個(gè)冗余問題。
現(xiàn)在,創(chuàng)建一個(gè) 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)該看到一個(gè)投票結(jié)果頁面,并且在你每次投票之后都會(huì)更新。 如果你提交時(shí)沒有選擇任何 Choice,你應(yīng)該看到錯(cuò)誤信息。
注解
我們的 vote() 視圖代碼有一個(gè)小問題。代碼首先從數(shù)據(jù)庫中獲取了 selected_choice 對象,接著計(jì)算 vote 的新值,最后把值存回?cái)?shù)據(jù)庫。如果網(wǎng)站有兩個(gè)用戶同時(shí)投票,在同一時(shí)間,可能會(huì)導(dǎo)致問題。假如數(shù)據(jù)庫中存儲(chǔ)的是2,會(huì)被 votes 返回。然后,對于兩個(gè)用戶,新值3計(jì)算完畢,并被保存,但是期望值是4。
這個(gè)問題被稱為 競爭條件 。如果你對此有興趣,你可以閱讀 Avoiding race conditions using F() 來學(xué)習(xí)如何解決這個(gè)問題。
使用通用視圖:代碼還是少點(diǎn)好
detail() 和 results() 視圖都很精簡 —— 并且,像上面提到的那樣,存在冗余問題。用來顯示一個(gè)投票列表的 index() 視圖和它們類似。
這些視圖反映基本的 Web 開發(fā)中的一個(gè)常見情況:根據(jù) URL 中的參數(shù)從數(shù)據(jù)庫中獲取數(shù)據(jù)、載入模板文件然后返回渲染后的模板。 由于這種情況特別常見,Django 提供一種快捷方式,叫做“通用視圖”系統(tǒng)。
通用視圖將常見的模式抽象化,可以使你在編寫應(yīng)用時(shí)甚至不需要編寫Python代碼。
讓我們將我們的投票應(yīng)用轉(zhuǎn)換成使用通用視圖系統(tǒng),這樣我們可以刪除許多我們的代碼。我們僅僅需要做以下幾步來完成轉(zhuǎn)換,我們將:
請繼續(xù)閱讀來了解詳細(xì)信息。
改良 URLconf
首先,打開 polls/urls.py 這個(gè) URLconf 并將它修改成:
from django.urls import pathfrom . import viewsapp_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'), ]注意,第二個(gè)和第三個(gè)匹配準(zhǔn)則中,路徑字符串中匹配模式的名稱已經(jīng)由 <question_id> 改為 <pk>。
改良視圖
下一步,我們將刪除舊的 index, detail, 和 results 視圖,并用 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 genericfrom .models import Choice, Questionclass 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 = Questiontemplate_name = 'polls/detail.html'class ResultsView(generic.DetailView):model = Questiontemplate_name = 'polls/results.html'def vote(request, question_id):... # same as above, no changes needed.我們在這里使用兩個(gè)通用視圖: ListView 和 DetailView 。這兩個(gè)視圖分別抽象“顯示一個(gè)對象列表”和“顯示一個(gè)特定類型對象的詳細(xì)信息頁面”這兩種概念。
- 每個(gè)通用視圖需要知道它將作用于哪個(gè)模型。 這由 model 屬性提供。
- DetailView 期望從 URL 中捕獲名為 “pk” 的主鍵值,所以我們?yōu)橥ㄓ靡晥D把 question_id 改成 pk 。
默認(rèn)情況下,通用視圖 DetailView 使用一個(gè)叫做 <app name>/<model name>_detail.html 的模板。在我們的例子中,它將使用 “polls/question_detail.html” 模板。template_name 屬性是用來告訴 Django 使用一個(gè)指定的模板名字,而不是自動(dòng)生成的默認(rèn)名字。 我們也為 results 列表視圖指定了 template_name —— 這確保 results 視圖和 detail 視圖在渲染時(shí)具有不同的外觀,即使它們在后臺(tái)都是同一個(gè) DetailView 。
類似地,ListView 使用一個(gè)叫做 <app name>/<model name>_list.html 的默認(rèn)模板;我們使用 template_name 來告訴 ListView 使用我們創(chuàng)建的已經(jīng)存在的 “polls/index.html” 模板。
在之前的教程中,提供模板文件時(shí)都帶有一個(gè)包含 question 和 latest_question_list 變量的 context。對于 DetailView , question 變量會(huì)自動(dòng)提供—— 因?yàn)槲覀兪褂?Django 的模型 (Question), Django 能夠?yàn)?context 變量決定一個(gè)合適的名字。然而對于 ListView, 自動(dòng)生成的 context 變量是 question_list。為了覆蓋這個(gè)行為,我們提供 context_object_name 屬性,表示我們想使用 latest_question_list。作為一種替換方案,你可以改變你的模板來匹配新的 context 變量 —— 這是一種更便捷的方法,告訴 Django 使用你想使用的變量名。
啟動(dòng)服務(wù)器,使用一下基于通用視圖的新投票應(yīng)用。
當(dāng)你對你所寫的表單和通用視圖感到滿意后,下一節(jié)我們來了解如何測試我們的投票應(yīng)用。
總結(jié)
以上是生活随笔為你收集整理的编写你的第一个 Django 应用,第 4 部分的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 785. Is Graph Bipart
- 下一篇: LeetCode Algorithm 3