從模型建立表單

ModelForm

class 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。以下是完整的轉換列表

模型欄位

表單欄位

AutoField

不在表單中表示

BigAutoField

不在表單中表示

BigIntegerField

IntegerFieldmin_value 設定為 -9223372036854775808,且 max_value 設定為 9223372036854775807。

BinaryField

CharField,如果模型欄位上的 editable 設定為 True,否則不在表單中表示。

BooleanField

BooleanField,或如果 null=True,則為 NullBooleanField

CharField

CharFieldmax_length 設定為模型欄位的 max_length,且如果 null=True,則 empty_value 設定為 None

DateField

DateField

DateTimeField

DateTimeField

DecimalField

DecimalField

DurationField

DurationField

EmailField

EmailField

FileField

FileField

FilePathField

FilePathField

FloatField

FloatField

ForeignKey

ModelChoiceField (見下方)

ImageField

ImageField

IntegerField

IntegerField

IPAddressField

IPAddressField

GenericIPAddressField

GenericIPAddressField

JSONField

JSONField

ManyToManyField

ModelMultipleChoiceField (見下方)

PositiveBigIntegerField

IntegerField

PositiveIntegerField

IntegerField

PositiveSmallIntegerField

IntegerField

SlugField

SlugField

SmallAutoField

不在表單中表示

SmallIntegerField

IntegerField

TextField

CharFieldwidget=forms.Textarea

TimeField

TimeField

URLField

URLField

UUIDField

UUIDField

如你所預期,ForeignKeyManyToManyField 模型欄位類型是特殊情況

  • ForeignKeydjango.forms.ModelChoiceField 表示,這是一個 ChoiceField,其選項是一個模型 QuerySet

  • ManyToManyFielddjango.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 有兩個主要步驟

  1. 驗證表單

  2. 驗證模型實例

就像一般的表單驗證一樣,模型表單驗證在呼叫 is_valid() 或存取 errors 屬性時會隱式觸發,並在呼叫 full_clean() 時會顯式觸發,雖然在實務上你通常不會使用後者的方法。

Model 驗證 (Model.full_clean()) 是在表單驗證步驟中觸發,在呼叫表單的 clean() 方法之後。

警告

清理過程會以各種方式修改傳遞給 ModelForm 建構子的模型實例。例如,模型上的任何日期欄位都會轉換為實際的日期物件。驗證失敗可能會使底層的模型實例處於不一致的狀態,因此不建議重複使用它。

覆寫 clean() 方法

你可以覆寫模型表單上的 clean() 方法,以提供額外的驗證,方式與你在一般表單上相同。

附加到模型物件的模型表單實例將包含一個 instance 屬性,讓其方法可以存取該特定的模型實例。

警告

ModelForm.clean() 方法設定一個旗標,使模型驗證步驟驗證標記為 uniqueunique_togetherunique_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

如果表單的資料中沒有出現選填欄位,則產生的模型實例會使用該欄位的模型欄位預設值(如果有的話)。此行為不適用於使用 CheckboxInputCheckboxSelectMultipleSelectMultiple(或任何 value_omitted_from_data() 方法總是回傳 False 的自訂 Widget),因為未選取的核取方塊和未選取的 <select multiple> 不會出現在 HTML 表單提交的資料中。如果你正在設計 API,並希望使用其中一個 Widget 的欄位具有預設的回退行為,請使用自訂的表單欄位或 Widget。

這個 save() 方法接受一個選填的 commit 關鍵字參數,該參數接受 TrueFalse。如果你使用 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)。

但是,在你可以保證這些安全問題不適用於你的情況下,可以使用兩種可用的快捷方式:

  1. fields 屬性設定為特殊值 '__all__',以表示應該使用模型中的所有欄位。例如:

    from django.forms import ModelForm
    
    
    class AuthorForm(ModelForm):
        class Meta:
            model = Author
            fields = "__all__"
    
  2. ModelForm 的內部 Meta 類別的 exclude 屬性設定為要從表單中排除的欄位列表。

    例如

    class PartialAuthorForm(ModelForm):
        class Meta:
            model = Author
            exclude = ["title"]
    

    由於 Author 模型有 3 個欄位 nametitlebirth_date,這將導致欄位 namebirth_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 類別或實例。

例如,如果你希望 Authorname 屬性的 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 類別的 labelshelp_textserror_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_classesformfield_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 屬性(例如 widgetslabelshelp_textserror_messages)所做的任何自訂都會被忽略;這些只適用於自動產生的欄位。

同樣地,宣告式定義的欄位不會從對應的模型中提取其屬性,例如 max_lengthrequired。如果你想維持模型中指定的行為,你必須在宣告表單欄位時明確地設定相關參數。

例如,如果 Article 模型看起來像這樣

class Article(models.Model):
    headline = models.CharField(
        max_length=200,
        null=True,
        blank=True,
        help_text="Use puns liberally",
    )
    content = models.TextField()

並且你想為 headline 做一些自訂驗證,同時保持 blankhelp_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.fieldsMeta.exclude 列表,你也可以子類化父代的 Meta 內部類別

>>> class RestrictedArticleForm(EnhancedArticleForm):
...     class Meta(ArticleForm.Meta):
...         exclude = ["body"]
...

這會從 EnhancedArticleForm 新增額外的方法,並修改原始的 ArticleForm.Meta 以移除一個欄位。

但是,有幾件事需要注意。

  • 正常的 Python 名稱解析規則適用。如果你有多個宣告 Meta 內部類別的基底類別,則只會使用第一個。這表示子類的 Meta(如果存在),否則使用第一個父類的 Meta,依此類推。

  • 可以同時繼承 FormModelForm,但是,你必須確保 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()})

可以使用 fieldsexclude 關鍵字參數,或 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_numextra 參數來限制顯示的額外表單數量 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,預設情況下,ModelFormSetclean() 方法會驗證表單集中沒有任何項目違反模型上的唯一性限制(uniqueunique_togetherunique_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})

請注意,在此範例中,我們在 POSTGET 情況下都傳遞了 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 %} 迴圈來迭代欄位,則您需要呈現主鍵欄位。例如,如果您要呈現模型的 nameage 欄位。

<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)。如果 BookAuthorForeignKey 有一個 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_nameinlineformset_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})

請注意,我們在 POSTGET 情況下都傳遞了 instance

指定要在內嵌表單中使用的 widget

inlineformset_factory 使用 modelformset_factory 並將其大多數引數傳遞給 modelformset_factory。這表示您可以使用 widgets 參數,就像將它傳遞給 modelformset_factory 一樣。請參閱上面的 使用 widget 指定要在表單中使用的 widget

返回頂部