Django做一個投票應(yīng)用—第四章

編寫一個簡單的表單?

讓我們更新一下在上一個教程中編寫的投票詳細頁面的模板 ("polls/detail.html") ,讓它包含一個 HTML?<form>?元素:

polls/templates/polls/detail.html

<h1>{{question.question_text}}</h1>{%iferror_message%}<p><strong>{{error_message}}</strong></p>{%endif%}<formaction="{%url'polls:vote'question.id%}"method="post">{%csrf_token%}{%forchoiceinquestion.choice_set.all%}<inputtype="radio"name="choice"id="choice{{forloop.counter}}"value="{{choice.id}}"/><labelfor="choice{{forloop.counter}}">{{choice.choice_text}}</label><br/>{%endfor%}<inputtype="submit"value="Vote"/></form>

簡要說明:

上面的模板在 Question 的每個 Choice 前添加一個單選按鈕。 每個單選按鈕的?value?屬性是對應(yīng)的各個 Choice 的 ID。每個單選按鈕的?name?是?"choice"?减江。這意味著,當(dāng)有人選擇一個單選按鈕并提交表單提交時乏屯,它將發(fā)送一個 POST 數(shù)據(jù)?choice=#?柳骄,其中# 為選擇的 Choice 的 ID。這是 HTML 表單的基本概念仰迁。

