Skip to main content

从模型创建表单

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。

BooleanField

BooleanField

CharField

CharFieldmax_length 设置为模型字段 max_length

CommaSeparatedIntegerField

CharField

DateField

DateField

DateTimeField

DateTimeField

DecimalField

DecimalField

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

SmallIntegerField

IntegerField

TextField

CharFieldwidget=forms.Textarea

TimeField

TimeField

URLField

URLField

如您所料,ForeignKeyManyToManyField 模型字段类型是特殊情况:

  • ForeignKeydjango.forms.ModelChoiceField 表示,django.forms.ModelChoiceField 是其选择是 QuerySet 模型的 ChoiceField

  • ManyToManyFielddjango.forms.ModelMultipleChoiceField 表示,django.forms.ModelMultipleChoiceField 是其选择是 QuerySet 模型的 MultipleChoiceField

此外,每个生成的表单字段的属性设置如下:

  • 如果模型字段具有 blank=True,则在表单字段上将 required 设置为 False。否则,required=True

  • 表单字段的 label 设置为模型字段的 verbose_name,第一个字符大写。

  • 表单字段的 help_text 设置为模型字段的 help_text

  • 如果模型字段具有 choices 集合,则表单字段的 widget 将被设置为 Select,其中选择来自模型字段的 choices。选项通常包括默认选择的空白选项。如果需要该字段,则强制用户进行选择。如果模型字段具有 blank=False 和显式 default 值(将首先选择 default 值),则不会包括空白选择。

最后,请注意,您可以覆盖用于给定模型字段的表单字段。见下面的 Overriding the default fields

一个完整的例子

考虑这套模型:

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):              # __unicode__ on Python 2
        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 的注意事项

form field 级别或在 形式元 级别定义的错误消息始终优先于在 model field 级别定义的错误消息。

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

您可以通过将 NON_FIELD_ERRORS 密钥添加到 ModelForm 内部 Meta 类的 error_messages 字典中来覆盖模型验证引发的来自 NON_FIELD_ERRORS 的错误消息:

from django.forms import ModelForm
from django.core.exceptions import NON_FIELD_ERRORS

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 (如果有)。此行为不适用于使用 CheckboxInputCheckboxSelectMultiple 的字段(或 value_omitted_from_data() 方法总是返回 False 的任何自定义窗口小部件),因为未选中的复选框不会出现在HTML表单提交的数据中。如果您正在设计API并想要 BooleanField 的默认后备广告素材,请使用自定义表单字段或小部件。

Changed in Django 1.10.1:

旧版本没有 CheckboxInput 的例外,这意味着如果是模型字段默认值,未选中的复选框将接收 True 的值。

Changed in Django 1.10.2:

添加 value_omitted_from_data() 方法。

save() 方法接受可选的 commit 关键字参数,它接受 TrueFalse。如果使用 commit=False 调用 save(),那么它将返回一个尚未保存到数据库的对象。在这种情况下,由您在结果模型实例上调用 save()。如果要在保存对象之前对其进行自定义处理,或者如果要使用其中一个专用 模型保存选项,这将非常有用。默认情况下,commitTrue

当您的模型与另一个模型具有多对多关系时,可以看到使用 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 从模型创建的 any 表单将不包括该字段。

注解

上述逻辑不包括在表单中的任何字段将不会由表单的 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) 的更多详情,请参阅 section on saving forms

覆盖默认字段

默认字段类型,如上面的 Field types 表中所述,是合理的默认值。如果您的模型中有 DateField,那么您可能希望在您的表单中将其表示为 DateField。但是 ModelForm 为您提供了为给定模型更改表单字段的灵活性。

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

例如,如果您希望 CharFieldAuthorname 属性由 <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)。

同样,如果要进一步自定义字段,您可以指定内部 Meta 类的 labelshelp_textserror_messages 属性。

例如,如果您要自定义 name 字段的所有面向用户的字符串的措辞:

from django.utils.translation import ugettext_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 以自定义由表单实例化的字段类型。

例如,如果您要对 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,
        }

最后,如果你想完全控制一个字段,包括它的类型,验证器,必需的等等 - 你可以通过声明性地指定字段,像在常规 Form 中一样。

