Django 教程第 9 部分:處理表單

在本教程中,我們將向你展示如何在 Django 中使用 HTML 表單,特別是編寫表單以建立、更新和刪除模型例項的最簡單方法。作為演示的一部分,我們將擴充套件LocalLibrary 網站,以便圖書管理員可以使用我們自己的表單(而不是使用管理應用程式)續借圖書,並建立、更新和刪除作者。

預備知識 完成所有之前的教程主題,包括Django 教程第 8 部分:使用者身份驗證和許可權
目標 理解如何編寫表單來從使用者獲取資訊並更新資料庫。理解基於類的通用編輯檢視如何極大地簡化建立用於處理單個模型的表單。

概述

HTML 表單是網頁上一個或多個欄位/小部件的集合,可用於收集使用者提交給伺服器的資訊。表單是收集使用者輸入的靈活機制,因為有適合輸入多種不同型別資料的部件,包括文字框、複選框、單選按鈕、日期選擇器等等。表單也是一種相對安全地與伺服器共享資料的方式,因為它們允許我們透過帶有跨站點請求偽造保護的 POST 請求傳送資料。

雖然我們到目前為止還沒有在本教程中建立任何表單,但我們已經在 Django 管理站點中遇到過它們——例如,下面的截圖顯示了一個用於編輯我們的 Book 模型之一的表單,它由許多選擇列表和文字編輯器組成。

Admin Site - Book Add

處理表單可能很複雜!開發人員需要編寫表單的 HTML,在伺服器端(可能也在瀏覽器端)驗證並正確清理輸入的資料,重新提交帶有錯誤訊息的表單以通知使用者任何無效欄位,在資料成功提交後處理資料,最後以某種方式響應使用者以指示成功。 Django 表單透過提供一個框架,允許你以程式設計方式定義表單及其欄位,然後使用這些物件生成表單 HTML 程式碼並處理大部分驗證和使用者互動,從而大大減輕了所有這些步驟的工作量。

在本教程中,我們將向你展示建立和使用表單的一些方法,特別是通用編輯檢視如何顯著減少建立用於操作模型的表單所需的工作量。在此過程中,我們將透過新增一個表單來允許圖書管理員續借圖書,從而擴充套件我們的 LocalLibrary 應用程式,並且我們將建立頁面來建立、編輯和刪除圖書和作者(重新實現上面所示的編輯圖書表單的基本版本)。

HTML 表單

首先,簡單介紹一下 HTML 表單。考慮一個簡單的 HTML 表單,其中包含一個用於輸入“團隊”名稱的文字欄位及其關聯的標籤。

Simple name field example in HTML form

表單在 HTML 中定義為 <form>…</form> 標籤內元素的集合,其中至少包含一個 type="submit"input 元素。

html
<form action="/team_name_url/" method="post">
  <label for="team_name">Enter name: </label>
  <input
    id="team_name"
    type="text"
    name="name_field"
    value="Default name for team." />
  <input type="submit" value="OK" />
</form>

雖然這裡我們只有一個用於輸入團隊名稱的文字欄位,但表單*可能*有任意數量的其他輸入元素及其關聯的標籤。欄位的 type 屬性定義了將顯示哪種小部件。欄位的 nameid 用於在 JavaScript/CSS/HTML 中識別該欄位,而 value 定義了欄位首次顯示時的初始值。匹配的團隊標籤使用 label 標籤指定(見上面的“輸入名稱”),其 for 欄位包含關聯 inputid 值。

submit 輸入將預設顯示為一個按鈕。可以按下此按鈕將表單中所有其他輸入元素(在本例中,只有 team_name 欄位)中的資料上傳到伺服器。表單屬性定義了用於傳送資料的 HTTP method 以及伺服器上資料的目標 (action)。

  • action:當表單提交時,資料將被髮送到何處進行處理的資源/URL。如果未設定(或設定為空字串),則表單將提交回當前頁面 URL。
  • method:用於傳送資料的 HTTP 方法:postget
    • 如果資料將導致伺服器資料庫發生更改,則應始終使用 POST 方法,因為它能夠更有效地抵禦跨站點偽造請求攻擊。
    • GET 方法只應用於不更改使用者資料的表單(例如,搜尋表單)。建議在需要能夠書籤或共享 URL 時使用它。

伺服器的作用是首先呈現表單的初始狀態——要麼包含空白欄位,要麼預先填充初始值。使用者按下提交按鈕後,伺服器將接收來自 Web 瀏覽器的資料並必須驗證資訊。如果表單包含無效資料,伺服器應再次顯示錶單,這次在“有效”欄位中顯示使用者輸入的資料,並在無效欄位旁顯示描述問題的訊息。一旦伺服器收到包含所有有效表單資料的請求,它就可以執行適當的操作(例如:儲存資料、返回搜尋結果、上傳檔案等),然後通知使用者。

正如你所想象的,建立 HTML、驗證返回的資料、在需要時重新顯示帶錯誤報告的輸入資料,以及對有效資料執行所需操作,所有這些都可能需要大量的努力才能“做對”。Django 透過省去一些繁重和重複的程式碼,讓這一切變得容易得多!