我們設(shè)置表單的?action?為?{%?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 已經(jīng)擁有一個用來防御它的非常容易使用的系統(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

fromdjango.httpimportHttpResponse,HttpResponseRedirectfromdjango.shortcutsimportget_object_or_404,renderfromdjango.urlsimportreversefrom.modelsimportChoice,Question# ...defvote(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.returnrender(request,'polls/detail.html',{'question':question,'error_message':"You didn't select a choice.",})else:selected_choice.votes+=1selected_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.returnHttpResponseRedirect(reverse('polls:results',args=(question.id,)))

以上代碼中有些內(nèi)容還未在本教程中提到過:

request.POST?是一個類字典對象斟薇,讓你可以通過關(guān)鍵字的名字獲取提交的數(shù)據(jù)。 這個例子中恕酸,?request.POST['choice']?以字符串形式返回選擇的 Choice 的 ID堪滨。?request.POST?的值永遠是字符串。

注意蕊温,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 表單和一個錯誤信息。

在增加 Choice 的得票數(shù)之后筐咧,代碼返回一個?HttpResponseRedirect?而不是常用的?HttpResponse?鸯旁、?HttpResponseRedirect?只接收一個參數(shù):用戶將要被重定向的 URL(請繼續(xù)看下去,我們將會解釋如何構(gòu)造這個例子中的 URL)量蕊。

As the Python comment above points out, you should always return an?HttpResponseRedirect?after successfully dealing with POST data. This tip isn't specific to Django; it's just good Web development practice.

在這個例子中铺罢,我們在?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

fromdjango.shortcutsimportget_object_or_404,renderdefresults(request,question_id):question=get_object_or_404(Question,pk=question_id)returnrender(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>{%forchoiceinquestion.choice_set.all%}<li>{{choice.choice_text}}--{{choice.votes}}vote{{choice.votes|pluralize}}</li>{%endfor%}</ul><ahref="{%url'polls:detail'question.id%}">Vote again?</a>

現(xiàn)在叨襟,在你的瀏覽器中訪問?/polls/1/?然后為 Question 投票。你應(yīng)該看到一個投票結(jié)果頁面幔荒,并且在你每次投票之后都會更新糊闽。 如果你提交時沒有選擇任何 Choice,你應(yīng)該看到錯誤信息铺峭。

Note

我們的?vote()?視圖代碼有一個小問題墓怀。代碼首先從數(shù)據(jù)庫中獲取了?selected_choice?對象汽纠,接著計算?vote?的新值卫键,最后把值存回數(shù)據(jù)庫。如果網(wǎng)站有兩個方可同時投票在?同一時間?虱朵,可能會導(dǎo)致問題莉炉。同樣的值钓账,42,會被?votes?返回絮宁。然后梆暮,對于兩個用戶,新值43計算完畢绍昂,并被保存啦粹,但是期望值是44。

這個問題被稱為?競爭條件?窘游。如果你對此有興趣唠椭,你可以閱讀?Avoiding race conditions using F()?來學(xué)習(xí)如何解決這個問題。

使用通用視圖:代碼還是少點好?

detail()?(在?教程第3部分?中)和?results()?視圖都很簡單 —— 并且忍饰,像上面提到的那樣贪嫂,存在冗余問題。用來顯示一個投票列表的?index()?視圖(也在?教程第 3 部分?中)和它們類似艾蓝。

這些視圖反映基本的 Web 開發(fā)中的一個常見情況:根據(jù) URL 中的參數(shù)從數(shù)據(jù)庫中獲取數(shù)據(jù)力崇、載入模板文件然后返回渲染后的模板。 由于這種情況特別常見赢织,Django 提供一種快捷方式亮靴,叫做“通用視圖”系統(tǒng)。

通用視圖將常見的模式抽象化于置,可以使你在編寫應(yīng)用時甚至不需要編寫Python代碼台猴。

讓我們將我們的投票應(yīng)用轉(zhuǎn)換成使用通用視圖系統(tǒng),這樣我們可以刪除許多我們的代碼俱两。我們僅僅需要做以下幾步來完成轉(zhuǎn)換饱狂,我們將:

轉(zhuǎn)換 URLconf。

刪除一些舊的宪彩、不再需要的視圖休讳。

基于 Django 的通用視圖引入新的視圖。

請繼續(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

fromdjango.urlsimportpathfrom.importviewsapp_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'),]

注意奖地,第二個和第三個匹配準(zhǔn)則中,路徑字符串中匹配模式的名稱已經(jīng)由?<question_id>?改為?<pk>赋焕。

改良視圖?

下一步参歹,我們將刪除舊的?index,?detail, 和?results?視圖,并用 Django 的通用視圖代替隆判。打開?polls/views.py?文件犬庇,并將它修改成:

polls/views.py

fromdjango.httpimportHttpResponseRedirectfromdjango.shortcutsimportget_object_or_404,renderfromdjango.urlsimportreversefromdjango.viewsimportgenericfrom.modelsimportChoice,QuestionclassIndexView(generic.ListView):template_name='polls/index.html'context_object_name='latest_question_list'defget_queryset(self):"""Return the last five published questions."""returnQuestion.objects.order_by('-pub_date')[:5]classDetailView(generic.DetailView):model=Questiontemplate_name='polls/detail.html'classResultsView(generic.DetailView):model=Questiontemplate_name='polls/results.html'defvote(request,question_id):...# same as above, no changes needed.

我們在這里使用兩個通用視圖:?ListView?和?DetailView?。這兩個視圖分別抽象“顯示一個對象列表”和“顯示一個特定類型對象的詳細信息頁面”這兩種概念侨嘀。

每個通用視圖需要知道它將作用于哪個模型械筛。 這由?model?屬性提供。

DetailView?期望從 URL 中捕獲名為?"pk"?的主鍵值飒炎,所以我們?yōu)橥ㄓ靡晥D把?question_id?改成?pk?埋哟。

默認情況下,通用視圖?DetailView?使用一個叫做?<app?name>/<model?name>_detail.html?的模板郎汪。在我們的例子中赤赊,它將使用?"polls/question_detail.html"?模板。template_name?屬性是用來告訴 Django 使用一個指定的模板名字煞赢,而不是自動生成的默認名字抛计。 我們也為?results?列表視圖指定了?template_name?—— 這確保 results 視圖和 detail 視圖在渲染時具有不同的外觀,即使它們在后臺都是同一個?DetailView?照筑。

類似地吹截,ListView?使用一個叫做?<app?name>/<model?name>_list.html?的默認模板;我們使用?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)于通用視圖的詳細信息据块,請查看?通用視圖的文檔

當(dāng)你對你所寫的表單和通用視圖感到滿意后码邻,請閱讀?教程的第 5 部分?來了解如何測試我們的投票應(yīng)用折剃。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末另假,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子怕犁,更是在濱河造成了極大的恐慌边篮,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奏甫,死亡現(xiàn)場離奇詭異戈轿,居然都是意外死亡,警方通過查閱死者的電腦和手機阵子,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門思杯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人挠进,你說我怎么就攤上這事色乾。” “怎么了领突?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵暖璧,是天一觀的道長。 經(jīng)常有香客問我君旦,道長澎办,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任金砍,我火速辦了婚禮局蚀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘恕稠。我一直安慰自己至会,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布谱俭。 她就那樣靜靜地躺著奉件,像睡著了一般。 火紅的嫁衣襯著肌膚如雪昆著。 梳的紋絲不亂的頭發(fā)上县貌,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音凑懂,去河邊找鬼煤痕。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的摆碉。 我是一名探鬼主播塘匣,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼巷帝!你這毒婦竟也來了忌卤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤楞泼,失蹤者是張志新(化名)和其女友劉穎驰徊,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體堕阔,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡棍厂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了超陆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牺弹。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖时呀,靈堂內(nèi)的尸體忽然破棺而出张漂,到底是詐尸還是另有隱情,我是刑警寧澤退唠,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布鹃锈,位于F島的核電站,受9級特大地震影響瞧预,放射性物質(zhì)發(fā)生泄漏屎债。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一垢油、第九天 我趴在偏房一處隱蔽的房頂上張望盆驹。 院中可真熱鬧,春花似錦滩愁、人聲如沸躯喇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽廉丽。三九已至,卻和暖如春妻味,著一層夾襖步出監(jiān)牢的瞬間正压,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工责球, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留焦履,地道東北人拓劝。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像嘉裤,于是被迫代替她去往敵國和親郑临。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容