Django 教程 第 9 部分:使用表單
在本教程中,我們將向您展示如何在 Django 中使用 HTML 表單,特別是編寫表單以建立、更新和刪除模型例項的最簡單方法。作為演示的一部分,我們將擴充套件 LocalLibrary 網站,以便圖書館員可以續借書籍,並使用我們自己的表單建立、更新和刪除作者(而不是使用管理員應用程式)。
| 先決條件 | 完成所有之前的教程主題,包括 Django 教程 第 8 部分:使用者身份驗證和許可權。 |
|---|---|
| 目標 | 瞭解如何編寫表單以從使用者獲取資訊並更新資料庫。瞭解通用基於類的編輯檢視如何極大地簡化建立用於處理單個模型的表單。 |
概述
一個 HTML 表單 是網頁上一個或多個欄位/小部件的組,可用於收集使用者資訊以提交到伺服器。表單是一種靈活的收集使用者輸入的機制,因為有適合輸入許多不同型別資料的小部件,包括文字框、複選框、單選按鈕、日期選擇器等等。表單也是一種相對安全的與伺服器共享資料的方式,因為它們允許我們透過跨站點請求偽造保護以 POST 請求傳送資料。
雖然到目前為止,我們還沒有在本教程中建立任何表單,但我們已經在 Django 管理站點中遇到了它們——例如,下面的螢幕截圖顯示了一個用於編輯我們其中一個 Book 模型的表單,該表單由多個選擇列表和文字編輯器組成。
使用表單可能很複雜!開發人員需要為表單編寫 HTML,在伺服器端(以及可能在瀏覽器中)驗證和正確清理輸入的資料,重新發布帶有錯誤訊息的表單以告知使用者任何無效欄位,在成功提交資料時處理資料,最後以某種方式響應使用者以指示成功。Django 表單透過提供一個框架來簡化所有這些步驟中的大部分工作,該框架允許您以程式設計方式定義表單及其欄位,然後使用這些物件生成表單 HTML 程式碼並處理大部分驗證和使用者互動。
在本教程中,我們將向您展示一些建立和使用表單的方法,尤其是通用編輯檢視如何顯著減少建立用於操作模型的表單所需的工作量。在此過程中,我們將透過新增一個允許圖書館員續借圖書館書籍的表單來擴充套件我們的 LocalLibrary 應用程式,並且我們將建立頁面來建立、編輯和刪除書籍和作者(複製上面顯示的用於編輯書籍的基本版本表單)。
HTML 表單
首先,簡要概述一下 HTML 表單。考慮一個簡單的 HTML 表單,其中包含一個用於輸入某個“團隊”名稱的文字欄位及其關聯標籤
表單在 HTML 中定義為 <form>…</form> 標籤內的元素集合,其中至少包含一個 input 元素的 type="submit"。
<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 屬性定義將顯示哪種小部件。欄位的 name 和 id 用於在 JavaScript/CSS/HTML 中識別字段,而 value 定義欄位首次顯示時的初始值。匹配的團隊標籤使用 label 標籤指定(參見上面的“輸入名稱”),其中 for 欄位包含關聯 input 的 id 值。
submit 輸入預設顯示為按鈕。可以按下此按鈕將表單中所有其他輸入元素中的資料上傳到伺服器(在本例中,僅為 team_name 欄位)。表單屬性定義用於傳送資料的 HTTP method 和伺服器上資料的目標(action)
action:當提交表單時,將資料傳送到該資源/URL 進行處理。如果未設定此值(或設定為空字串),則表單將提交回當前頁面 URL。method:用於傳送資料的 HTTP 方法:post 或 get。- 如果資料將導致伺服器資料庫發生更改,則應始終使用
POST方法,因為它可以更好地抵禦跨站點請求偽造攻擊。 GET方法僅應用於不更改使用者資料的表單(例如,搜尋表單)。建議在您希望能夠新增書籤或共享 URL 時使用它。
- 如果資料將導致伺服器資料庫發生更改,則應始終使用
伺服器的作用首先是呈現初始表單狀態——要麼包含空白欄位,要麼預先填充初始值。使用者按下提交按鈕後,伺服器將接收來自 Web 瀏覽器的表單資料,並必須驗證資訊。如果表單包含無效資料,伺服器應再次顯示錶單,這次在“有效”欄位中顯示使用者輸入的資料,以及描述無效欄位問題的訊息。一旦伺服器收到包含所有有效表單資料的請求,它就可以執行相應的操作(例如:儲存資料、返回搜尋結果、上傳檔案等),然後通知使用者。
可以想象,建立 HTML、驗證返回的資料、根據需要重新顯示輸入的資料以及錯誤報告,以及對有效資料執行所需的操作,所有這些都需要花費相當大的精力才能“正確”。Django 透過減輕一些繁重的工作和重複的程式碼來簡化這一切!
Django 表單處理流程
Django 的表單處理使用我們在之前的教程中瞭解到的所有相同技術(用於顯示有關我們模型的資訊):檢視獲取請求,執行任何必要的操作,包括從模型中讀取資料,然後生成並返回一個 HTML 頁面(來自模板,我們向其中傳遞一個包含要顯示資料的上下文)。使事情變得更復雜的是,伺服器還需要能夠處理使用者提供的資料,並在出現任何錯誤時重新顯示頁面。
下圖顯示了 Django 如何處理表單請求的過程流程圖,從請求包含表單的頁面開始(以綠色顯示)。
根據上圖,Django 的表單處理主要執行以下操作
- 首次由使用者請求時顯示預設表單。
- 如果要建立新記錄,表單可能包含空白欄位;或者它可能預先填充了初始值(例如,如果要更改記錄或具有有用的預設初始值)。
- 此時,表單稱為未繫結表單,因為它不與任何使用者輸入的資料關聯(儘管它可能具有初始值)。
- 接收來自提交請求的資料並將其繫結到表單。
- 將資料繫結到表單意味著當我們需要重新顯示錶單時,使用者輸入的資料和任何錯誤都可用。
- 清理和驗證資料。
- 清理資料會對輸入欄位進行清理,例如刪除可能用於向伺服器傳送惡意內容的無效字元,並將它們轉換為一致的 Python 型別。
- 驗證檢查值是否適合該欄位(例如,它們是否在正確的日期範圍內,長度是否不過短或過長等)。
- 如果任何資料無效,則重新顯示錶單,這次顯示任何使用者填充的值以及問題欄位的錯誤訊息。
- 如果所有資料均有效,則執行所需的操作(例如儲存資料、傳送電子郵件、返回搜尋結果、上傳檔案等)。
- 所有操作完成後,將使用者重定向到另一個頁面。
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 類派生,並宣告表單的欄位。下面顯示了我們圖書館書籍續借表單的一個非常基本的表單類——將其新增到您的新檔案中
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) 接受日期,並將使用預設 widget:DateInput 進行呈現。
還有許多其他型別的表單欄位,您會從它們與等效模型欄位類的相似性中大體上識別出來
BooleanFieldCharFieldChoiceFieldTypedChoiceFieldDateFieldDateTimeFieldDecimalFieldDurationFieldEmailFieldFileFieldFilePathFieldFloatFieldImageFieldIntegerFieldGenericIPAddressFieldMultipleChoiceFieldTypedMultipleChoiceFieldNullBooleanFieldRegexFieldSlugFieldTimeFieldURLFieldUUIDFieldComboFieldMultiValueFieldSplitDateTimeFieldModelMultipleChoiceFieldModelChoiceField
大多數字段共有的引數列在下面(這些引數都有合理的預設值)
required:如果為True,則欄位不能留空或賦予None值。欄位預設情況下是必需的,因此您將設定required=False以允許表單中出現空白值。label:在 HTML 中呈現欄位時使用的標籤。如果未指定label,Django 將根據欄位名稱建立一個標籤,方法是將第一個字母大寫並將下劃線替換為空格(例如,續訂日期)。label_suffix:預設情況下,冒號顯示在標籤之後(例如,續訂日期:)。此引數允許您指定包含其他字元的不同字尾。initial:顯示錶單時欄位的初始值。widget:要使用的顯示小部件。help_text(如上例所示):可以在表單中顯示的其他文字,以解釋如何使用該欄位。error_messages:欄位的錯誤訊息列表。如果需要,您可以用自己的訊息覆蓋這些訊息。validators:驗證欄位時將呼叫的函式列表。localize:啟用表單資料輸入的本地化(有關更多資訊,請參閱連結)。disabled:如果為True,則顯示該欄位,但無法編輯其值。預設為False。
驗證
Django 提供了許多可以在其中驗證資料的位置。驗證單個欄位最簡單的方法是覆蓋要檢查的欄位的clean_<fieldname>()方法。例如,我們可以透過實現clean_renewal_date()來驗證輸入的renewal_date值是否在現在和 4 周之間,如下所示。
更新您的forms.py檔案,使其如下所示
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的底部
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”任何我們喜歡的名稱,因為我們可以完全控制檢視函式(我們沒有使用期望具有特定名稱的引數的通用詳細資訊檢視類)。但是,pk是“主鍵”的縮寫,是一個合理的約定!
檢視
如上所述的Django 表單處理過程,檢視必須在第一次呼叫時渲染預設表單,然後在資料無效時重新渲染它並顯示錯誤訊息,或者在資料有效時處理資料並重定向到新頁面。為了執行這些不同的操作,檢視必須能夠知道它是在第一次被呼叫以渲染預設表單,還是在隨後被呼叫以驗證資料。
對於使用POST請求將資訊提交到伺服器的表單,最常見的模式是檢視針對POST請求型別(if request.method == 'POST':)進行測試以識別表單驗證請求,並針對GET(使用else條件)進行測試以識別初始表單建立請求。如果您想使用GET請求提交資料,那麼識別這是第一次還是後續檢視呼叫的典型方法是讀取表單資料(例如,讀取表單中隱藏的值)。
書籍續訂過程將寫入我們的資料庫,因此,按照慣例,我們使用POST請求方法。下面的程式碼片段顯示了這種函式檢視的(非常標準的)模式。
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 周。
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()函式以檢查日期是否在正確的範圍內。
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值中。
警告:雖然您也可以透過請求直接訪問表單資料(例如,如果使用 GET 請求,則為request.POST['renewal_date']或request.GET['renewal_date']),但這不推薦。清理後的資料經過清理、驗證並轉換為 Python 友好的型別。
表單處理部分檢視的最後一步是重定向到另一個頁面,通常是“成功”頁面。在本例中,我們使用HttpResponseRedirect和reverse()重定向到名為'all-borrowed'的檢視(這是在Django 教程第 8 部分:使用者身份驗證和許可權中作為“挑戰”建立的)。如果您沒有建立該頁面,請考慮重定向到 URL 為'/'的主頁。
表單處理本身所需的一切都在這裡,但我們仍然需要將對檢視的訪問許可權限制為僅登入的管理員,他們有權續訂書籍。我們使用@login_required來要求使用者已登入,並使用我們現有的can_mark_returned許可權的@permission_required函式裝飾器來允許訪問(裝飾器按順序處理)。請注意,我們可能應該在BookInstance中建立了一個新的許可權設定(“can_renew”),但為了使示例簡單,我們將重用現有的許可權。
因此,最終檢視如下所示。請將其複製到django-locallibrary-tutorial/catalog/views.py的底部。
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)並將下面的程式碼複製到其中
{% 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)以及提交資料的方法(在本例中為“HTTP POST”)——如果您還記得頁面頂部的HTML 表單概述,則顯示的空action表示表單資料將釋出回頁面的當前 URL(這就是我們想要的)。在標籤內部,我們定義了submit輸入,使用者可以按下它來提交資料。新增到表單標籤內部的{% csrf_token %}是 Django 的跨站點偽造保護的一部分。
注意:將{% csrf_token %}新增到您建立的每個使用POST提交資料的 Django 模板中。這將減少表單被惡意使用者劫持的可能性。
剩下的就是{{ form }}模板變量了,我們在上下文字典中將其傳遞給了模板。也許並不奇怪,當按所示方式使用時,它提供了所有表單欄位的預設渲染,包括它們的標籤、小部件和幫助文字——渲染結果如下所示
<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 }},也會提供相同的渲染。
如果您輸入無效日期,您還會在頁面上看到錯誤列表的渲染(請參見下面的errorlist)。
<tr>
<th><label for="id_renewal_date">Renewal date:</label></th>
<td>
<ul class="errorlist">
<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 部分:使用者身份驗證和許可權中接受了“挑戰”,您將看到一個檢視,顯示圖書館中所有借出的書籍,該檢視僅對圖書館工作人員可見。該檢視可能類似於以下內容
{% 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值是在此處定義的。
{% 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/(有效的bookinstance_id可以透過導航到圖書館中的書籍詳細資訊頁面並複製id欄位來獲得)。
它看起來像什麼?
如果您成功,預設表單將如下所示
輸入無效值的表單將如下所示
包含續借連結的所有書籍列表將如下所示
ModelForm
使用上面描述的方法建立Form類非常靈活,允許您建立任何您喜歡的表單頁面並將其與任何模型或模型關聯。
但是,如果您只需要一個表單來對映單個模型的欄位,那麼您的模型將已經定義了表單中需要的大部分資訊:欄位、標籤、幫助文字等。與其在表單中重新建立模型定義,不如使用ModelForm輔助類從您的模型建立表單。然後,此ModelForm可以在您的檢視中以與普通Form完全相同的方式使用。
下面顯示了一個基本的ModelForm,其中包含與我們原始的RenewBookForm相同的欄位。您只需新增class Meta以及關聯的model(BookInstance)和要在表單中包含的模型fields列表即可建立表單。
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向您展示瞭如何覆蓋這些欄位,並且如果預設值不足,您可以類似地設定widgets和error_messages。
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。
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。只要您還將相應的表單變數名稱從renewal_date更新為due_back(如第二個表單宣告中所示:RenewBookModelForm(initial={'due_back': proposed_renewal_date}),您就可以在當前使用RenewBookForm的任何地方匯入並使用它。
通用編輯檢視
我們在上面的函式檢視示例中使用的表單處理演算法代表了表單編輯檢視中極其常見的模式。Django 透過為基於模型的建立、編輯和刪除檢視建立通用編輯檢視,為您抽象化了大部分此“樣板程式碼”。這些不僅處理“檢視”行為,而且還根據模型自動為您建立表單類(ModelForm)。
注意:除了此處描述的編輯檢視之外,還有一個FormView類,它在“靈活性”與“編碼工作量”方面介於我們的函式檢視和其他通用檢視之間。使用FormView,您仍然需要建立Form,但您不必實現所有標準的表單處理模式。相反,您只需提供一個函式的實現,該函式將在已知提交有效後被呼叫。
在本節中,我們將使用通用編輯檢視建立頁面,以新增建立、編輯和刪除庫中Author記錄的功能——有效地提供了管理站點部分的基本重新實現(如果您需要以比管理站點更靈活的方式提供管理功能,這將很有用)。
檢視
開啟檢視檔案(django-locallibrary-tutorial/catalog/views.py)並將以下程式碼塊追加到其底部
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})
)
如您所見,要建立、更新或刪除檢視,您需要分別從CreateView、UpdateView和DeleteView派生,然後定義關聯的模型。我們還將呼叫這些檢視的許可權分別限制為僅登入使用者且具有add_author、change_author和delete_author許可權的使用者。
對於“建立”和“更新”案例,您還需要指定要在表單中顯示的欄位(使用與ModelForm相同的語法)。在這種情況下,我們展示瞭如何單獨列出它們以及列出“所有”欄位的語法。您還可以使用field_name/value對的字典為每個欄位指定初始值(這裡我們任意設定了死亡日期以進行演示——您可能希望將其刪除)。預設情況下,這些檢視將在成功時重定向到顯示新建立/編輯的模型專案的頁面,在本例中將是我們之前教程中建立的作者詳細資訊檢視。您可以透過顯式宣告引數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)並將以下配置新增到檔案的底部
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並複製下面的文字。
{% 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並複製下面的文字。
{% 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。
{% 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** 並追加以下程式碼
{% 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/)。頁面應如下面的螢幕截圖所示。
輸入欄位的值,然後按提交以儲存作者記錄。您現在應該被帶到新作者的詳細資訊檢視,其 URL 類似於http://127.0.0.1:8000/catalog/author/10。
您可以透過選擇“更新作者”連結(URL 類似於http://127.0.0.1:8000/catalog/author/10/update/)來測試編輯記錄 - 我們沒有顯示螢幕截圖,因為它看起來就像“建立”頁面一樣!
最後,如果作者記錄未在任何書籍中使用,我們可以在詳細資訊頁面上從側邊欄中選擇“刪除作者”來刪除頁面。如果作者記錄未在任何書籍中使用,Django 應該顯示如下所示的刪除頁面。按“是,刪除。”以刪除記錄並轉到所有作者的列表。
挑戰自我
建立一些表單以建立、編輯和刪除Book記錄。您可以使用與Authors完全相同的結構(對於刪除,請記住,在刪除所有關聯的BookInstance記錄之前,您不能刪除Book),並且必須使用正確的許可權。如果您的book_form.html模板只是author_form.html模板的複製重新命名版本,則新的“建立書籍”頁面將如下面的螢幕截圖所示
總結
建立和處理表單可能是一個複雜的過程!Django 透過提供宣告、渲染和驗證表單的程式設計機制,使它變得更容易。此外,Django 提供了通用的表單編輯檢視,這些檢視可以完成幾乎所有定義可以建立、編輯和刪除與單個模型例項關聯的記錄的頁面的工作。
表單還可以做更多的事情(請檢視下面的另請參閱列表),但您現在應該瞭解如何在自己的網站中新增基本的表單和表單處理程式碼。
另請參閱
- 使用表單(Django 文件)
- 編寫您的第一個 Django 應用程式,第 4 部分 > 編寫一個簡單的表單(Django 文件)
- 表單 API(Django 文件)
- 表單欄位(Django 文件)
- 表單和欄位驗證(Django 文件)
- 使用基於類的檢視處理表單(Django 文件)
- 從模型建立表單(Django 文件)
- 通用編輯檢視(Django 文件)