表單集合

class BaseFormSet[原始碼]

表單集合是一種抽象層,用於在同一個頁面上處理多個表單。它可以被比喻為一個資料網格。假設您有以下的表單

>>> from django import forms
>>> class ArticleForm(forms.Form):
...     title = forms.CharField()
...     pub_date = forms.DateField()
...

您可能會希望允許使用者一次建立多篇文章。要從 ArticleForm 建立一個表單集合,您應該這樣做

>>> from django.forms import formset_factory
>>> ArticleFormSet = formset_factory(ArticleForm)

您現在已經建立了一個名為 ArticleFormSet 的表單集合類別。實例化表單集合後,您就可以迭代表單集合中的表單,並像處理一般表單一樣顯示它們

>>> formset = ArticleFormSet()
>>> for form in formset:
...     print(form)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></div>

如您所見,它只顯示了一個空的表單。顯示的空表單數量由 extra 參數控制。預設情況下,formset_factory() 會定義一個額外的表單;以下範例將建立一個表單集合類別來顯示兩個空白表單

>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)

表單集合可以被迭代和索引,以它們被建立的順序存取表單。如果需要,您可以覆寫預設的 iterationindexing 行為來重新排序表單。

將初始資料用於表單集合

初始資料是表單集合主要可用性的驅動力。如上所示,您可以定義額外表單的數量。這意味著您正在告訴表單集合,除了從初始資料產生的表單數量之外,還要顯示多少個額外的表單。讓我們看一個範例

>>> import datetime
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
>>> formset = ArticleFormSet(
...     initial=[
...         {
...             "title": "Django is now open source",
...             "pub_date": datetime.date.today(),
...         }
...     ]
... )

>>> for form in formset:
...     print(form)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" value="Django is now open source" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" value="2023-02-11" id="id_form-0-pub_date"></div>
<div><label for="id_form-1-title">Title:</label><input type="text" name="form-1-title" id="id_form-1-title"></div>
<div><label for="id_form-1-pub_date">Pub date:</label><input type="text" name="form-1-pub_date" id="id_form-1-pub_date"></div>
<div><label for="id_form-2-title">Title:</label><input type="text" name="form-2-title" id="id_form-2-title"></div>
<div><label for="id_form-2-pub_date">Pub date:</label><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></div>

現在總共有三個表單顯示在上方。一個用於傳入的初始資料,另外兩個是額外的表單。另請注意,我們正在傳入一個字典列表作為初始資料。

如果您使用 initial 來顯示表單集合,您應該在處理該表單集合的提交時傳入相同的 initial,以便表單集合可以偵測到使用者變更了哪些表單。例如,您可能會有類似這樣的東西:ArticleFormSet(request.POST, initial=[...])

限制表單的最大數量

formset_factory()max_num 參數可讓您限制表單集合將顯示的表單數量

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1)
>>> formset = ArticleFormSet()
>>> for form in formset:
...     print(form)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></div>

如果 max_num 的值大於初始資料中現有項目的數量,則最多會將 extra 個額外的空白表單新增至表單集合,只要表單總數不超過 max_num。例如,如果 extra=2max_num=2,並且表單集合使用一個 initial 項目初始化,則會顯示初始項目的表單和一個空白表單。

如果初始資料中的項目數量超過 max_num,則無論 max_num 的值為何,都會顯示所有初始資料表單,並且不會顯示額外的表單。例如,如果 extra=3max_num=1,並且表單集合使用兩個初始項目初始化,則會顯示兩個包含初始資料的表單。

max_num 值為 None(預設值)會對顯示的表單數量設定較高的上限 (1000)。實際上,這相當於沒有限制。

預設情況下,max_num 只會影響顯示的表單數量,而不會影響驗證。如果將 validate_max=True 傳遞給 formset_factory(),則 max_num 將會影響驗證。請參閱 validate_max

限制實例化表單的最大數量

formset_factory()absolute_max 參數允許限制在提供 POST 資料時可以實例化的表單數量。這可以防止使用偽造的 POST 請求來攻擊記憶體耗盡。

