Django4.0 開始-編寫你的第一個Django應(yīng)用,第4部分

2022-03-16 18:05 更新

編寫一個簡單的表單

讓我們更新一下在上一個教程中編寫的投票詳細頁面的模板 ("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>

簡要說明:

  • 上面的模板在 Question 的每個 ?Choice ?前添加一個單選按鈕。 每個單選按鈕的 ?value ?屬性是對應(yīng)的各個 ?Choice ?的 ID。每個單選按鈕的 ?name ?是 ?"choice"? 。這意味著,當有人選擇一個單選按鈕并提交表單提交時,它將發(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ù)。當你創(chuàng)建一個改變服務(wù)器端數(shù)據(jù)的表單時,使用 ?method="post"?。這不是 Django 的特定技巧;這是優(yōu)秀的網(wǎng)站開發(fā)技巧。
  • ?forloop.counter? 指示 ?for ?標簽已經(jīng)循環(huán)多少次。
  • 由于我們創(chuàng)建一個 POST 表單(它具有修改數(shù)據(jù)的作用),所以我們需要小心跨站點請求偽造。你不必太過擔心,因為 Django 自帶了一個非常有用的防御系統(tǒng)。 簡而言之,所有針對內(nèi)部 URL 的 POST 表單都應(yīng)該使用 ?{% csrf_token %}? 模板標簽。

現(xiàn)在,讓我們來創(chuàng)建一個 Django 視圖來處理提交的數(shù)據(jù)。記住,在 教程第 3 部分 中,我們?yōu)橥镀睉?yīng)用創(chuàng)建了一個 URLconf ,包含這一行:

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

我們還創(chuàng)建了一個 ?vote()? 函數(shù)的虛擬實現(xiàn)。讓我們來創(chuàng)建一個真實的版本。 將下面的代碼添加到 ?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):
        # 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 += 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? 的值永遠是字符串。注意,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)。正如上面的 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 ?對象。

當有人對 Question 進行投票后, ?vote()? 視圖將請求重定向到 Question 的結(jié)果界面。讓我們來編寫這個視圖:

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? 模板:

<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)該看到錯誤信息。

注意:

我們的 vote() 視圖代碼有一個小問題。代碼首先從數(shù)據(jù)庫中獲取了 ?selected_choice對象,接著計算 ?vote的新值,最后把值存回數(shù)據(jù)庫。如果網(wǎng)站有兩個方可同時投票在 同一時間 ,可能會導致問題。同樣的值,42,會被 ?votes返回。然后,對于兩個用戶,新值43計算完畢,并被保存,但是期望值是44。這個問題被稱為 競爭條件 。

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

?detail() ?(在 教程第 3 部分 中)和 ?results()? 視圖都很精簡 —— 并且,像上面提到的那樣,存在冗余問題。用來顯示一個投票列表的 ?index() ?視圖(也在 教程第 3 部分 中)和它們類似。
這些視圖反映基本的網(wǎng)絡(luò)開發(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 的通用視圖引入新的視圖。

改良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'),
]

注意,第二個和第三個匹配準則中,路徑字符串中匹配模式的名稱已經(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 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.

我們在這里使用兩個通用視圖: ?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)用。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號