Django 表單處理流程

Django 的表單處理使用我們之前教程中學到的所有相同技術(用於顯示模型資訊):檢視獲取請求,執行所需的任何操作,包括從模型讀取資料,然後生成並返回一個 HTML 頁面(來自模板,我們將上下文傳遞到其中,上下文包含要顯示的資料)。更復雜的是,伺服器還需要能夠處理使用者提供的資料,並在出現任何錯誤時重新顯示頁面。

下面顯示了 Django 如何處理表單請求的流程圖,從請求包含表單的頁面(綠色顯示)開始。

Updated form handling process doc.

根據上圖,Django 表單處理主要完成以下事項:

  1. 使用者首次請求時顯示預設表單。

    • 如果你正在建立新記錄,表單可能包含空白欄位,或者它可能預先填充了初始值(例如,如果你正在更改記錄,或者有有用的預設初始值)。
    • 此時表單被稱為*未繫結*,因為它沒有與任何使用者輸入的資料關聯(儘管它可能有初始值)。
  2. 接收來自提交請求的資料並將其繫結到表單。

    • 將資料繫結到表單意味著當我們需要重新顯示錶單時,使用者輸入的資料和任何錯誤都可用。
  3. 清理和驗證資料。

    • 清理資料會執行輸入欄位的清理,例如刪除可能用於向伺服器傳送惡意內容的無效字元,並將它們轉換為一致的 Python 型別。
    • 驗證檢查值是否適合該欄位(例如,它們是否在正確的日期範圍內,是否太短或太長等)
  4. 如果任何資料無效,重新顯示錶單,這次顯示任何使用者填充的值以及問題欄位的錯誤訊息。

  5. 如果所有資料都有效,則執行所需操作(例如儲存資料、傳送電子郵件、返回搜尋結果、上傳檔案等)。

  6. 所有操作完成後,將使用者重定向到另一個頁面。

Django 提供了許多工具和方法來幫助你完成上述任務。最基本的是 Form 類,它簡化了表單 HTML 的生成和資料清理/驗證。在下一節中,我們將透過圖書管理員續借圖書頁面的實際示例來描述表單的工作原理。

注意:理解 Form 的用法將有助於你理解我們稍後討論的 Django 更“高階”的表單框架類。

使用表單和函式檢視續借圖書表單

接下來,我們將新增一個頁面,允許圖書管理員續借已借出的圖書。為此,我們將建立一個表單,允許使用者輸入一個日期值。我們將用當前日期起 3 周的初始值(正常借閱期)填充該欄位,並新增一些驗證,以確保圖書管理員不能輸入過去的日期或未來過遠的日期。輸入有效日期後,我們會將其寫入當前記錄的 BookInstance.due_back 欄位。

該示例將使用基於函式的檢視和 Form 類。以下部分解釋了表單的工作原理,以及你需要對我們正在進行的 LocalLibrary 專案進行的更改。

表單

Form 類是 Django 表單處理系統的核心。它指定了表單中的欄位、它們的佈局、顯示小部件、標籤、初始值、有效值,以及(驗證後)與無效欄位關聯的錯誤訊息。該類還提供了使用預定義格式(表格、列表等)在模板中呈現自身的方法,或獲取任何元素值的方法(允許細粒度手動呈現)。

宣告表單

Form 的宣告語法與 Model 的宣告語法非常相似,並共享相同的欄位型別(以及一些相似的引數)。這很有道理,因為在這兩種情況下,我們都需要確保每個欄位處理正確型別的資料,被約束為有效資料,並具有用於顯示/文件的描述。

表單資料儲存在應用程式目錄內的應用程式 forms.py 檔案中。建立並開啟檔案 django-locallibrary-tutorial/catalog/forms.py。要建立 Form,我們匯入 forms 庫,從 Form 類派生,並宣告表單的欄位。下面是我們的圖書續借表單的一個非常基本的表單類——將其新增到你的新檔案中:

python
from django import forms

class RenewBookForm(forms.Form):
    renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).")

表單欄位

在本例中,我們有一個單獨的DateField,用於輸入續借日期,它將在 HTML 中以空白值、預設標籤“續借日期:”和一些有用的使用文字:“輸入現在到 4 周之間的日期(預設 3 周)。”呈現。由於沒有指定其他可選引數,該欄位將接受使用input_formats(YYYY-MM-DD (2024-11-06)、MM/DD/YYYY (02/26/2024)、MM/DD/YY (10/25/24))的日期,並將使用預設的widgetDateInput進行渲染。

還有許多其他型別的表單欄位,你會發現它們與等效的模型欄位類非常相似:

大多數字段通用的引數如下所示(它們具有合理的預設值):

  • required:如果為 True,則欄位不能為空白或給定 None 值。欄位預設是必需的,因此你將設定 required=False 以允許表單中出現空白值。
  • label:在 HTML 中渲染欄位時使用的標籤。如果未指定 label,Django 將透過將第一個字母大寫並將下劃線替換為空格來從欄位名稱建立標籤(例如,Renewal date)。
  • label_suffix:預設情況下,標籤後面會顯示一個冒號(例如,Renewal date​:)。此引數允許你指定包含其他字元的不同字尾。
  • initial:表單顯示時欄位的初始值。
  • widget:要使用的顯示小部件。
  • help_text(如上例所示):可在表單中顯示的額外文字,用於解釋如何使用該欄位。
  • error_messages:欄位的錯誤訊息列表。如果需要,你可以用自己的訊息覆蓋這些訊息。
  • validators:驗證欄位時將呼叫的函式列表。
  • localize:啟用表單資料輸入的本地化(更多資訊請參閱連結)。
  • disabled:如果此項為 True,則顯示該欄位但其值不能編輯。預設值為 False

驗證

Django 提供了許多地方來驗證你的資料。驗證單個欄位最簡單的方法是覆蓋你想要檢查的欄位的 clean_<field_name>() 方法。因此,例如,我們可以透過實現 clean_renewal_date() 來驗證輸入的 renewal_date 值是否在現在和 4 周之間,如下所示。

更新你的 forms.py 檔案,使其看起來像這樣:

python
import datetime

from django import forms

from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

class RenewBookForm(forms.Form):
    renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).")

    def clean_renewal_date(self):
        data = self.cleaned_data['renewal_date']

        # Check if a date is not in the past.
        if data < datetime.date.today():
            raise ValidationError(_('Invalid date - renewal in past'))

        # Check if a date is in the allowed range (+4 weeks from today).
        if data > datetime.date.today() + datetime.timedelta(weeks=4):
            raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead'))

        # Remember to always return the cleaned data.
        return data

有兩點需要注意。首先,我們使用 self.cleaned_data['renewal_date'] 獲取資料,並且無論我們是否在函式末尾更改資料,我們都會返回此資料。此步驟透過使用預設驗證器獲取“清理”和淨化潛在不安全輸入的資料,並將其轉換為資料正確的標準型別(在本例中為 Python datetime.datetime 物件)。

第二點是,如果一個值超出我們的範圍,我們會引發 ValidationError,並指定在輸入無效值時希望在表單中顯示的錯誤文字。上面的例子還將此文字包裝在 Django 的翻譯函式 gettext_lazy()(作為 _() 匯入)中,如果你以後想翻譯你的網站,這是一個很好的做法。

注意:表單和欄位驗證(Django 文件)中,還有許多其他驗證表單的方法和示例。例如,在有多個相互依賴的欄位的情況下,你可以覆蓋Form.clean() 函式,並再次引發 ValidationError

這就是這個表單所需要的一切!

URL 配置

在我們建立檢視之前,讓我們為續借圖書頁面新增一個 URL 配置。將以下配置複製到 django-locallibrary-tutorial/catalog/urls.py 的底部:

python
urlpatterns += [
    path('book/<uuid:pk>/renew/', views.renew_book_librarian, name='renew-book-librarian'),
]

URL 配置會將格式為 /catalog/book/<bookinstance_id>/renew/ 的 URL 重定向到 views.py 中名為 renew_book_librarian() 的函式,並將 BookInstance ID 作為名為 pk 的引數傳送。該模式僅在 pk 是格式正確的 uuid 時才匹配。

注意:我們可以隨意命名捕獲的 URL 資料,因為我們完全控制檢視函式(我們沒有使用期望特定名稱引數的通用詳細檢視類)。但是,pk 是“主鍵”的縮寫,這是一個合理的約定!

檢視

如上文Django 表單處理流程所述,檢視在首次呼叫時必須渲染預設表單,然後如果資料無效,則重新渲染帶有錯誤訊息的表單,或者如果資料有效,則處理資料並重定向到新頁面。為了執行這些不同的操作,檢視必須能夠知道它是在首次呼叫以渲染預設表單,還是在後續呼叫以驗證資料。

對於使用 POST 請求將資訊提交到伺服器的表單,最常見的模式是檢視針對 POST 請求型別進行測試(if request.method == 'POST':)以識別表單驗證請求,並使用 GET(使用 else 條件)來識別初始表單建立請求。如果你想使用 GET 請求提交資料,那麼識別這是第一次還是後續檢視呼叫的典型方法是讀取表單資料(例如,讀取表單中的隱藏值)。

圖書續借流程將寫入我們的資料庫,因此,按照慣例,我們使用 POST 請求方法。下面的程式碼片段顯示了此類函式檢視的(非常標準的)模式。

python
import datetime

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse

from catalog.forms import RenewBookForm

def renew_book_librarian(request, pk):
    book_instance = get_object_or_404(BookInstance, pk=pk)

    # If this is a POST request then process the Form data
    if request.method == 'POST':

        # Create a form instance and populate it with data from the request (binding):
        form = RenewBookForm(request.POST)

        # Check if the form is valid:
        if form.is_valid():
            # process the data in form.cleaned_data as required (here we just write it to the model due_back field)
            book_instance.due_back = form.cleaned_data['renewal_date']
            book_instance.save()

            # redirect to a new URL:
            return HttpResponseRedirect(reverse('all-borrowed'))

    # If this is a GET (or any other method) create the default form.
    else:
        proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
        form = RenewBookForm(initial={'renewal_date': proposed_renewal_date})

    context = {
        'form': form,
        'book_instance': book_instance,
    }

    return render(request, 'catalog/book_renew_librarian.html', context)