>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, absolute_max=1500)
>>> data = {
...     "form-TOTAL_FORMS": "1501",
...     "form-INITIAL_FORMS": "0",
... }
>>> formset = ArticleFormSet(data)
>>> len(formset.forms)
1500
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['Please submit at most 1000 forms.']

absolute_maxNone 時,它會預設為 max_num + 1000。(如果 max_numNone,則會預設為 2000)。

如果 absolute_max 小於 max_num,則會引發 ValueError

表單集合驗證

使用表單集合進行驗證幾乎與一般 Form 相同。表單集合上有一個 is_valid 方法,可方便地驗證表單集合中的所有表單

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm)
>>> data = {
...     "form-TOTAL_FORMS": "1",
...     "form-INITIAL_FORMS": "0",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
True

我們沒有將任何資料傳遞到表單集合,這會導致有效的表單。表單集合非常聰明,會忽略未變更的額外表單。如果我們提供一篇無效的文章

>>> data = {
...     "form-TOTAL_FORMS": "2",
...     "form-INITIAL_FORMS": "0",
...     "form-0-title": "Test",
...     "form-0-pub_date": "1904-06-16",
...     "form-1-title": "Test",
...     "form-1-pub_date": "",  # <-- this date is missing but required
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {'pub_date': ['This field is required.']}]

如我們所見,formset.errors 是一個列表,其條目對應於表單集合中的表單。針對兩個表單分別執行了驗證,並且第二個項目出現了預期的錯誤訊息。

就像使用一般 Form 一樣,表單集合中每個表單的欄位都可以包含 HTML 屬性,例如用於瀏覽器驗證的 maxlength。但是,表單集合的表單欄位不會包含 required 屬性,因為在新增和刪除表單時,該驗證可能不正確。

BaseFormSet.total_error_count()[原始碼]

要檢查表單集合中有多少個錯誤,我們可以使用 total_error_count 方法

>>> # Using the previous example
>>> formset.errors
[{}, {'pub_date': ['This field is required.']}]
>>> len(formset.errors)
2
>>> formset.total_error_count()
1

我們也可以檢查表單資料是否與初始資料不同 (即,表單的傳送沒有任何資料)

>>> data = {
...     "form-TOTAL_FORMS": "1",
...     "form-INITIAL_FORMS": "0",
...     "form-0-title": "",
...     "form-0-pub_date": "",
... }
>>> formset = ArticleFormSet(data)
>>> formset.has_changed()
False

了解 ManagementForm

您可能已經注意到上面表單集合的資料中需要的額外資料 (form-TOTAL_FORMSform-INITIAL_FORMS)。ManagementForm 需要這些資料。這個表單由表單集合用來管理表單集合中包含的表單集合。如果您不提供此管理資料,表單集合將會無效

>>> data = {
...     "form-0-title": "Test",
...     "form-0-pub_date": "",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False

它用來追蹤目前顯示的表單實例數量。如果您透過 JavaScript 新增表單,您也應該遞增此表單中的計數欄位。另一方面,如果您使用 JavaScript 允許刪除現有物件,則需要確保透過在 POST 資料中包含 form-#-DELETE 來正確標記要刪除的項目。預期所有表單都會出現在 POST 資料中,無論如何。

管理表單本身可作為表單集合的屬性使用。當在模板中呈現表單集合時,您可以透過呈現 {{ my_formset.management_form }} (請適當地替換您的表單集合名稱) 來包含所有管理資料。

注意

除了此處範例中顯示的 form-TOTAL_FORMSform-INITIAL_FORMS 欄位之外,管理表單還包含 form-MIN_NUM_FORMSform-MAX_NUM_FORMS 欄位。它們與管理表單的其餘部分一起輸出,但僅為了方便客戶端程式碼使用。這些欄位不是必需的,因此不會顯示在 POST 資料範例中。

total_form_countinitial_form_count

BaseFormSet 有幾個與 ManagementFormtotal_form_countinitial_form_count 密切相關的方法。

total_form_count 會回傳此表單集合中的表單總數。initial_form_count 會回傳表單集合中預先填寫的表單數量,也用於判斷需要多少個表單。您可能永遠不需要覆寫這些方法中的任何一個,因此請確保在執行操作之前了解它們的作用。

empty_form

BaseFormSet 提供了一個額外的屬性 empty_form,它會回傳一個帶有 __prefix__ 前綴的表單實例,以便在具有 JavaScript 的動態表單中更容易使用。

error_messages

error_messages 引數可讓您覆寫表單集合會引發的預設訊息。傳入一個字典,其中鍵與您想要覆寫的錯誤訊息相符。錯誤訊息鍵包括 'too_few_forms''too_many_forms''missing_management_form''too_few_forms''too_many_forms' 錯誤訊息可能包含 %(num)d,它將會被 min_nummax_num 取代。

例如,以下是管理表單遺失時的預設錯誤訊息

>>> formset = ArticleFormSet({})
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['ManagementForm data is missing or has been tampered with. Missing fields: form-TOTAL_FORMS, form-INITIAL_FORMS. You may need to file a bug report if the issue persists.']

以下是自訂錯誤訊息

>>> formset = ArticleFormSet(
...     {}, error_messages={"missing_management_form": "Sorry, something went wrong."}
... )
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['Sorry, something went wrong.']

自訂表單集合驗證

表單集合有一個類似於 Form 類別中的 clean 方法。您可以在此定義自己的表單集合層級驗證。

>>> from django.core.exceptions import ValidationError
>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm

>>> class BaseArticleFormSet(BaseFormSet):
...     def clean(self):
...         """Checks that no two articles have the same title."""
...         if any(self.errors):
...             # Don't bother validating the formset unless each form is valid on its own
...             return
...         titles = set()
...         for form in self.forms:
...             if self.can_delete and self._should_delete_form(form):
...                 continue
...             title = form.cleaned_data.get("title")
...             if title in titles:
...                 raise ValidationError("Articles in a set must have distinct titles.")
...             titles.add(title)
...

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> data = {
...     "form-TOTAL_FORMS": "2",
...     "form-INITIAL_FORMS": "0",
...     "form-0-title": "Test",
...     "form-0-pub_date": "1904-06-16",
...     "form-1-title": "Test",
...     "form-1-pub_date": "1912-06-23",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
['Articles in a set must have distinct titles.']

表單集合的 clean 方法會在所有 Form.clean 方法被呼叫之後呼叫。錯誤會使用表單集合上的 non_form_errors() 方法找到。

非表單錯誤將會使用額外的 nonform 類別呈現,以幫助區分它們與表單特定的錯誤。例如,{{ formset.non_form_errors }} 看起來會像這樣

<ul class="errorlist nonform">
    <li>Articles in a set must have distinct titles.</li>
</ul>

驗證表單集合中的表單數量

Django 提供了幾種方法來驗證提交的表單的最小或最大數量。需要對表單數量進行更客製化驗證的應用程式應該使用自訂表單集合驗證。

validate_max

如果將 validate_max=True 傳遞給 formset_factory(),驗證也會檢查資料集中表單的數量(減去標記為刪除的表單)是否小於或等於 max_num

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, max_num=1, validate_max=True)
>>> data = {
...     "form-TOTAL_FORMS": "2",
...     "form-INITIAL_FORMS": "0",
...     "form-0-title": "Test",
...     "form-0-pub_date": "1904-06-16",
...     "form-1-title": "Test 2",
...     "form-1-pub_date": "1912-06-23",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
['Please submit at most 1 form.']

validate_max=True 會嚴格根據 max_num 進行驗證,即使 max_num 因為提供的初始資料過多而超出。

可以透過將 'too_many_forms' 訊息傳遞給 error_messages 引數來自訂錯誤訊息。

注意

無論 validate_max 的值為何,如果資料集中的表單數量超過 absolute_max,則表單將會驗證失敗,就像設定了 validate_max 一樣,此外,只會驗證前 absolute_max 個表單。其餘的將會完全截斷。這是為了防止使用偽造的 POST 請求進行記憶體耗盡攻擊。請參閱 限制實例化表單的最大數量

validate_min

如果將 validate_min=True 傳遞給 formset_factory(),驗證也會檢查資料集中表單的數量(減去標記為刪除的表單)是否大於或等於 min_num

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, min_num=3, validate_min=True)
>>> data = {
...     "form-TOTAL_FORMS": "2",
...     "form-INITIAL_FORMS": "0",
...     "form-0-title": "Test",
...     "form-0-pub_date": "1904-06-16",
...     "form-1-title": "Test 2",
...     "form-1-pub_date": "1912-06-23",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
['Please submit at least 3 forms.']

可以透過將 'too_few_forms' 訊息傳遞給 error_messages 引數來自訂錯誤訊息。

注意

無論 validate_min 的值為何,如果表單集合不包含任何資料,則會顯示 extra + min_num 個空表單。

處理表單的排序和刪除

formset_factory() 提供了兩個可選參數 can_ordercan_delete,以協助表單集合中的表單排序以及從表單集合中刪除表單。

can_order

BaseFormSet.can_order

預設值:False

讓您可以建立具有排序能力的表單集合

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, can_order=True)
>>> formset = ArticleFormSet(
...     initial=[
...         {"title": "Article #1", "pub_date": datetime.date(2008, 5, 10)},
...         {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
...     ]
... )
>>> for form in formset:
...     print(form)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date"></div>
<div><label for="id_form-0-ORDER">Order:</label><input type="number" name="form-0-ORDER" value="1" id="id_form-0-ORDER"></div>
<div><label for="id_form-1-title">Title:</label><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title"></div>
<div><label for="id_form-1-pub_date">Pub date:</label><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date"></div>
<div><label for="id_form-1-ORDER">Order:</label><input type="number" name="form-1-ORDER" value="2" id="id_form-1-ORDER"></div>
<div><label for="id_form-2-title">Title:</label><input type="text" name="form-2-title" id="id_form-2-title"></div>
<div><label for="id_form-2-pub_date">Pub date:</label><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></div>
<div><label for="id_form-2-ORDER">Order:</label><input type="number" name="form-2-ORDER" id="id_form-2-ORDER"></div>

這會為每個表單新增一個額外的欄位。這個新的欄位名為 ORDER,它是一個 forms.IntegerField。對於來自初始資料的表單,它會自動為它們分配一個數值。讓我們看看當使用者變更這些值時會發生什麼

>>> data = {
...     "form-TOTAL_FORMS": "3",
...     "form-INITIAL_FORMS": "2",
...     "form-0-title": "Article #1",
...     "form-0-pub_date": "2008-05-10",
...     "form-0-ORDER": "2",
...     "form-1-title": "Article #2",
...     "form-1-pub_date": "2008-05-11",
...     "form-1-ORDER": "1",
...     "form-2-title": "Article #3",
...     "form-2-pub_date": "2008-05-01",
...     "form-2-ORDER": "0",
... }

>>> formset = ArticleFormSet(
...     data,
...     initial=[
...         {"title": "Article #1", "pub_date": datetime.date(2008, 5, 10)},
...         {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
...     ],
... )
>>> formset.is_valid()
True
>>> for form in formset.ordered_forms:
...     print(form.cleaned_data)
...
{'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0, 'title': 'Article #3'}
{'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': 'Article #2'}
{'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2, 'title': 'Article #1'}

BaseFormSet 還提供了一個 ordering_widget 屬性和 get_ordering_widget() 方法,用於控制與 can_order 一起使用的 widget。

ordering_widget

BaseFormSet.ordering_widget

預設值:NumberInput

設定 ordering_widget 以指定與 can_order 一起使用的 widget 類別

>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     ordering_widget = HiddenInput
...

>>> ArticleFormSet = formset_factory(
...     ArticleForm, formset=BaseArticleFormSet, can_order=True
... )

get_ordering_widget

BaseFormSet.get_ordering_widget()[原始碼]

如果需要為 can_order 提供小工具實例,請覆寫 get_ordering_widget()

>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     def get_ordering_widget(self):
...         return HiddenInput(attrs={"class": "ordering"})
...

>>> ArticleFormSet = formset_factory(
...     ArticleForm, formset=BaseArticleFormSet, can_order=True
... )

can_delete

BaseFormSet.can_delete

預設值:False

讓您建立可以選擇刪除表單的表單集合。

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True)
>>> formset = ArticleFormSet(
...     initial=[
...         {"title": "Article #1", "pub_date": datetime.date(2008, 5, 10)},
...         {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
...     ]
... )
>>> for form in formset:
...     print(form)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date"></div>
<div><label for="id_form-0-DELETE">Delete:</label><input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE"></div>
<div><label for="id_form-1-title">Title:</label><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title"></div>
<div><label for="id_form-1-pub_date">Pub date:</label><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date"></div>
<div><label for="id_form-1-DELETE">Delete:</label><input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE"></div>
<div><label for="id_form-2-title">Title:</label><input type="text" name="form-2-title" id="id_form-2-title"></div>
<div><label for="id_form-2-pub_date">Pub date:</label><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></div>
<div><label for="id_form-2-DELETE">Delete:</label><input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE"></div>

can_order 類似,這會為每個表單新增一個名為 DELETE 的欄位,且為 forms.BooleanField。當資料傳入時,標記任何刪除欄位,您可以使用 deleted_forms 來存取它們。

>>> data = {
...     "form-TOTAL_FORMS": "3",
...     "form-INITIAL_FORMS": "2",
...     "form-0-title": "Article #1",
...     "form-0-pub_date": "2008-05-10",
...     "form-0-DELETE": "on",
...     "form-1-title": "Article #2",
...     "form-1-pub_date": "2008-05-11",
...     "form-1-DELETE": "",
...     "form-2-title": "",
...     "form-2-pub_date": "",
...     "form-2-DELETE": "",
... }

>>> formset = ArticleFormSet(
...     data,
...     initial=[
...         {"title": "Article #1", "pub_date": datetime.date(2008, 5, 10)},
...         {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
...     ],
... )
>>> [form.cleaned_data for form in formset.deleted_forms]
[{'DELETE': True, 'pub_date': datetime.date(2008, 5, 10), 'title': 'Article #1'}]

如果您使用 ModelFormSet,當您呼叫 formset.save() 時,已刪除表單的模型實例將會被刪除。

如果您呼叫 formset.save(commit=False),物件將不會自動刪除。您需要在 formset.deleted_objects 的每個物件上呼叫 delete(),才能真正刪除它們。

>>> instances = formset.save(commit=False)
>>> for obj in formset.deleted_objects:
...     obj.delete()
...

另一方面,如果您使用一般的 FormSet,則需要自行處理 formset.deleted_forms,也許在您的表單集合的 save() 方法中,因為沒有一般定義刪除表單的意義。

BaseFormSet 也提供 deletion_widget 屬性和 get_deletion_widget() 方法,用於控制與 can_delete 一起使用的小工具。

deletion_widget

BaseFormSet.deletion_widget

預設值:CheckboxInput

設定 deletion_widget 以指定要與 can_delete 一起使用的小工具類別。

>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     deletion_widget = HiddenInput
...

>>> ArticleFormSet = formset_factory(
...     ArticleForm, formset=BaseArticleFormSet, can_delete=True
... )

get_deletion_widget

BaseFormSet.get_deletion_widget()[原始碼]

如果需要為 can_delete 提供小工具實例,請覆寫 get_deletion_widget()

>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     def get_deletion_widget(self):
...         return HiddenInput(attrs={"class": "deletion"})
...

>>> ArticleFormSet = formset_factory(
...     ArticleForm, formset=BaseArticleFormSet, can_delete=True
... )

can_delete_extra

BaseFormSet.can_delete_extra

預設值:True

在設定 can_delete=True 時,指定 can_delete_extra=False 將移除刪除額外表單的選項。

將額外欄位新增至表單集合

如果您需要將額外欄位新增至表單集合,這很容易完成。表單集合基底類別提供了一個 add_fields 方法。您可以覆寫此方法來新增自己的欄位,甚至重新定義排序和刪除欄位的預設欄位/屬性。

>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     def add_fields(self, form, index):
...         super().add_fields(form, index)
...         form.fields["my_field"] = forms.CharField()
...

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> formset = ArticleFormSet()
>>> for form in formset:
...     print(form)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></div>
<div><label for="id_form-0-my_field">My field:</label><input type="text" name="form-0-my_field" id="id_form-0-my_field"></div>

將自訂參數傳遞至表單集合表單

有時候,您的表單類別會採用自訂參數,例如 MyArticleForm。您可以在建立表單集合實例時傳遞此參數。

>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm

>>> class MyArticleForm(ArticleForm):
...     def __init__(self, *args, user, **kwargs):
...         self.user = user
...         super().__init__(*args, **kwargs)
...

>>> ArticleFormSet = formset_factory(MyArticleForm)
>>> formset = ArticleFormSet(form_kwargs={"user": request.user})

form_kwargs 也可能取決於特定的表單實例。表單集合基底類別提供了一個 get_form_kwargs 方法。此方法接受一個引數 - 表單集合中表單的索引。索引為 empty_form 時為 None

>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory

>>> class BaseArticleFormSet(BaseFormSet):
...     def get_form_kwargs(self, index):
...         kwargs = super().get_form_kwargs(index)
...         kwargs["custom_kwarg"] = index
...         return kwargs
...

>>> ArticleFormSet = formset_factory(MyArticleForm, formset=BaseArticleFormSet)
>>> formset = ArticleFormSet()

自訂表單集合的前綴

在呈現的 HTML 中,表單集合會在每個欄位的名稱中包含前綴。預設情況下,前綴為 'form',但可以使用表單集合的 prefix 引數來自訂。

例如,在預設情況下,您可能會看到

<label for="id_form-0-title">Title:</label>
<input type="text" name="form-0-title" id="id_form-0-title">

但是使用 ArticleFormset(prefix='article'),則會變成

<label for="id_article-0-title">Title:</label>
<input type="text" name="article-0-title" id="id_article-0-title">

如果您想在檢視中使用多個表單集合,這會很有用。

在檢視和範本中使用表單集合

表單集合具有與呈現相關的以下屬性和方法:

BaseFormSet.renderer

指定用於表單集合的 呈現器。預設為 FORM_RENDERER 設定所指定的呈現器。

BaseFormSet.template_name[原始碼]

如果將表單集合轉換為字串 (例如,透過 print(formset) 或在範本中透過 {{ formset }}) 呈現的範本名稱。

預設情況下,屬性會傳回呈現器的 formset_template_name 值。您可以將它設定為字串範本名稱,以便覆寫特定表單集合類別的範本名稱。

此範本將用於呈現表單集合的管理表單,然後根據表單的 template_name 所定義的範本來呈現表單集合中的每個表單。

BaseFormSet.template_name_div

呼叫 as_div() 時使用的範本名稱。預設情況下為 "django/forms/formsets/div.html"。此範本會呈現表單集合的管理表單,然後根據表單的 as_div() 方法來呈現表單集合中的每個表單。

BaseFormSet.template_name_p

呼叫 as_p() 時使用的範本名稱。預設情況下為 "django/forms/formsets/p.html"。此範本會呈現表單集合的管理表單,然後根據表單的 as_p() 方法來呈現表單集合中的每個表單。

BaseFormSet.template_name_table

呼叫 as_table() 時使用的範本名稱。預設為 "django/forms/formsets/table.html"。此範本會依照表單的 as_table() 方法,呈現表單集合的管理表單,然後呈現表單集合中的每個表單。

BaseFormSet.template_name_ul

呼叫 as_ul() 時使用的範本名稱。預設為 "django/forms/formsets/ul.html"。此範本會依照表單的 as_ul() 方法,呈現表單集合的管理表單,然後呈現表單集合中的每個表單。

BaseFormSet.get_context()[原始碼]

傳回在範本中呈現表單集合的內容。

可用的內容如下

  • formset:表單集合的實例。

BaseFormSet.render(template_name=None, context=None, renderer=None)

render 方法由 __str__ 以及 as_div()as_p()as_ul()as_table() 方法呼叫。所有引數都是選用的,預設為

BaseFormSet.as_div()

使用 template_name_div 範本呈現表單集合。

BaseFormSet.as_p()

使用 template_name_p 範本呈現表單集合。

BaseFormSet.as_table()

使用 template_name_table 範本呈現表單集合。

BaseFormSet.as_ul()

使用 template_name_ul 範本呈現表單集合。

在視圖中使用表單集合與使用一般 Form 類別沒有太大區別。您唯一需要注意的是,請確保在範本中使用管理表單。讓我們來看一個範例視圖

from django.forms import formset_factory
from django.shortcuts import render
from myapp.forms import ArticleForm


def manage_articles(request):
    ArticleFormSet = formset_factory(ArticleForm)
    if request.method == "POST":
        formset = ArticleFormSet(request.POST, request.FILES)
        if formset.is_valid():
            # do something with the formset.cleaned_data
            pass
    else:
        formset = ArticleFormSet()
    return render(request, "manage_articles.html", {"formset": formset})

manage_articles.html 範本可能如下所示

<form method="post">
    {{ formset.management_form }}
    <table>
        {% for form in formset %}
        {{ form }}
        {% endfor %}
    </table>
</form>

不過,有一個稍微簡短的方法可以處理上述情況,讓表單集合本身處理管理表單

<form method="post">
    <table>
        {{ formset }}
    </table>
</form>

上述程式碼最終會呼叫表單集合類別上的 BaseFormSet.render() 方法。這會使用 template_name 屬性指定的範本來呈現表單集合。與表單類似,預設情況下,表單集合會以 as_table 呈現,並且提供其他協助程式方法 as_pas_ul。您可以指定 template_name 屬性,或更廣泛地說,透過 覆寫預設範本 來客製化表單集合的呈現方式。

手動呈現的 can_deletecan_order

如果您在範本中手動呈現欄位,則可以使用 {{ form.DELETE }} 呈現 can_delete 參數

<form method="post">
    {{ formset.management_form }}
    {% for form in formset %}
        <ul>
            <li>{{ form.title }}</li>
            <li>{{ form.pub_date }}</li>
            {% if formset.can_delete %}
                <li>{{ form.DELETE }}</li>
            {% endif %}
        </ul>
    {% endfor %}
</form>

同樣地,如果表單集合具有排序能力 (can_order=True),則可以使用 {{ form.ORDER }} 來呈現它。

在視圖中使用多個表單集合

您可以在視圖中根據需要使用多個表單集合。表單集合的許多行為都是從表單繼承而來的。也就是說,您可以使用 prefix,以指定的值作為表單集合表單欄位名稱的前綴,以允許將多個表單集合傳送到視圖,而不會發生名稱衝突。讓我們看看如何完成這個操作

from django.forms import formset_factory
from django.shortcuts import render
from myapp.forms import ArticleForm, BookForm


def manage_articles(request):
    ArticleFormSet = formset_factory(ArticleForm)
    BookFormSet = formset_factory(BookForm)
    if request.method == "POST":
        article_formset = ArticleFormSet(request.POST, request.FILES, prefix="articles")
        book_formset = BookFormSet(request.POST, request.FILES, prefix="books")
        if article_formset.is_valid() and book_formset.is_valid():
            # do something with the cleaned_data on the formsets.
            pass
    else:
        article_formset = ArticleFormSet(prefix="articles")
        book_formset = BookFormSet(prefix="books")
    return render(
        request,
        "manage_articles.html",
        {
            "article_formset": article_formset,
            "book_formset": book_formset,
        },
    )

然後,您會像平常一樣呈現表單集合。重要的是要指出,您需要在 POST 和非 POST 的情況下傳遞 prefix,以便正確呈現和處理。

每個表單集合的 prefix 會取代新增至每個欄位 nameid HTML 屬性的預設 form 前綴。

返回頂部