如果要指定字段的验证器,可以通过声明性地定义字段并设置其 validators 参数来实现:

from django.forms import ModelForm, CharField
from myapp.models import Article

class ArticleForm(ModelForm):
    slug = CharField(validators=[validate_slug])

    class Meta:
        model = Article
        fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']
New in Django 1.9:

已添加 Meta.field_classes 属性。

注解

当您显式实例化这样的表单字段时,重要的是要了解 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",))

模型formets

class models.BaseModelFormSet

常规formets 一样,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 = 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="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() 生成formets。这意味着模型形式集只是一个知道如何与特定模型交互的基本形式集的扩展。

更改查询集

默认情况下,从模型创建表单集时,表单集将使用包括模型中所有对象(例如,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(BaseAuthorFormSet, self).__init__(*args, **kwargs)
        self.queryset = Author.objects.filter(name__startswith='O')

然后,将您的 BaseAuthorFormSet 类传递到工厂函数:

>>> AuthorFormSet = modelformset_factory(
...     Author, fields=('name', 'title'), formset=BaseAuthorFormSet)

如果要返回不包含 any 预先存在的模型实例的表单集,可以指定一个空的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 窗口小部件类。这与 widgets 字典在 ModelForm 的内部 Meta 类上的工作方式相同:

>>> 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__',则所有字段将被本地化。

提供初始值

与常规formets一样,通过在实例化 modelformset_factory() 返回的模型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() 方法设置。您可以在 Selecting the fields to use 中找到有关此限制的更多信息,这也适用于常规 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() 后,您的模型formset将有三个新属性,包含formset的更改:

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

限制可编辑对象的数量

与常规formsets一样,您可以使用 modelformset_factory()max_numextra 参数来限制显示的额外窗体数量。

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

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

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

在视图中使用模型formset

模型formets非常类似于formets。假设我们想要提供一个表单集来编辑 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() 将数据保存到数据库中。 (这在上面的描述中,在 将对象保存在formset中。)

覆盖 ModelFormSet 上的 clean()

ModelForms 一样,默认情况下 ModelFormSetclean() 方法将验证formset中没有项目违反您的模型(uniqueunique_togetherunique_for_date|month|year)的唯一约束。如果要覆盖 ModelFormSet 上的 clean() 方法并保持此验证,则必须调用父类的 clean 方法:

from django.forms import BaseModelFormSet

class MyModelFormSet(BaseModelFormSet):
    def clean(self):
        super(MyModelFormSet, self).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(MyModelFormSet, self).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 参数。

在模板中使用formset

在Django模板中有三种方式来渲染表单集。

首先,你可以让formset做大部分的工作:

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

第二,你可以手动渲染formset,但让表单处理自己:

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

当您自己手动呈现表单时,请确保呈现如上所示的管理表单。见 管理表单文档

第三,您可以手动呈现每个字段:

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

如果您选择使用此第三种方法,并且不使用 {% for %} 循环对字段进行迭代,则需要渲染主键字段。例如,如果您正在渲染模型的 nameage 字段:

<form method="post" action="">
    {{ 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,请确保它被渲染)。

内联formets

class models.BaseInlineFormSet

内联formets是模型formets上的一个小的抽象层。这些简化了通过外键处理相关对象的情况。假设你有这两个模型:

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)

注解

inlineformset_factory() 使用 modelformset_factory() 和标记 can_delete=True

覆盖 InlineFormSet 上的方法

当覆盖 InlineFormSet 上的方法时,您应该子类化 BaseInlineFormSet,而不是 BaseModelFormSet

例如,如果要覆盖 clean():

from django.forms import BaseInlineFormSet

class CustomInlineFormSet(BaseInlineFormSet):
    def clean(self):
        super(CustomInlineFormSet, self).clean()
        # example custom validation across forms in the formset
        for form in self.forms:
            # your custom formset validation
            ...

参见 覆盖 ModelFormSet 上的 clean()

然后,当您创建内联formset时,传递可选参数 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'))

在视图中使用内联formset

您可能需要提供一个视图,允许用户编辑模型的相关对象。这里是如何做到这一点:

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 一样。见上面的 Specifying widgets to use in the form with widgets