首先,我們匯入表單 (RenewBookForm) 和檢視函式主體中使用的許多其他有用的物件/方法。

  • get_object_or_404():根據主鍵值從模型中返回指定物件,如果記錄不存在,則引發 Http404 異常(未找到)。
  • HttpResponseRedirect:這將建立到指定 URL 的重定向(HTTP 狀態碼 302)。
  • reverse():這會根據 URL 配置名稱和一組引數生成 URL。它等同於我們在模板中使用的 url 標籤的 Python 版本。
  • datetime:一個用於處理日期和時間的 Python 庫。

在檢視中,我們首先在 get_object_or_404() 中使用 pk 引數來獲取當前的 BookInstance(如果不存在,檢視將立即退出,頁面將顯示“未找到”錯誤)。如果這*不是* POST 請求(由 else 子句處理),那麼我們建立預設表單,為 renewal_date 欄位傳入一個 initial 值,即從當前日期起 3 周。

python
book_instance = get_object_or_404(BookInstance, pk=pk)

# If this is a GET (or any other method) create the default form
else:
    proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
    form = RenewBookForm(initial={'renewal_date': proposed_renewal_date})

context = {
    'form': form,
    'book_instance': book_instance,
}

return render(request, 'catalog/book_renew_librarian.html', context)

建立表單後,我們呼叫 render() 來建立 HTML 頁面,指定模板和包含我們表單的上下文。在這種情況下,上下文還包含我們的 BookInstance,我們將在模板中使用它來提供有關我們正在續借的圖書的資訊。

然而,如果這是一個 POST 請求,那麼我們建立 form 物件並用請求中的資料填充它。這個過程稱為“繫結”,它允許我們驗證表單。

然後我們檢查表單是否有效,這會執行所有欄位上的所有驗證程式碼——包括檢查日期欄位是否實際是有效日期的通用程式碼,以及我們特定表單的 clean_renewal_date() 函式來檢查日期是否在正確範圍內的程式碼。

python
book_instance = get_object_or_404(BookInstance, pk=pk)

# If this is a POST request then process the Form data
if request.method == 'POST':

    # Create a form instance and populate it with data from the request (binding):
    form = RenewBookForm(request.POST)

    # Check if the form is valid:
    if form.is_valid():
        # process the data in form.cleaned_data as required (here we just write it to the model due_back field)
        book_instance.due_back = form.cleaned_data['renewal_date']
        book_instance.save()

        # redirect to a new URL:
        return HttpResponseRedirect(reverse('all-borrowed'))

context = {
    'form': form,
    'book_instance': book_instance,
}

return render(request, 'catalog/book_renew_librarian.html', context)

如果表單無效,我們再次呼叫 render(),但這次上下文中傳遞的表單值將包含錯誤訊息。

如果表單有效,那麼我們就可以開始使用資料了,透過 form.cleaned_data 屬性訪問它(例如,data = form.cleaned_data['renewal_date'])。在這裡,我們只是將資料儲存到關聯 BookInstance 物件的 due_back 值中。

警告:雖然你也可以直接透過請求訪問表單資料(例如,request.POST['renewal_date'] 或在使用 GET 請求時為 request.GET['renewal_date']),但不建議這樣做。清理後的資料經過淨化、驗證並轉換為 Python 友好的型別。

檢視中表單處理部分的最後一步是重定向到另一個頁面,通常是“成功”頁面。在本例中,我們使用 HttpResponseRedirectreverse() 重定向到名為 'all-borrowed' 的檢視(這是在Django 教程第 8 部分:使用者身份驗證和許可權中作為“挑戰”建立的)。如果你沒有建立該頁面,請考慮重定向到 URL / 的主頁)。

這是表單處理本身所需的一切,但我們仍然需要限制檢視的訪問,只允許具有續借圖書許可權的已登入圖書管理員訪問。我們使用 @login_required 來要求使用者登入,並使用 @permission_required 函式裝飾器和我們現有的 can_mark_returned 許可權來允許訪問(裝飾器按順序處理)。請注意,我們可能應該在 BookInstance 中建立新的許可權設定 (can_renew),但為了簡化示例,我們將重用現有許可權。

因此,最終的檢視如下所示。請將其複製到 django-locallibrary-tutorial/catalog/views.py 的底部。

python
import datetime

from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse

from catalog.forms import RenewBookForm

@login_required
@permission_required('catalog.can_mark_returned', raise_exception=True)
def renew_book_librarian(request, pk):
    """View function for renewing a specific BookInstance by librarian."""
    book_instance = get_object_or_404(BookInstance, pk=pk)

    # If this is a POST request then process the Form data
    if request.method == 'POST':

        # Create a form instance and populate it with data from the request (binding):
        form = RenewBookForm(request.POST)

        # Check if the form is valid:
        if form.is_valid():
            # process the data in form.cleaned_data as required (here we just write it to the model due_back field)
            book_instance.due_back = form.cleaned_data['renewal_date']
            book_instance.save()

            # redirect to a new URL:
            return HttpResponseRedirect(reverse('all-borrowed'))

    # If this is a GET (or any other method) create the default form.
    else:
        proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3)
        form = RenewBookForm(initial={'renewal_date': proposed_renewal_date})

    context = {
        'form': form,
        'book_instance': book_instance,
    }

    return render(request, 'catalog/book_renew_librarian.html', context)

