從模型建立表單¶
ModelForm
¶
如果你正在建構一個資料庫驅動的應用程式,你很可能會有一些表單與 Django 模型密切相關。例如,你可能有一個 BlogComment
模型,而且你想要建立一個表單讓使用者提交評論。在這種情況下,在你的表單中定義欄位類型是多餘的,因為你已經在你的模型中定義了欄位。
基於這個原因,Django 提供了一個輔助類別,讓你從 Django 模型建立一個 Form
類別。
例如
>>> from django.forms import ModelForm
>>> from myapp.models import Article
# Create the form class.
>>> class ArticleForm(ModelForm):
... class Meta:
... model = Article
... fields = ["pub_date", "headline", "content", "reporter"]
...
# Creating a form to add an article.
>>> form = ArticleForm()
# Creating a form to change an existing article.
>>> article = Article.objects.get(pk=1)
>>> form = ArticleForm(instance=article)
欄位類型¶
產生的 Form
類別將會為每個指定的模型欄位都有一個表單欄位,順序依照 fields
屬性中指定的順序。
每個模型欄位都有一個相對應的預設表單欄位。例如,模型上的 CharField
在表單中會表示為一個 CharField
。模型 ManyToManyField
表示為一個 MultipleChoiceField
。以下是完整的轉換列表
模型欄位 |
表單欄位 |
---|---|
不在表單中表示 |
|
不在表單中表示 |
|
|
|
|
|
|
|
|
|
|
|
|
|
不在表單中表示 |
|
|
|
如你所預期,ForeignKey
和 ManyToManyField
模型欄位類型是特殊情況
ForeignKey
由django.forms.ModelChoiceField
表示,這是一個ChoiceField
,其選項是一個模型QuerySet
。ManyToManyField
由django.forms.ModelMultipleChoiceField
表示,這是一個MultipleChoiceField
,其選項是一個模型QuerySet
。
此外,每個產生的表單欄位都有如下設定的屬性
如果模型欄位有
blank=True
,則表單欄位上的required
設定為False
。否則,required=True
。表單欄位的
label
設定為模型欄位的verbose_name
,第一個字元大寫。表單欄位的
help_text
設定為模型欄位的help_text
。如果模型欄位有設定
choices
,則表單欄位的widget
將會設定為Select
,選項來自模型欄位的choices
。這些選項通常會包含一個預設選取的空白選項。如果該欄位為必填,這會強迫使用者進行選擇。如果模型欄位有blank=False
和一個明確的default
值,則不會包含空白選項(default
值將會被初始選取)。
最後,請注意,你可以覆寫用於給定模型欄位的表單欄位。請參閱下方覆寫預設欄位。
完整範例¶
考慮以下模型集合
from django.db import models
from django.forms import ModelForm
TITLE_CHOICES = {
"MR": "Mr.",
"MRS": "Mrs.",
"MS": "Ms.",
}
class Author(models.Model):
name = models.CharField(max_length=100)
title = models.CharField(max_length=3, choices=TITLE_CHOICES)
birth_date = models.DateField(blank=True, null=True)
def __str__(self):
return self.name
class Book(models.Model):
name = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ["name", "title", "birth_date"]
class BookForm(ModelForm):
class Meta:
model = Book
fields = ["name", "authors"]
使用這些模型,上面的 ModelForm
子類別大致等同於這個 (唯一的差異是 save()
方法,我們稍後會討論)。
from django import forms
class AuthorForm(forms.Form):
name = forms.CharField(max_length=100)
title = forms.CharField(
max_length=3,
widget=forms.Select(choices=TITLE_CHOICES),
)
birth_date = forms.DateField(required=False)
class BookForm(forms.Form):
name = forms.CharField(max_length=100)
authors = forms.ModelMultipleChoiceField(queryset=Author.objects.all())
ModelForm
上的驗證¶
驗證 ModelForm
有兩個主要步驟
就像一般的表單驗證一樣,模型表單驗證在呼叫 is_valid()
或存取 errors
屬性時會隱式觸發,並在呼叫 full_clean()
時會顯式觸發,雖然在實務上你通常不會使用後者的方法。
Model
驗證 (Model.full_clean()
) 是在表單驗證步驟中觸發,在呼叫表單的 clean()
方法之後。
警告
清理過程會以各種方式修改傳遞給 ModelForm
建構子的模型實例。例如,模型上的任何日期欄位都會轉換為實際的日期物件。驗證失敗可能會使底層的模型實例處於不一致的狀態,因此不建議重複使用它。
覆寫 clean()
方法¶
你可以覆寫模型表單上的 clean()
方法,以提供額外的驗證,方式與你在一般表單上相同。
附加到模型物件的模型表單實例將包含一個 instance
屬性,讓其方法可以存取該特定的模型實例。
警告
ModelForm.clean()
方法設定一個旗標,使模型驗證步驟驗證標記為 unique
、unique_together
或 unique_for_date|month|year
的模型欄位的唯一性。
如果你想要覆寫 clean()
方法並維持此驗證,你必須呼叫父類別的 clean()
方法。
與模型驗證的互動¶
作為驗證流程的一部分,ModelForm
會呼叫你的模型上每個在表單中有對應欄位的欄位的 clean()
方法。如果你排除任何模型欄位,驗證將不會在這些欄位上執行。請參閱表單驗證文件,以了解更多關於欄位清理和驗證如何運作的資訊。
模型的 clean()
方法會在執行任何唯一性檢查之前被呼叫。請參閱驗證物件,以取得更多關於模型 clean()
hook 的資訊。
關於模型的 error_messages
的注意事項¶
在表單 欄位
層級或在表單 Meta層級定義的錯誤訊息,總是優先於在模型 欄位
層級定義的錯誤訊息。
只有當在模型驗證步驟中引發 ValidationError
,並且在表單層級沒有定義對應的錯誤訊息時,才會使用在模型 欄位
上定義的錯誤訊息。
你可以透過將 NON_FIELD_ERRORS
鍵加入到 ModelForm
內部 Meta
類別的 error_messages
字典中,來覆寫模型驗證引發的 NON_FIELD_ERRORS
的錯誤訊息。
from django.core.exceptions import NON_FIELD_ERRORS
from django.forms import ModelForm
class ArticleForm(ModelForm):
class Meta:
error_messages = {
NON_FIELD_ERRORS: {
"unique_together": "%(model_name)s's %(field_labels)s are not unique.",
}
}
save()
方法¶
每個 ModelForm
也都有一個 save()
方法。此方法會從繫結到表單的資料建立並儲存一個資料庫物件。ModelForm
的子類別可以接受一個現有的模型實例作為關鍵字參數 instance
;如果提供了此參數,save()
將更新該實例。如果沒有提供,save()
將建立指定模型的新實例。
>>> from myapp.models import Article
>>> from myapp.forms import ArticleForm
# Create a form instance from POST data.
>>> f = ArticleForm(request.POST)
# Save a new Article object from the form's data.
>>> new_article = f.save()
# Create a form to edit an existing Article, but use
# POST data to populate the form.
>>> a = Article.objects.get(pk=1)
>>> f = ArticleForm(request.POST, instance=a)
>>> f.save()
請注意,如果表單尚未驗證,呼叫 save()
將會透過檢查 form.errors
來執行驗證。如果表單中的資料未通過驗證(也就是說,如果 form.errors
的結果為 True
),則會引發 ValueError
。
如果表單的資料中沒有出現選填欄位,則產生的模型實例會使用該欄位的模型欄位預設值
(如果有的話)。此行為不適用於使用 CheckboxInput
、CheckboxSelectMultiple
或 SelectMultiple
(或任何 value_omitted_from_data()
方法總是回傳 False
的自訂 Widget),因為未選取的核取方塊和未選取的 <select multiple>
不會出現在 HTML 表單提交的資料中。如果你正在設計 API,並希望使用其中一個 Widget 的欄位具有預設的回退行為,請使用自訂的表單欄位或 Widget。
這個 save()
方法接受一個選填的 commit
關鍵字參數,該參數接受 True
或 False
。如果你使用 commit=False
呼叫 save()
,它將會回傳一個尚未儲存到資料庫的物件。在這種情況下,你需要自行在產生的模型實例上呼叫 save()
。如果你想要在儲存物件之前對其執行自訂處理,或想要使用其中一個特殊的模型儲存選項,這將會很有用。commit
的預設值是 True
。
當你的模型與另一個模型具有多對多關係時,使用 commit=False
也會有另一個副作用。如果你的模型具有多對多關係,並且你在儲存表單時指定 commit=False
,Django 無法立即儲存多對多關係的表單資料。這是因為在資料庫中存在實例之前,無法儲存實例的多對多資料。
為了解決這個問題,每次使用 commit=False
儲存表單時,Django 會在你的 ModelForm
子類別中新增一個 save_m2m()
方法。在手動儲存表單產生的實例之後,你可以呼叫 save_m2m()
來儲存多對多表單資料。例如:
# Create a form instance with POST data.
>>> f = AuthorForm(request.POST)
# Create, but don't save the new author instance.
>>> new_author = f.save(commit=False)
# Modify the author in some way.
>>> new_author.some_field = "some_value"
# Save the new instance.
>>> new_author.save()
# Now, save the many-to-many data for the form.
>>> f.save_m2m()
只有在你使用 save(commit=False)
時才需要呼叫 save_m2m()
。當你在表單上使用 save()
時,所有資料(包括多對多資料)都會被儲存,而不需要任何額外的方法呼叫。例如:
# Create a form instance with POST data.
>>> a = Author()
>>> f = AuthorForm(request.POST, instance=a)
# Create and save the new author instance. There's no need to do anything else.
>>> new_author = f.save()
除了 save()
和 save_m2m()
方法之外,ModelForm
的運作方式與任何其他 forms
表單完全相同。例如,is_valid()
方法用於檢查有效性,is_multipart()
方法用於判斷表單是否需要多部分檔案上傳(因此是否必須將 request.FILES
傳遞給表單)等。請參閱將上傳的檔案繫結到表單以取得更多資訊。
選擇要使用的欄位¶
強烈建議你使用 fields
屬性明確設定所有應在表單中編輯的欄位。如果不這樣做,當表單意外允許使用者設定某些欄位時,很容易導致安全問題,尤其是在模型中新增新欄位時。根據表單的呈現方式,這個問題可能甚至在網頁上都看不到。
另一種方法是自動包含所有欄位,或只移除某些欄位。眾所周知,這種基本方法安全性較低,並且已導致主要網站上的嚴重漏洞(例如GitHub)。
但是,在你可以保證這些安全問題不適用於你的情況下,可以使用兩種可用的快捷方式:
將
fields
屬性設定為特殊值'__all__'
,以表示應該使用模型中的所有欄位。例如:from django.forms import ModelForm class AuthorForm(ModelForm): class Meta: model = Author fields = "__all__"
將
ModelForm
的內部Meta
類別的exclude
屬性設定為要從表單中排除的欄位列表。例如
class PartialAuthorForm(ModelForm): class Meta: model = Author exclude = ["title"]
由於
Author
模型有 3 個欄位name
、title
和birth_date
,這將導致欄位name
和birth_date
出現在表單上。
如果使用這兩者中的任何一個,欄位在表單中出現的順序將會是欄位在模型中定義的順序,而 ManyToManyField
實例會最後出現。
此外,Django 會套用以下規則:如果你在模型欄位上設定 editable=False
,則從模型透過 ModelForm
建立的任何表單都不會包含該欄位。
注意
根據上述邏輯未包含在表單中的任何欄位,將不會由表單的 save()
方法設定。此外,如果你手動將排除的欄位新增回表單,它們將不會從模型實例初始化。
Django 會阻止任何嘗試儲存不完整模型的嘗試,因此如果模型不允許遺失的欄位為空,並且沒有為遺失的欄位提供預設值,任何嘗試使用遺失欄位 save()
ModelForm
的嘗試都會失敗。為了避免此失敗,你必須使用遺失但必要的欄位的初始值來實例化模型
author = Author(title="Mr")
form = PartialAuthorForm(request.POST, instance=author)
form.save()
或者,你可以使用 save(commit=False)
並手動設定任何額外需要的欄位
form = PartialAuthorForm(request.POST)
author = form.save(commit=False)
author.title = "Mr"
author.save()
請參閱關於儲存表單的章節,以了解更多關於使用 save(commit=False)
的詳細資訊。
覆寫預設欄位¶
如上方欄位類型表格所述,預設的欄位類型是合理的預設值。如果你的模型中有一個 DateField
,你很可能會希望它在表單中表示為 DateField
。但 ModelForm
提供了彈性,讓你能夠更改特定模型的表單欄位。
若要為欄位指定自訂的 widget,請使用內部 Meta
類別的 widgets
屬性。這應該是一個字典,將欄位名稱對應到 widget 類別或實例。
例如,如果你希望 Author
的 name
屬性的 CharField
由 <textarea>
而不是其預設的 <input type="text">
表示,你可以覆寫該欄位的 widget
from django.forms import ModelForm, Textarea
from myapp.models import Author
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ["name", "title", "birth_date"]
widgets = {
"name": Textarea(attrs={"cols": 80, "rows": 20}),
}
widgets
字典接受 widget 實例(例如,Textarea(...)
)或類別(例如,Textarea
)。請注意,對於具有非空 choices
屬性的模型欄位,widgets
字典會被忽略。在這種情況下,你必須覆寫表單欄位才能使用不同的 widget。
同樣地,如果你想進一步自訂欄位,可以指定內部 Meta
類別的 labels
、help_texts
和 error_messages
屬性。
例如,如果你想要自訂 name
欄位的所有面向使用者的字串的措辭
from django.utils.translation import gettext_lazy as _
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ["name", "title", "birth_date"]
labels = {
"name": _("Writer"),
}
help_texts = {
"name": _("Some useful help text."),
}
error_messages = {
"name": {
"max_length": _("This writer's name is too long."),
},
}
你也可以指定 field_classes
或 formfield_callback
來客製化表單實例化的欄位類型。
例如,如果你想為 slug
欄位使用 MySlugFormField
,你可以這樣做
from django.forms import ModelForm
from myapp.models import Article
class ArticleForm(ModelForm):
class Meta:
model = Article
fields = ["pub_date", "headline", "content", "reporter", "slug"]
field_classes = {
"slug": MySlugFormField,
}
或
from django.forms import ModelForm
from myapp.models import Article
def formfield_for_dbfield(db_field, **kwargs):
if db_field.name == "slug":
return MySlugFormField()
return db_field.formfield(**kwargs)
class ArticleForm(ModelForm):
class Meta:
model = Article
fields = ["pub_date", "headline", "content", "reporter", "slug"]
formfield_callback = formfield_for_dbfield
最後,如果你想要完全控制欄位(包括其類型、驗證器、是否為必要欄位等等),你可以像在一般的 Form
中一樣,宣告式地指定欄位。
如果你想指定欄位的驗證器,你可以宣告式地定義該欄位並設定其 validators
參數
from django.forms import CharField, ModelForm
from myapp.models import Article
class ArticleForm(ModelForm):
slug = CharField(validators=[validate_slug])
class Meta:
model = Article
fields = ["pub_date", "headline", "content", "reporter", "slug"]
注意
當你像這樣明確地實例化表單欄位時,了解 ModelForm
和一般的 Form
之間的關係非常重要。
ModelForm
是一個可以自動產生某些欄位的常規 Form
。自動產生的欄位取決於 Meta
類別的內容,以及哪些欄位已經宣告式地定義。基本上,ModelForm
將 **只** 產生表單中 **缺少** 的欄位,或者換句話說,就是沒有宣告式定義的欄位。
宣告式定義的欄位會保持原樣,因此對 Meta
屬性(例如 widgets
、labels
、help_texts
或 error_messages
)所做的任何自訂都會被忽略;這些只適用於自動產生的欄位。
同樣地,宣告式定義的欄位不會從對應的模型中提取其屬性,例如 max_length
或 required
。如果你想維持模型中指定的行為,你必須在宣告表單欄位時明確地設定相關參數。
例如,如果 Article
模型看起來像這樣
class Article(models.Model):
headline = models.CharField(
max_length=200,
null=True,
blank=True,
help_text="Use puns liberally",
)
content = models.TextField()
並且你想為 headline
做一些自訂驗證,同時保持 blank
和 help_text
值與指定的值相同,你可以像這樣定義 ArticleForm
class ArticleForm(ModelForm):
headline = MyFormField(
max_length=200,
required=False,
help_text="Use puns liberally",
)
class Meta:
model = Article
fields = ["headline", "content"]
你必須確保表單欄位的類型可以用於設定對應模型欄位的內容。當它們不相容時,你會得到一個 ValueError
,因為不會進行隱式轉換。
請參閱表單欄位文件以取得有關欄位及其參數的更多資訊。
啟用欄位的本地化¶
預設情況下,ModelForm
中的欄位不會本地化其資料。若要啟用欄位的本地化,你可以使用 Meta
類別上的 localized_fields
屬性。
>>> from django.forms import ModelForm
>>> from myapp.models import Author
>>> class AuthorForm(ModelForm):
... class Meta:
... model = Author
... localized_fields = ['birth_date']
如果 localized_fields
設定為特殊值 '__all__'
,則所有欄位都將本地化。
表單繼承¶
與基本表單一樣,你可以透過繼承來擴展和重複使用 ModelForms
。如果你需要在父類別上宣告額外的欄位或額外的方法,以供許多衍生自模型的表單使用,這會很有用。例如,使用先前的 ArticleForm
類別
>>> class EnhancedArticleForm(ArticleForm):
... def clean_pub_date(self): ...
...
這會建立一個行為與 ArticleForm
完全相同的表單,除了 pub_date
欄位有一些額外的驗證和清理。
如果你想更改 Meta.fields
或 Meta.exclude
列表,你也可以子類化父代的 Meta
內部類別
>>> class RestrictedArticleForm(EnhancedArticleForm):
... class Meta(ArticleForm.Meta):
... exclude = ["body"]
...
這會從 EnhancedArticleForm
新增額外的方法,並修改原始的 ArticleForm.Meta
以移除一個欄位。
但是,有幾件事需要注意。
正常的 Python 名稱解析規則適用。如果你有多個宣告
Meta
內部類別的基底類別,則只會使用第一個。這表示子類的Meta
(如果存在),否則使用第一個父類的Meta
,依此類推。可以同時繼承
Form
和ModelForm
,但是,你必須確保ModelForm
出現在 MRO 中的第一個位置。這是因為這些類別依賴不同的 metaclass,而一個類別只能有一個 metaclass。可以透過在子類別上將名稱設定為
None
,以宣告方式移除從父類別繼承的Field
。你只能使用此技術來選擇退出由父類別以宣告方式定義的欄位;它不會阻止
ModelForm
metaclass 產生預設欄位。若要選擇退出預設欄位,請參閱選取要使用的欄位。
提供初始值¶
與一般表單一樣,可以透過在實例化表單時指定 initial
參數來指定表單的初始資料。以這種方式提供的初始值將覆寫表單欄位的初始值和附加模型實例的值。例如
>>> article = Article.objects.get(pk=1)
>>> article.headline
'My headline'
>>> form = ArticleForm(initial={"headline": "Initial headline"}, instance=article)
>>> form["headline"].value()
'Initial headline'
ModelForm 工廠函數¶
你可以使用獨立函數 modelform_factory()
從給定的模型建立表單,而不是使用類別定義。如果你沒有太多自訂需要進行,這可能會更方便
>>> from django.forms import modelform_factory
>>> from myapp.models import Book
>>> BookForm = modelform_factory(Book, fields=["author", "title"])
這也可以用於修改現有的表單,例如透過指定要用於給定欄位的 widget
>>> from django.forms import Textarea
>>> Form = modelform_factory(Book, form=BookForm, widgets={"title": Textarea()})
可以使用 fields
和 exclude
關鍵字參數,或 ModelForm
內部 Meta
類別上的對應屬性,指定要包含的欄位。請參閱 ModelForm
的選取要使用的欄位文件。
... 或啟用特定欄位的本地化
>>> Form = modelform_factory(Author, form=AuthorForm, localized_fields=["birth_date"])
模型表單集¶
- class models.BaseModelFormSet¶
與一般表單集一樣,Django 提供了一些增強的表單集類別,使處理 Django 模型更方便。讓我們重複使用上面的 Author
模型
>>> from django.forms import modelformset_factory
>>> from myapp.models import Author
>>> AuthorFormSet = modelformset_factory(Author, fields=["name", "title"])
使用 fields
將表單集限制為僅使用給定的欄位。或者,你可以採取「選擇退出」的方法,指定要排除的欄位
>>> AuthorFormSet = modelformset_factory(Author, exclude=["birth_date"])
這將會建立一個能夠處理與 Author
模型相關聯的資料的 formset。它的運作方式與一般的 formset 相同。
>>> formset = AuthorFormSet()
>>> print(formset)
<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS"><input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS"><input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS"><input type="hidden" name="form-MAX_NUM_FORMS" value="1000" id="id_form-MAX_NUM_FORMS">
<div><label for="id_form-0-name">Name:</label><input id="id_form-0-name" type="text" name="form-0-name" maxlength="100"></div>
<div><label for="id_form-0-title">Title:</label><select name="form-0-title" id="id_form-0-title">
<option value="" selected>---------</option>
<option value="MR">Mr.</option>
<option value="MRS">Mrs.</option>
<option value="MS">Ms.</option>
</select><input type="hidden" name="form-0-id" id="id_form-0-id"></div>
注意
modelformset_factory()
使用 formset_factory()
來產生 formset。這表示模型 formset 是基本 formset 的延伸,它知道如何與特定的模型互動。
注意
當使用 多表繼承 時,由 formset factory 產生的表單將會包含一個父連結欄位(預設為 <parent_model_name>_ptr
),而不是 id
欄位。
變更 queryset¶
預設情況下,當您從模型建立 formset 時,該 formset 將使用包含模型中所有物件的 queryset(例如,Author.objects.all()
)。您可以使用 queryset
參數覆寫此行為。
>>> formset = AuthorFormSet(queryset=Author.objects.filter(name__startswith="O"))
或者,您也可以建立一個子類別,在 __init__
中設定 self.queryset
。
from django.forms import BaseModelFormSet
from myapp.models import Author
class BaseAuthorFormSet(BaseModelFormSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.queryset = Author.objects.filter(name__startswith="O")
然後,將您的 BaseAuthorFormSet
類別傳遞給 factory 函式。
>>> AuthorFormSet = modelformset_factory(
... Author, fields=["name", "title"], formset=BaseAuthorFormSet
... )
如果您想回傳一個不包含任何模型預先存在實例的 formset,您可以指定一個空的 QuerySet。
>>> AuthorFormSet(queryset=Author.objects.none())
變更表單¶
預設情況下,當您使用 modelformset_factory
時,將會使用 modelform_factory()
建立一個模型表單。通常,指定一個自訂模型表單會很有用。例如,您可以建立一個具有自訂驗證的自訂模型表單。
class AuthorForm(forms.ModelForm):
class Meta:
model = Author
fields = ["name", "title"]
def clean_name(self):
# custom validation for the name field
...
然後,將您的模型表單傳遞給 factory 函式。
AuthorFormSet = modelformset_factory(Author, form=AuthorForm)
並不總是需要定義自訂模型表單。modelformset_factory
函式具有多個參數,這些參數會傳遞給 modelform_factory
,如下所述。
使用 widgets
指定要在表單中使用的 widget¶
使用 widgets
參數,您可以指定一個值字典來自訂特定欄位的 ModelForm
的 widget 類別。這與 ModelForm
的內部 Meta
類別上的 widgets
字典的運作方式相同。
>>> AuthorFormSet = modelformset_factory(
... Author,
... fields=["name", "title"],
... widgets={"name": Textarea(attrs={"cols": 80, "rows": 20})},
... )
使用 localized_fields
啟用欄位的本地化¶
使用 localized_fields
參數,您可以啟用表單中欄位的本地化。
>>> AuthorFormSet = modelformset_factory(
... Author, fields=['name', 'title', 'birth_date'],
... localized_fields=['birth_date'])
如果 localized_fields
設定為特殊值 '__all__'
,則所有欄位都將本地化。
提供初始值¶
與一般 formset 一樣,可以透過在實例化由 modelformset_factory()
回傳的模型 formset 類別時指定 initial
參數,來為 formset 中的表單指定初始資料。但是,對於模型 formset,初始值僅適用於額外的表單,這些表單未附加到現有的模型實例。如果 initial
的長度超過額外表單的數量,則會忽略多餘的初始資料。如果具有初始資料的額外表單未被使用者變更,它們將不會被驗證或儲存。
儲存 formset 中的物件¶
與 ModelForm
一樣,您可以將資料儲存為模型物件。這是透過 formset 的 save()
方法完成的。
# Create a formset instance with POST data.
>>> formset = AuthorFormSet(request.POST)
# Assuming all is valid, save the data.
>>> instances = formset.save()
save()
方法會回傳已儲存到資料庫的實例。如果在繫結資料中給定實例的資料沒有變更,則該實例不會儲存到資料庫,並且不會包含在回傳值中(在上面的範例中為 instances
)。
當表單中缺少欄位時(例如,因為它們已被排除),這些欄位將不會由 save()
方法設定。您可以在選擇要使用的欄位中找到有關此限制的更多資訊,此限制也適用於常規的 ModelForms
。
傳遞 commit=False
以回傳未儲存的模型實例。
# don't save to the database
>>> instances = formset.save(commit=False)
>>> for instance in instances:
... # do something with instance
... instance.save()
...
這讓您能夠在將資料儲存到資料庫之前將資料附加到實例。如果您的 formset 包含 ManyToManyField
,您還需要呼叫 formset.save_m2m()
,以確保正確儲存多對多關係。
呼叫 save()
後,您的模型 formset 將具有三個新的屬性,其中包含 formset 的變更。
- models.BaseModelFormSet.changed_objects¶
- models.BaseModelFormSet.deleted_objects¶
- models.BaseModelFormSet.new_objects¶
限制可編輯物件的數量¶
與一般 formset 一樣,您可以使用 max_num
和 extra
參數來限制顯示的額外表單數量 modelformset_factory()
。
max_num
不會阻止顯示現有物件。
>>> Author.objects.order_by("name")
<QuerySet [<Author: Charles Baudelaire>, <Author: Paul Verlaine>, <Author: Walt Whitman>]>
>>> AuthorFormSet = modelformset_factory(Author, fields=["name"], max_num=1)
>>> formset = AuthorFormSet(queryset=Author.objects.order_by("name"))
>>> [x.name for x in formset.get_queryset()]
['Charles Baudelaire', 'Paul Verlaine', 'Walt Whitman']
此外,extra=0
不會阻止建立新的模型實例,因為您可以使用 JavaScript 新增其他表單或傳送額外的 POST 資料。請參閱防止建立新物件,以了解如何執行此操作。
如果 max_num
的值大於現有相關物件的數量,則最多會將 extra
個額外的空白表單新增到 formset 中,只要表單總數不超過 max_num
。
>>> AuthorFormSet = modelformset_factory(Author, fields=["name"], max_num=4, extra=2)
>>> formset = AuthorFormSet(queryset=Author.objects.order_by("name"))
>>> for form in formset:
... print(form)
...
<div><label for="id_form-0-name">Name:</label><input id="id_form-0-name" type="text" name="form-0-name" value="Charles Baudelaire" maxlength="100"><input type="hidden" name="form-0-id" value="1" id="id_form-0-id"></div>
<div><label for="id_form-1-name">Name:</label><input id="id_form-1-name" type="text" name="form-1-name" value="Paul Verlaine" maxlength="100"><input type="hidden" name="form-1-id" value="3" id="id_form-1-id"></div>
<div><label for="id_form-2-name">Name:</label><input id="id_form-2-name" type="text" name="form-2-name" value="Walt Whitman" maxlength="100"><input type="hidden" name="form-2-id" value="2" id="id_form-2-id"></div>
<div><label for="id_form-3-name">Name:</label><input id="id_form-3-name" type="text" name="form-3-name" maxlength="100"><input type="hidden" name="form-3-id" id="id_form-3-id"></div>
max_num
值為 None
(預設值)會對顯示的表單數量設定一個高限制(1000)。實際上,這相當於沒有限制。
防止建立新物件¶
使用 edit_only
參數,您可以防止建立任何新物件。
>>> AuthorFormSet = modelformset_factory(
... Author,
... fields=["name", "title"],
... edit_only=True,
... )
在這裡,formset 將僅編輯現有的 Author
實例。不會建立或編輯任何其他物件。
在檢視中使用模型 formset¶
模型 formset 非常類似於 formset。假設我們要顯示一個 formset 來編輯 Author
模型實例。
from django.forms import modelformset_factory
from django.shortcuts import render
from myapp.models import Author
def manage_authors(request):
AuthorFormSet = modelformset_factory(Author, fields=["name", "title"])
if request.method == "POST":
formset = AuthorFormSet(request.POST, request.FILES)
if formset.is_valid():
formset.save()
# do something.
else:
formset = AuthorFormSet()
return render(request, "manage_authors.html", {"formset": formset})
如您所見,模型 formset 的檢視邏輯與「正常」formset 的檢視邏輯沒有太大的差異。唯一的區別在於我們呼叫 formset.save()
將資料儲存到資料庫。(這已在上面的儲存 formset 中的物件中說明。)
覆寫 ModelFormSet
上的 clean()
¶
如同 ModelForms
,預設情況下,ModelFormSet
的 clean()
方法會驗證表單集中沒有任何項目違反模型上的唯一性限制(unique
、unique_together
或 unique_for_date|month|year
)。如果您想要覆寫 ModelFormSet
上的 clean()
方法並維持此驗證,您必須呼叫父類別的 clean
方法。
from django.forms import BaseModelFormSet
class MyModelFormSet(BaseModelFormSet):
def clean(self):
super().clean()
# example custom validation across forms in the formset
for form in self.forms:
# your custom formset validation
...
另外請注意,當您到達此步驟時,每個 Form
都已建立個別的模型實例。修改 form.cleaned_data
中的值不足以影響儲存的值。如果您想在 ModelFormSet.clean()
中修改值,您必須修改 form.instance
。
from django.forms import BaseModelFormSet
class MyModelFormSet(BaseModelFormSet):
def clean(self):
super().clean()
for form in self.forms:
name = form.cleaned_data["name"].upper()
form.cleaned_data["name"] = name
# update the instance value.
form.instance.name = name
使用自訂 queryset¶
如前所述,您可以覆寫模型表單集使用的預設 queryset。
from django.forms import modelformset_factory
from django.shortcuts import render
from myapp.models import Author
def manage_authors(request):
AuthorFormSet = modelformset_factory(Author, fields=["name", "title"])
queryset = Author.objects.filter(name__startswith="O")
if request.method == "POST":
formset = AuthorFormSet(
request.POST,
request.FILES,
queryset=queryset,
)
if formset.is_valid():
formset.save()
# Do something.
else:
formset = AuthorFormSet(queryset=queryset)
return render(request, "manage_authors.html", {"formset": formset})
請注意,在此範例中,我們在 POST
和 GET
情況下都傳遞了 queryset
引數。
在範本中使用表單集¶
在 Django 範本中,有三種方式可以呈現表單集。
首先,您可以讓表單集處理大部分的工作。
<form method="post">
{{ formset }}
</form>
第二,您可以手動呈現表單集,但讓表單自行處理。
<form method="post">
{{ formset.management_form }}
{% for form in formset %}
{{ form }}
{% endfor %}
</form>
當您手動呈現表單時,請務必如上所示呈現管理表單。請參閱 管理表單文件。
第三,您可以手動呈現每個欄位。
<form method="post">
{{ formset.management_form }}
{% for form in formset %}
{% for field in form %}
{{ field.label_tag }} {{ field }}
{% endfor %}
{% endfor %}
</form>
如果您選擇使用第三種方法,並且未使用 {% for %}
迴圈來迭代欄位,則您需要呈現主鍵欄位。例如,如果您要呈現模型的 name
和 age
欄位。
<form method="post">
{{ formset.management_form }}
{% for form in formset %}
{{ form.id }}
<ul>
<li>{{ form.name }}</li>
<li>{{ form.age }}</li>
</ul>
{% endfor %}
</form>
請注意我們需要明確呈現 {{ form.id }}
。這確保模型表單集在 POST
情況下可以正常運作。(此範例假設主鍵名稱為 id
。如果您已明確定義自己的主鍵,而不是稱為 id
,請確保它被呈現。)
內嵌表單集¶
- class models.BaseInlineFormSet¶
內嵌表單集是模型表單集之上的一個小型抽象層。它們簡化了通過外鍵處理相關物件的情況。假設您有這兩個模型:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
author = models.ForeignKey(Author, on_delete=models.CASCADE)
title = models.CharField(max_length=100)
如果您想要建立一個表單集,讓您可以編輯屬於特定作者的書籍,您可以這樣做:
>>> from django.forms import inlineformset_factory
>>> BookFormSet = inlineformset_factory(Author, Book, fields=["title"])
>>> author = Author.objects.get(name="Mike Royko")
>>> formset = BookFormSet(instance=author)
BookFormSet
的 前綴 是 'book_set'
(<模型名稱>_set
)。如果 Book
到 Author
的 ForeignKey
有一個 related_name
,則會改用它。
注意
inlineformset_factory()
使用 modelformset_factory()
並標記 can_delete=True
。
覆寫 InlineFormSet
上的方法¶
在覆寫 InlineFormSet
上的方法時,您應該將 BaseInlineFormSet
子類化,而不是 BaseModelFormSet
。
例如,如果您想要覆寫 clean()
:
from django.forms import BaseInlineFormSet
class CustomInlineFormSet(BaseInlineFormSet):
def clean(self):
super().clean()
# example custom validation across forms in the formset
for form in self.forms:
# your custom formset validation
...
另請參閱 覆寫 ModelFormSet 上的 clean()。
然後,當您建立內嵌表單集時,傳入可選引數 formset
:
>>> from django.forms import inlineformset_factory
>>> BookFormSet = inlineformset_factory(
... Author, Book, fields=["title"], formset=CustomInlineFormSet
... )
>>> author = Author.objects.get(name="Mike Royko")
>>> formset = BookFormSet(instance=author)
同一個模型有多個外鍵¶
如果您的模型包含多個指向同一個模型的外鍵,您需要使用 fk_name
手動解決歧義。例如,考慮以下模型:
class Friendship(models.Model):
from_friend = models.ForeignKey(
Friend,
on_delete=models.CASCADE,
related_name="from_friends",
)
to_friend = models.ForeignKey(
Friend,
on_delete=models.CASCADE,
related_name="friends",
)
length_in_months = models.IntegerField()
為了解決這個問題,您可以使用 fk_name
到 inlineformset_factory()
。
>>> FriendshipFormSet = inlineformset_factory(
... Friend, Friendship, fk_name="from_friend", fields=["to_friend", "length_in_months"]
... )
在視圖中使用內嵌表單集¶
您可能想要提供一個視圖,讓使用者可以編輯模型的相關物件。以下是如何做到這一點:
def manage_books(request, author_id):
author = Author.objects.get(pk=author_id)
BookInlineFormSet = inlineformset_factory(Author, Book, fields=["title"])
if request.method == "POST":
formset = BookInlineFormSet(request.POST, request.FILES, instance=author)
if formset.is_valid():
formset.save()
# Do something. Should generally end with a redirect. For example:
return HttpResponseRedirect(author.get_absolute_url())
else:
formset = BookInlineFormSet(instance=author)
return render(request, "manage_books.html", {"formset": formset})
請注意,我們在 POST
和 GET
情況下都傳遞了 instance
。
指定要在內嵌表單中使用的 widget¶
inlineformset_factory
使用 modelformset_factory
並將其大多數引數傳遞給 modelformset_factory
。這表示您可以使用 widgets
參數,就像將它傳遞給 modelformset_factory
一樣。請參閱上面的 使用 widget 指定要在表單中使用的 widget。