从模型创建表单 — Django 文档

来自菜鸟教程
Django/docs/3.0.x/topics/forms/modelforms
跳转至:导航、​搜索

从模型创建表单

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在model字段上设置为True,否则不在表格中表示。
BooleanField BooleanFieldNullBooleanField 如果 null=True
CharField CharFieldmax_length 设置为模型字段的 max_lengthempty_value 设置为 None 如果 null=True
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
ManyToManyField ModelMultipleChoiceField(见下文)
NullBooleanField NullBooleanField
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() 挂钩的更多信息,请参阅 验证对象


关于型号 error_messages 的注意事项

表单字段 级别或 表单元 级别定义的错误消息始终优先于在 模型字段 级别定义的错误消息。

模型字段 上定义的错误消息仅在 模型验证 步骤期间引发 ValidationError 时使用,并且没有在表单级别定义相应的错误消息。

您可以通过将 NON_FIELD_ERRORS 键添加到 ModelForm 的内部 Meta 类:

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

如果可选字段未出现在表单数据中,则生成的模型实例将模型字段 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 会阻止保存不完整模型的任何尝试,因此如果模型不允许缺失的字段为空,并且没有为缺失的字段提供默认值,则任何尝试 save() a 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 字典。 在这种情况下,您必须覆盖表单字段以使用不同的小部件。

同样,如果您想进一步自定义字段,您可以指定内部 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_classes 来自定义表单实例化的字段类型。

例如,如果您想将 MySlugFormField 用于 slug 字段,您可以执行以下操作:

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,
        }

最后,如果你想完全控制一个字段——包括它的类型、验证器、必填项等。 – 您可以像在常规 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 类的内容以及已经以声明方式定义的字段。 基本上,ModelFormonly 生成表单中 missing 的字段,或者换句话说,未声明性定义的字段。

以声明方式定义的字段保持原样,因此对 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_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

regular formsets 一样,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-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS">
<tr><th><label for="id_form-0-name">Name:</label></th><td><input id="id_form-0-name" type="text" name="form-0-name" maxlength="100"></td></tr>
<tr><th><label for="id_form-0-title">Title:</label></th><td><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"></td></tr>

笔记

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)

如果要返回不包含模型的 任何 预先存在的实例的表单集,您可以指定一个空的 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
        ...

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

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 数据。 表单集 :ticket:`还没有提供功能 <26142>` 用于防止创建新实例的“仅编辑”视图。

如果max_num的值大于现有相关对象的数量,最多extra个额外的空白表单将被添加到表单集中,只要表单的总数不超过[ X204X]:

>>> 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.as_table())
<tr><th><label for="id_form-0-name">Name:</label></th><td><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"></td></tr>
<tr><th><label for="id_form-1-name">Name:</label></th><td><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"></td></tr>
<tr><th><label for="id_form-2-name">Name:</label></th><td><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"></td></tr>
<tr><th><label for="id_form-3-name">Name:</label></th><td><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"></td></tr>

None(默认值)的 max_num 值对显示的表单数量(1000)设置了上限。 在实践中,这相当于没有限制。


在视图中使用模型表单集

模型表单集与表单集非常相似。 假设我们想要呈现一个表单集来编辑 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_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

使用自定义查询集

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

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,
            queryset=Author.objects.filter(name__startswith='O'),
        )
        if formset.is_valid():
            formset.save()
            # Do something.
    else:
        formset = AuthorFormSet(queryset=Author.objects.filter(name__startswith='O'))
    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 )。 如果 BookForeignKeyAuthor 具有 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


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

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