模板

建立檢視中引用的模板(/catalog/templates/catalog/book_renew_librarian.html)並將下面的程式碼複製到其中。

django
{% extends "base_generic.html" %}

{% block content %}
  <h1>Renew: {{ book_instance.book.title }}</h1>
  <p>Borrower: {{ book_instance.borrower }}</p>
  <p {% if book_instance.is_overdue %} class="text-danger"{% endif %} >Due date: {{ book_instance.due_back }}</p>

  <form action="" method="post">
    {% csrf_token %}
    <table>
    {{ form.as_table }}
    </table>
    <input type="submit" value="Submit">
  </form>
{% endblock %}

這些大部分內容對於之前的教程來說是完全熟悉的。

我們擴充套件了基本模板,然後重新定義了內容塊。我們可以引用 {{ book_instance }}(及其變數),因為它已在 render() 函式中傳遞到上下文物件中,我們使用這些變數來列出書名、借閱者和原始到期日。

表單程式碼相對簡單。首先,我們宣告 form 標籤,指定表單提交的位置 (action) 和提交資料的方法 (在本例中為 POST) — 如果你還記得頁面頂部HTML 表單概述,空 action 如所示,意味著表單資料將釋出回當前頁面 URL (這正是我們想要的)。在標籤內部,我們定義了 submit 輸入,使用者可以按下該輸入來提交資料。表單標籤內新增的 {% csrf_token %} 是 Django 跨站點偽造保護的一部分。

注意:{% csrf_token %} 新增到你建立的每個使用 POST 提交資料的 Django 模板中。這將減少表單被惡意使用者劫持的可能性。

剩下就是 {{ form }} 模板變量了,我們已將其傳遞到上下文字典中的模板。毫不奇怪,如所示使用時,它提供了所有表單欄位的預設渲染,包括它們的標籤、小部件和幫助文字——渲染結果如下所示:

html
<tr>
  <th><label for="id_renewal_date">Renewal date:</label></th>
  <td>
    <input
      id="id_renewal_date"
      name="renewal_date"
      type="text"
      value="2023-11-08"
      required />
    <br />
    <span class="helptext">
      Enter date between now and 4 weeks (default 3 weeks).
    </span>
  </td>
</tr>

注意:這可能不太明顯,因為我們只有一個欄位,但預設情況下,每個欄位都定義在自己的表格行中。如果你引用模板變數 {{ form.as_table }},也會提供相同的渲染。

如果你輸入了一個無效日期,你還會得到一個渲染在頁面上的錯誤列表(參見下面的 error-list)。

html
<tr>
  <th><label for="id_renewal_date">Renewal date:</label></th>
  <td>
    <ul class="error-list">
      <li>Invalid date - renewal in past</li>
    </ul>
    <input
      id="id_renewal_date"
      name="renewal_date"
      type="text"
      value="2023-11-08"
      required />
    <br />
    <span class="helptext">
      Enter date between now and 4 weeks (default 3 weeks).
    </span>
  </td>
</tr>

使用表單模板變數的其他方式

如上所示,使用 {{ form.as_table }},每個欄位都渲染為表格行。你也可以將每個欄位渲染為列表項(使用 {{ form.as_ul }})或段落(使用 {{ form.as_p }})。

還可以透過使用點符號索引其屬性來完全控制表單每個部分的渲染。因此,例如,我們可以訪問 renewal_date 欄位的許多單獨項:

  • {{ form.renewal_date }}:整個欄位。
  • {{ form.renewal_date.errors }}:錯誤列表。
  • {{ form.renewal_date.id_for_label }}:標籤的 ID。
  • {{ form.renewal_date.help_text }}:欄位幫助文字。

有關如何在模板中手動渲染表單和動態遍歷模板欄位的更多示例,請參閱使用表單 > 手動渲染欄位(Django 文件)。

測試頁面

如果你接受了Django 教程第 8 部分:使用者身份驗證和許可權中的“挑戰”,你將看到一個顯示圖書館中所有借出圖書的檢視,該檢視僅對圖書館工作人員可見。該檢視可能看起來與此類似:

django
{% extends "base_generic.html" %}

{% block content %}
    <h1>All Borrowed Books</h1>

    {% if bookinstance_list %}
    <ul>

      {% for bookinst in bookinstance_list %}
      <li class="{% if bookinst.is_overdue %}text-danger{% endif %}">
        <a href="{% url 'book-detail' bookinst.book.pk %}">{{ bookinst.book.title }}</a> ({{ bookinst.due_back }}) {% if user.is_staff %}- {{ bookinst.borrower }}{% endif %}
      </li>
      {% endfor %}
    </ul>

    {% else %}
      <p>There are no books borrowed.</p>
    {% endif %}
{% endblock %}

