从模型创建表单

ModelForm

class ModelForm[source]

如果您正在构建一个基于数据库的应用程序,则很有可能您将拥有与 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

IntegerField,其中 min_value 设置为 -9223372036854775808 且 max_value 设置为 9223372036854775807。

BinaryField

CharField,如果模型字段上的 editable 设置为 True,否则表单中未表示。

BooleanField

BooleanField,或者如果 null=True 则为 NullBooleanField

CharField

CharField,其中 max_length 设置为模型字段的 max_lengthempty_value 如果 null=True 则设置为 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

CharField,其中 widget=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属性,该属性使它的方法可以访问该特定模型实例。

警告

The ModelForm.clean()方法设置一个标志,该标志使模型验证步骤验证标记为uniqueunique_togetherunique_for_date|month|year的模型字段的唯一性。

如果您想覆盖clean()方法并保持此验证,则必须调用父类的clean()方法。

与模型验证的交互

作为验证过程的一部分,ModelForm将调用模型中每个具有对应表单字段的字段的clean()方法。如果您排除了任何模型字段,则不会对这些字段运行验证。有关字段清理和验证的工作原理的更多信息,请参阅表单验证文档。

模型的clean()方法将在进行任何唯一性检查之前被调用。有关模型的clean()钩子的更多信息,请参阅验证对象

关于模型的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.",
            }
        }

The 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

如果可选字段未出现在表单数据中,则生成的模型实例将使用模型字段default(如果存在)作为该字段的值。此行为不适用于使用CheckboxInputCheckboxSelectMultipleSelectMultiple(或任何自定义小部件,其value_omitted_from_data()方法始终返回False)的字段,因为未选中的复选框和未选择的<select multiple>不会出现在HTML表单提交的数据中。如果您正在设计API并希望使用这些小部件之一的字段具有默认回退行为,请使用自定义表单字段或小部件。

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 将阻止任何保存不完整模型的尝试,因此,如果模型不允许缺少的字段为空,并且没有为缺少的字段提供默认值,则任何使用缺少字段保存 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 使您可以灵活地更改给定模型的表单字段。

要为字段指定自定义小部件,请使用内部 Meta 类的 widgets 属性。这应该是一个字典,将字段名称映射到小部件类或实例。

例如,如果您希望 Authorname 属性的 CharField<textarea> 而不是其默认的 <input type="text"> 表示,您可以覆盖字段的小部件

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 字典接受小部件实例(例如,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中首先出现。这是因为这些类依赖于不同的元类,并且一个类只能有一个元类。

  • 可以通过将子类上的名称设置为None来声明性地删除从父类继承的Field

    此技术只能用于选择退出父类声明性定义的字段;它不会阻止ModelForm元类生成默认字段。要选择退出默认字段,请参阅选择要使用的字段

提供初始值

与常规表单一样,可以通过在实例化表单时指定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"])

这也可以用于对现有表单进行修改,例如通过指定要用于给定字段的小部件。

>>> 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 = 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()生成表单集。这意味着模型表单集是基本表单集的扩展,它知道如何与特定模型交互。

注意

在使用多表继承时,表单集工厂生成的表单将包含一个父链接字段(默认为<parent_model_name>_ptr),而不是id字段。

更改查询集

默认情况下,当您从模型创建表单集时,表单集将使用一个包含模型中所有对象的查询集(例如,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类传递给工厂函数。

>>> AuthorFormSet = modelformset_factory(
...     Author, fields=["name", "title"], formset=BaseAuthorFormSet
... )

如果要返回不包含模型任何现有实例的表单集,可以指定一个空查询集。

>>> 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
        ...

然后,将您的模型表单传递给工厂函数。

AuthorFormSet = modelformset_factory(Author, form=AuthorForm)

并非总是需要定义自定义模型表单。modelformset_factory函数有一些参数传递给modelform_factory,如下所述。

使用widgets在表单中指定要使用的小部件

使用widgets参数,可以指定一个值字典来自定义特定字段的ModelForm的小部件类。这与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__',则所有字段都将被本地化。

提供初始值

与常规表单集一样,可以通过在实例化modelformset_factory()返回的模型表单集类时指定initial参数来指定表单集中的表单的初始数据。但是,对于模型表单集,初始值仅适用于额外表单,即未附加到现有模型实例的表单。如果initial的长度超过额外表单的数量,则将忽略多余的初始数据。如果用户没有更改具有初始数据的额外表单,则不会对其进行验证或保存。

保存表单集中的对象

ModelForm一样,可以将数据保存为模型对象。这是通过表单集的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()
...

这使您能够在将实例保存到数据库之前向其附加数据。如果您的表单集包含ManyToManyField,则还需要调用formset.save_m2m()以确保正确保存多对多关系。

调用save()后,您的模型表单集将具有三个新的属性,其中包含表单集的更改。

models.BaseModelFormSet.changed_objects
models.BaseModelFormSet.deleted_objects
models.BaseModelFormSet.new_objects

限制可编辑对象的数量

与常规表单集一样,您可以使用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 个额外的空白表单,只要表单总数不超过 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,
... )

在此,表单集将仅编辑现有的 Author 实例。不会创建或编辑任何其他对象。

在视图中使用模型表单集

模型表单集与表单集非常相似。假设我们要呈现一个表单集来编辑 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.save() 将数据保存到数据库中。(这在上面表单集中保存对象中进行了描述)。

覆盖 ModelFormSet 上的 clean()

就像 ModelForms 一样,默认情况下,ModelFormSetclean() 方法将验证表单集中没有任何项违反模型上的唯一约束(无论是 uniqueunique_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

使用自定义查询集

如前所述,您可以覆盖模型表单集使用的默认查询集。

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'<model name>_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_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})

请注意,我们在 POSTGET 情况下都传递了 instance

指定要在内联表单中使用的部件

inlineformset_factory 使用 modelformset_factory 并将其大多数参数传递给 modelformset_factory。这意味着您可以像传递给 modelformset_factory 一样使用 widgets 参数。请参阅上面使用 widgets 在表单中指定要使用的部件

返回顶部