撰寫你的第一個 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 資料中未提供 choicerequest.POST['choice'] 將引發 KeyError。如果未給定 choice,上述程式碼會檢查 KeyError 並重新顯示包含錯誤訊息的問題表單。

  • F("votes") + 1 指示資料庫將投票計數增加 1。

  • 增加選項計數後,程式碼會傳回 HttpResponseRedirect,而不是一般的 HttpResponseHttpResponseRedirect 採用單一引數:使用者將重新導向的 URL(請參閱以下要點,瞭解我們如何在這種情況下建構 URL)。

    如同上面的 Python 註解所指出的,你應該在成功處理 POST 資料後一律傳回 HttpResponseRedirect。此提示並非 Django 特有;它是一般良好的網頁開發實務。

  • 在此範例中,我們在 HttpResponseRedirect 建構函式中使用 reverse() 函式。此函式有助於避免在檢視函式中硬式編碼 URL。它會收到我們想要傳遞控制權的檢視名稱,以及指向該檢視的 URL 模式的變數部分。在此案例中,使用我們在教學 3中設定的 URLconf,此 reverse() 呼叫會傳回類似以下的字串

    "/polls/3/results/"
    

    其中 3question.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 程式碼即可撰寫應用程式的程度。例如,ListViewDetailView 通用檢視分別抽象化「顯示物件清單」和「顯示特定物件類型的詳細資訊頁面」的概念。

讓我們將投票應用程式轉換為使用通用視圖系統,這樣我們就可以刪除一堆自己的程式碼。 我們需要幾個步驟來進行轉換。 我們將會

  1. 轉換 URLconf。

  2. 刪除一些舊的、不需要的視圖。

  3. 引入基於 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"

修改視圖

接下來,我們將移除舊的 indexdetailresults 視圖,並改用 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 用於 DetailViewResultsView)或透過定義 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" 範本。

在本教學課程的前幾個部分中,範本已提供包含 questionlatest_question_list 上下文變數的內容。 對於 DetailView,會自動提供 question 變數 – 因為我們正在使用 Django 模型 (Question),Django 能夠決定上下文變數的適當名稱。 然而,對於 ListView,自動產生的上下文變數是 question_list。 為了覆寫此設定,我們提供了 context_object_name 屬性,指定我們要改用 latest_question_list。 作為替代方法,您可以變更範本以符合新的預設上下文變數 – 但告訴 Django 使用您想要的變數要容易得多。

執行伺服器,並使用基於通用視圖的新投票應用程式。

有關通用視圖的完整詳細資訊,請參閱通用視圖文件

當您熟悉表單和通用視圖時,請閱讀本教學課程的第 5 部分,以了解如何測試我們的投票應用程式。

返回頂部