我們可以透過將以下模板程式碼附加到上面的列表項文字旁邊,為每個專案新增一個指向圖書續借頁面的連結。請注意,此模板程式碼只能在 {% for %} 迴圈內執行,因為 bookinst 值在此處定義。

django
{% if perms.catalog.can_mark_returned %}- <a href="{% url 'renew-book-librarian' bookinst.id %}">Renew</a>{% endif %}

注意:請記住,你的測試登入需要具有 catalog.can_mark_returned 許可權才能看到上面新增的新“續借”連結並訪問連結頁面(也許使用你的超級使用者帳戶)。

你也可以手動構建一個測試 URL,例如:http://127.0.0.1:8000/catalog/book/<bookinstance_id>/renew/(透過導航到圖書館的圖書詳細資訊頁面並複製 id 欄位可以獲取有效的 bookinstance_id)。

它看起來怎麼樣?

如果成功,預設表單將如下所示:

Default form which displays the book details, due date, renewal date and a submit button appears in case the link works successfully

輸入無效值的表單將如下所示:

Same form as above with an error message: invalid date - renewal in the past

包含續借連結的所有圖書列表將如下所示:

Displays list of all renewed books along with their details. Past due is in red.

模型表單(ModelForms)

使用上述方法建立 Form 類非常靈活,允許你建立任何型別的表單頁面,並將其與任何一個或多個模型關聯。

然而,如果你只需要一個表單來對映*單個*模型的欄位,那麼你的模型將已經定義了表單中所需的大部分資訊:欄位、標籤、幫助文字等等。與其在表單中重新建立模型定義,不如使用ModelForm 輔助類從模型建立表單。然後,這個 ModelForm 可以在你的檢視中以與普通 Form 完全相同的方式使用。

下面顯示了一個包含與我們原始 RenewBookForm 相同欄位的基本 ModelForm。建立表單所需做的就是新增帶有相關 model (BookInstance) 和要包含在表單中的模型 fields 列表的 class Meta

python
from django.forms import ModelForm

from catalog.models import BookInstance

class RenewBookModelForm(ModelForm):
    class Meta:
        model = BookInstance
        fields = ['due_back']

注意:你也可以使用 fields = '__all__' 包含表單中的所有欄位,或者你可以使用 exclude(而不是 fields)來指定模型中*不*包含的欄位)。

這兩種方法都不推薦,因為新增到模型的新欄位會自動包含在表單中(開發人員不一定會考慮可能的安全隱患)。

注意:這看起來可能沒有比僅僅使用 Form 簡單多少(在這種情況下,確實沒有,因為我們只有一個欄位)。然而,如果你有很多欄位,它可以大大減少所需的程式碼量!

其餘資訊來自模型欄位定義(例如,標籤、小部件、幫助文字、錯誤訊息)。如果這些資訊不太正確,我們可以在 class Meta 中覆蓋它們,指定一個包含要更改的欄位及其新值的字典。例如,在此表單中,我們可能希望欄位的標籤為“續借日期”(而不是基於欄位名稱的預設標籤:到期日期),並且我們還希望我們的幫助文字特定於此用例。下面的 Meta 向你展示瞭如何覆蓋這些欄位,如果預設值不足,你也可以類似地設定 widgetserror_messages

python
class Meta:
    model = BookInstance
    fields = ['due_back']
    labels = {'due_back': _('New renewal date')}
    help_texts = {'due_back': _('Enter a date between now and 4 weeks (default 3).')}

要新增驗證,你可以使用與普通 Form 相同的方法——你定義一個名為 clean_<field_name>() 的函式,併為無效值引發 ValidationError 異常。與我們原始表單唯一的區別是模型欄位名為 due_back 而不是 renewal_date。此更改是必需的,因為 BookInstance 中對應的欄位名為 due_back

python
from django.forms import ModelForm

from catalog.models import BookInstance

class RenewBookModelForm(ModelForm):
    def clean_due_back(self):
       data = self.cleaned_data['due_back']

       # Check if a date is not in the past.
       if data < datetime.date.today():
           raise ValidationError(_('Invalid date - renewal in past'))

       # Check if a date is in the allowed range (+4 weeks from today).
       if data > datetime.date.today() + datetime.timedelta(weeks=4):
           raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead'))

       # Remember to always return the cleaned data.
       return data

    class Meta:
        model = BookInstance
        fields = ['due_back']
        labels = {'due_back': _('Renewal date')}
        help_texts = {'due_back': _('Enter a date between now and 4 weeks (default 3).')}

上面的 RenewBookModelForm 類現在在功能上等同於我們原來的 RenewBookForm。你可以匯入並使用它,無論你在何處使用 RenewBookForm,只要你同時將相應的表單變數名從 renewal_date 更新為 due_back,就像第二個表單宣告中那樣:RenewBookModelForm(initial={'due_back': proposed_renewal_date})

通用編輯檢視

我們在上面的函式檢視示例中使用的表單處理演算法代表了表單編輯檢視中極其常見的模式。Django 透過為建立、編輯和刪除基於模型的檢視建立通用編輯檢視,為你抽象了大部分這種“樣板”程式碼。這些檢視不僅處理“檢視”行為,還會自動為你從模型建立表單類(一個 ModelForm)。

注意:除了此處描述的編輯檢視之外,還有一個 FormView 類,它在“靈活性”與“編碼工作量”方面介於我們的函式檢視和其他通用檢視之間。使用 FormView,你仍然需要建立你的 Form,但你不必實現所有標準的表單處理模式。相反,你只需提供一個在提交被認為是有效後將呼叫的函式的實現。

在本節中,我們將使用通用編輯檢視來建立頁面,以新增建立、編輯和刪除圖書館中 Author 記錄的功能——有效地提供了管理站點部分的重新實現(如果你需要以比管理站點提供更靈活的方式提供管理功能,這可能很有用)。

檢視

開啟檢視檔案(django-locallibrary-tutorial/catalog/views.py)並將以下程式碼塊附加到其底部:

python
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Author

class AuthorCreate(PermissionRequiredMixin, CreateView):
    model = Author
    fields = ['first_name', 'last_name', 'date_of_birth', 'date_of_death']
    initial = {'date_of_death': '11/11/2023'}
    permission_required = 'catalog.add_author'

class AuthorUpdate(PermissionRequiredMixin, UpdateView):
    model = Author
    # Not recommended (potential security issue if more fields added)
    fields = '__all__'
    permission_required = 'catalog.change_author'

class AuthorDelete(PermissionRequiredMixin, DeleteView):
    model = Author
    success_url = reverse_lazy('authors')
    permission_required = 'catalog.delete_author'

    def form_valid(self, form):
        try:
            self.object.delete()
            return HttpResponseRedirect(self.success_url)
        except Exception as e:
            return HttpResponseRedirect(
                reverse("author-delete", kwargs={"pk": self.object.pk})
            )

正如你所看到的,要建立、更新或刪除檢視,你需要分別從 CreateViewUpdateViewDeleteView 派生,然後定義相關的模型。我們還將這些檢視的呼叫限制為僅限於具有 add_authorchange_authordelete_author 許可權的已登入使用者。

對於“建立”和“更新”情況,你還需要指定要在表單中顯示的欄位(使用與 ModelForm 相同的語法)。在本例中,我們展示瞭如何單獨列出它們以及列出“所有”欄位的語法。你還可以使用欄位名/對的字典為每個欄位指定初始值(這裡我們出於演示目的任意設定了死亡日期——你可能希望將其刪除)。預設情況下,這些檢視在成功後將重定向到顯示新建立/編輯的模型項的頁面,在我們的例子中,這將是我們之前教程中建立的作者詳細檢視。你可以透過顯式宣告引數 success_url 來指定備用重定向位置。

AuthorDelete 類不需要顯示任何欄位,因此不需要指定這些欄位。我們還設定了一個 success_url(如上所示),因為 Django 在成功刪除 Author 後沒有明顯的預設 URL 可以導航到。上面我們使用 reverse_lazy() 函式在作者刪除後重定向到我們的作者列表——reverse_lazy()reverse() 的惰性執行版本,這裡使用它是因為我們正在為基於類的檢視屬性提供 URL。

如果作者刪除始終成功,那就完事了。不幸的是,如果作者有相關的圖書,刪除 Author 將導致異常,因為我們的 Book 模型為作者的 ForeignKey 欄位指定了 on_delete=models.RESTRICT。為了處理這種情況,檢視會覆蓋 form_valid() 方法,以便如果刪除 Author 成功,則重定向到 success_url,否則,它只會重定向回相同的表單。我們將在下面的模板中更新,以明確你不能刪除在任何 Book 中使用的 Author 例項。

URL 配置

開啟你的 URL 配置檔案(django-locallibrary-tutorial/catalog/urls.py)並將以下配置新增到檔案底部:

python
urlpatterns += [
    path('author/create/', views.AuthorCreate.as_view(), name='author-create'),
    path('author/<int:pk>/update/', views.AuthorUpdate.as_view(), name='author-update'),
    path('author/<int:pk>/delete/', views.AuthorDelete.as_view(), name='author-delete'),
]

這裡沒有什麼特別新的東西!你可以看到檢視是類,因此必須透過 .as_view() 呼叫,並且你能夠識別每種情況下的 URL 模式。我們必須使用 pk 作為捕獲的主鍵值的名稱,因為這是檢視類期望的引數名稱。

模板

“建立”和“更新”檢視預設使用相同的模板,該模板將以你的模型命名:model_name_form.html(你可以使用檢視中的 template_name_suffix 欄位將字尾更改為除 _form 之外的其他內容,例如 template_name_suffix = '_other_suffix'

建立模板檔案 django-locallibrary-tutorial/catalog/templates/catalog/author_form.html 並複製下面的文字。

django
{% extends "base_generic.html" %}

{% block content %}
<form action="" method="post">
  {% csrf_token %}
  <table>
    {{ form.as_table }}
  </table>
  <input type="submit" value="Submit" />
</form>
{% endblock %}

這與我們之前的表單類似,並使用表格渲染欄位。另請注意我們如何再次宣告 {% csrf_token %} 以確保我們的表單能夠抵禦 CSRF 攻擊。

“刪除”檢視期望找到一個以 [model_name]_confirm_delete.html 格式命名的模板(同樣,你可以使用檢視中的 template_name_suffix 更改字尾)。建立模板檔案 django-locallibrary-tutorial/catalog/templates/catalog/author_confirm_delete.html 並複製下面的文字。

django
{% extends "base_generic.html" %}

{% block content %}

<h1>Delete Author: {{ author }}</h1>

{% if author.book_set.all %}

<p>You can't delete this author until all their books have been deleted:</p>
<ul>
  {% for book in author.book_set.all %}
    <li><a href="{% url 'book-detail' book.pk %}">{{book}}</a> ({{book.bookinstance_set.all.count}})</li>
  {% endfor %}
</ul>

{% else %}
<p>Are you sure you want to delete the author?</p>

<form action="" method="POST">
  {% csrf_token %}
  <input type="submit" action="" value="Yes, delete.">
</form>
{% endif %}

{% endblock %}

這個模板應該很熟悉。它首先檢查作者是否用於任何書籍,如果是,則顯示必須在刪除作者記錄之前刪除的書籍列表。如果不是,它會顯示一個表單,要求使用者確認是否要刪除作者記錄。

最後一步是將頁面連線到側邊欄。首先,我們將作者建立連結新增到基本模板中,以便所有登入的使用者(被視為“員工”且有權建立作者(catalog.add_author))都能在所有頁面中看到它。開啟 /django-locallibrary-tutorial/catalog/templates/base_generic.html,並新增允許使用者建立作者的行(在顯示“所有借出”圖書連結的同一塊中)。請記住使用其名稱 'author-create' 引用 URL,如下所示。

django
{% if user.is_staff %}
<hr>
<ul class="sidebar-nav">
<li>Staff</li>
   <li><a href="{% url 'all-borrowed' %}">All borrowed</a></li>
{% if perms.catalog.add_author %}
   <li><a href="{% url 'author-create' %}">Create author</a></li>
{% endif %}
</ul>
{% endif %}

我們將作者更新和刪除的連結新增到作者詳情頁。開啟 catalog/templates/catalog/author_detail.html 並附加以下程式碼:

django
{% block sidebar %}
  {{ block.super }}

  {% if perms.catalog.change_author or perms.catalog.delete_author %}
  <hr>
  <ul class="sidebar-nav">
    {% if perms.catalog.change_author %}
      <li><a href="{% url 'author-update' author.id %}">Update author</a></li>
    {% endif %}
    {% if not author.book_set.all and perms.catalog.delete_author %}
      <li><a href="{% url 'author-delete' author.id %}">Delete author</a></li>
    {% endif %}
    </ul>
  {% endif %}

{% endblock %}

此塊會覆蓋基本模板中的 sidebar 塊,然後使用 {{ block.super }} 拉取原始內容。然後,它會附加更新或刪除作者的連結,但僅當用戶具有正確的許可權並且作者記錄未與任何圖書關聯時。

頁面現在可以測試了!

測試頁面

首先,使用具有作者新增、更改和刪除許可權的帳戶登入網站。

導航到任何頁面,然後選擇側邊欄中的“建立作者”(URL 為 http://127.0.0.1:8000/catalog/author/create/)。頁面應如下面的截圖所示。

Form Example: Create Author

輸入欄位值,然後按提交儲存作者記錄。現在你應該會被帶到一個新作者的詳細檢視,其 URL 類似於 http://127.0.0.1:8000/catalog/author/10

Form Example: Author Detail showing Update and Delete links

你可以透過選擇“更新作者”連結(URL 類似 http://127.0.0.1:8000/catalog/author/10/update/)來測試編輯記錄——我們不顯示截圖,因為它看起來就像“建立”頁面!

最後,我們可以透過在詳細資訊頁面的側邊欄中選擇“刪除作者”來刪除該頁面。如果作者記錄未用於任何書籍,Django 將顯示如下所示的刪除頁面。按“是,刪除。”以刪除記錄並轉到所有作者列表。

Form with option to delete author

挑戰自我

建立一些表單來建立、編輯和刪除 Book 記錄。你可以使用與 Authors 完全相同的結構(對於刪除,請記住在刪除所有相關聯的 BookInstance 記錄之前,你不能刪除 Book),並且你必須使用正確的許可權。如果你的 book_form.html 模板只是 author_form.html 模板的複製-重新命名版本,那麼新的“建立圖書”頁面將如下面的截圖所示:

Screenshot displaying various fields in the form like title, author, summary, ISBN, genre and language

總結

建立和處理表單可能是一個複雜的過程!Django 透過提供宣告、渲染和驗證表單的程式設計機制,大大簡化了這一過程。此外,Django 提供了通用的表單編輯檢視,可以完成幾乎所有工作來定義可以建立、編輯和刪除與單個模型例項關聯的記錄的頁面。

表單還有很多可以做的(請檢視下面的另請參閱列表),但你現在應該瞭解如何在自己的網站中新增基本表單和表單處理程式碼。

另見

頁面(文件)未找到 /en-US/docs/Learn_web_development/Extensions/Server-side/Django/authentication_and_sessions