从模型创建表单

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 在表单中被表现为 CharFieldManyToManyField 则表现为 MultipleChoiceField 。以下是完整的转化清单:

模型字段 表单字段
AutoField 不呈现在表单中
BigAutoField 不呈现在表单中
BigIntegerField IntegerFieldmin_value 设置为-9223372036854775808,将 max_value 设置为9223372036854775807。
BooleanField BooleanField
CharField CharFieldmax_length 设置为模型字段的 max_length ,如果模型中设置了 null=True ,会将 empty_value 设置为 None
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 CharField 设置中 widget=forms.Textarea
TimeField TimeField
URLField URLField

如您所料, 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.full_clean() )在表单验证步骤中紧随表单的 clean() 方法被调用后触发。

Warning

Clean 过程会以各种方式去修改传递给 ModelForm 构造方法的模型实例。例如,模型上的所有日期字段都将转换为实际的日期对象。验证失败可能会使底层模型实例处于不一致状态,因此不推荐对其重用。

覆盖clean()方法

您可以重写模型表单上的 clean() 方法来提供额外的验证,方式和普通的表单一样。

访问模型对象对应的表单实例包含一个 instance 属性,让它可以访问对应的模型实例。

Warning

ModelForm.clean() 方法设置了一个标识符,使程序在 模型验证 这步去验证标记为 uniqueunique_togetherunique_for_date|month|year 的模型字段的唯一性。

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

与模型验证交互

作为验证过程的一部分, ModelForm 将调用模型上与表单字段对应的每个字段的 clean() 方法。如果您排除了一些模型字段,则验证将不会在这些字段上运行。更多有关字段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.",
            }
        }

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 来实现验证。如果表单验证不过,则会引发 ValueError —— 比如,如果 form.errors 返回 True

如果一个可选字段没有出现在表单的数据中,并且您给这个模型字段设置了 default ,那么对应的模型实例会使用这个值作为结果。此行为不适用于使用以下组件的字段: CheckboxInputCheckboxSelectMultiple 或者 SelectMultiple (或者所有其 value_omitted_from_data() 方法总是返回 False 的组件),因为未勾选的复选框和未选中的 <select multiple> 不会出现在HTML表单提交的数据中。如果您正在设计API并且希望使用这些组件之一的字段有默认回退行为,请使用自定义表单字段或组件。

save() 方法接受一个可选参数 commit ,它的值是 True 或者 False 。如果调用 save() 的时候使用 commit=False ,那么它会返回一个尚未保存到数据库的对象。在这种情况下,需要您自己在生成的模型实例上调用 save() 。如果要在保存对象之前对对象执行自定义操作,或者要使用其中一个专用的 模型保存选项 ,这很有用。 commit 的值默认为 True

另一个使用 commit=False 的作用,您可以在模型与另一个模型有多对多关系的时候看到。如果您的模型具有多对多关系,并且在保存表单时指定了 commit=False ,Django无法立即保存多对多关系的表单数据。这是因为实例的多对多数据只有实例在数据库中存在时才能保存。

要解决这个问题,Django会在您每次使用 commit=False 保存表单时,向 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 与普通的表单工作方式一样。例如,用 is_valid() 方法来检查合法性,用 is_multipart() 方法来确定表单是否需要multipart文件上传(之后是否必须将 request.FILES 传递给表单),等等。更多相关信息,请参阅 Binding uploaded files to a form

选择要使用的字段

强烈建议您使用 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 模型有三个字段 nametitlebirth_date ,上例的结果是字段 namebirth_date 会呈现在表单中。

不管使用哪一种,字段会按模型中定义的顺序在表单中出现, ManyToManyField 会排在最后。

另外,Django有个规则:如果您在模型字段中定义了 editable=False*任何*使用 ModelForm 给该模型创建的表单都不会包含这个字段。

Note

任何没在上面逻辑中包含的表单字段都会不被表单的 save() 方法处理。另外,如果手动将排除的字段添加回表单,它们也不会被模型实例初始化。

Django会阻止任何尝试保存不完整模型的行为,所以如果模型不允许缺省的字段为空,并且没有为该字段提供缺省值,那么任何尝试用这种字段的 ModelFormsave() 方法都会失败。为了避免这种情况,您必须使用初始值实例化您模型中缺省但又必填的字段:

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 )。

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

例如,如果您想对 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 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']

Note

当您像这样显式地实例化了一个表单字段,理解 ModelForm 和普通 Form 的关系很重要。

ModelForm 是一个可以自动生成特定字段的 Form 。哪些字段可以自动生成取决于 Meta 类的内容,以及是否已经被明确定义过。总的来说 ModelForm 仅会 自动生成表单中 缺失 的字段,或者说,没被明确定义的字段。

声明定义的字段会保持原样,因此,任何对 Meta 属性(例如 widgetslabelshelp_texts 或者 error_messages)的自定义设置都会被忽略;它们仅适用于自动生成的字段。

同样,显式定义的字段不会从对应的模型中获取他们的属性(比如 max_length 或者 required)。如果要保持模型中指定的行为,则必须在声明表单字段时显式设置相关参数。

例如,假设 Article 模型像下面这样:

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

且您希望对 headline 进行自定义验证,在保留指定的 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 字段会有一些额外的验证和cleaning。

如果要更改 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提供了几个增强的formset类,可以很方便地配合Django模型使用。让我们重用下上面的 Author 模型:

>>> from django.forms import modelformset_factory
>>> from myapp.models import Author
>>> AuthorFormSet = modelformset_factory(Author, fields=('name', 'title'))

使用 fields 参数限制formset仅使用给定的字段。或者,您可以使用排除法,指定排除哪些字段:

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

Note

modelformset_factory() 使用 formset_factory() 来生成表单集。这意味着模型formset只是一个知道如何与指定模型交互的普通formset的扩展。

更改查询集

默认情况下,当您创建一个模型ormset时,formset将使用一个包含模型中所有对象(例如 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)

如果您想返回一个不包含 任何 已存在模型实例的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
        ...

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

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

提供初始值

As with regular formsets, it's possible to specify initial data for forms in the formset by specifying an initial parameter when instantiating the model formset class returned by modelformset_factory(). However, with model formsets, the initial values only apply to extra forms, those that aren't attached to an existing model instance. If the extra forms with initial data aren't changed by the user, they won't be validated or saved.

Saving objects in the formset

As with a ModelForm, you can save the data as a model object. This is done with the formset's save() method:

# Create a formset instance with POST data.
>>> formset = AuthorFormSet(request.POST)

# Assuming all is valid, save the data.
>>> instances = formset.save()

The save() method returns the instances that have been saved to the database. If a given instance's data didn't change in the bound data, the instance won't be saved to the database and won't be included in the return value (instances, in the above example).

When fields are missing from the form (for example because they have been excluded), these fields will not be set by the save() method. You can find more information about this restriction, which also holds for regular ModelForms, in Selecting the fields to use.

Pass commit=False to return the unsaved model instances:

# don't save to the database
>>> instances = formset.save(commit=False)
>>> for instance in instances:
...     # do something with instance
...     instance.save()

This gives you the ability to attach data to the instances before saving them to the database. If your formset contains a ManyToManyField, you'll also need to call formset.save_m2m() to ensure the many-to-many relationships are saved properly.

After calling save(), your model formset will have three new attributes containing the formset's changes:

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

Limiting the number of editable objects

As with regular formsets, you can use the max_num and extra parameters to modelformset_factory() to limit the number of extra forms displayed.

max_num does not prevent existing objects from being displayed:

>>> 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']

Also, extra=0 doesn't prevent creation of new model instances as you can add additional forms with JavaScript or just send additional POST data. Formsets don't yet provide functionality for an "edit only" view that prevents creation of new instances.

If the value of max_num is greater than the number of existing related objects, up to extra additional blank forms will be added to the formset, so long as the total number of forms does not exceed 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)张表单,其实这相当于没有限制。

Using a model formset in a view

Model formsets are very similar to formsets. Let's say we want to present a formset to edit Author model instances:

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

As you can see, the view logic of a model formset isn't drastically different than that of a "normal" formset. The only difference is that we call formset.save() to save the data into the database. (This was described above, in Saving objects in the formset.)

Overriding clean() on a ModelFormSet

Just like with ModelForms, by default the clean() method of a ModelFormSet will validate that none of the items in the formset violate the unique constraints on your model (either unique, unique_together or unique_for_date|month|year). If you want to override the clean() method on a ModelFormSet and maintain this validation, you must call the parent class's clean method:

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

Also note that by the time you reach this step, individual model instances have already been created for each Form. Modifying a value in form.cleaned_data is not sufficient to affect the saved value. If you wish to modify a value in ModelFormSet.clean() you must modify 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

Using a custom queryset

As stated earlier, you can override the default queryset used by the model formset:

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

Note that we pass the queryset argument in both the POST and GET cases in this example.

Using the formset in the template

There are three ways to render a formset in a Django template.

First, you can let the formset do most of the work:

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

Second, you can manually render the formset, but let the form deal with itself:

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

When you manually render the forms yourself, be sure to render the management form as shown above. See the management form documentation.

Third, you can manually render each field:

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

If you opt to use this third method and you don't iterate over the fields with a {% for %} loop, you'll need to render the primary key field. For example, if you were rendering the name and age fields of a model:

<form method="post">
    {{ formset.management_form }}
    {% for form in formset %}
        {{ form.id }}
        <ul>
            <li>{{ form.name }}</li>
            <li>{{ form.age }}</li>
        </ul>
    {% endfor %}
</form>

Notice how we need to explicitly render {{ form.id }}. This ensures that the model formset, in the POST case, will work correctly. (This example assumes a primary key named id. If you've explicitly defined your own primary key that isn't called id, make sure it gets rendered.)

Inline formsets

class models.BaseInlineFormSet

Inline formsets is a small abstraction layer on top of model formsets. These simplify the case of working with related objects via a foreign key. Suppose you have these two models:

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)

If you want to create a formset that allows you to edit books belonging to a particular author, you could do this:

>>> 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's prefix is 'book_set' (<model name>_set ). If Book's ForeignKey to Author has a related_name, that's used instead.

Note

inlineformset_factory() uses modelformset_factory() and marks can_delete=True.

Overriding methods on an InlineFormSet

When overriding methods on InlineFormSet, you should subclass BaseInlineFormSet rather than BaseModelFormSet.

For example, if you want to override 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
            ...

See also Overriding clean() on a ModelFormSet.

Then when you create your inline formset, pass in the optional argument 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)

More than one foreign key to the same model

If your model contains more than one foreign key to the same model, you'll need to resolve the ambiguity manually using fk_name. For example, consider the following model:

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()

To resolve this, you can use fk_name to inlineformset_factory():

>>> FriendshipFormSet = inlineformset_factory(Friend, Friendship, fk_name='from_friend',
...     fields=('to_friend', 'length_in_months'))

Using an inline formset in a view

You may want to provide a view that allows a user to edit the related objects of a model. Here's how you can do that:

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

Notice how we pass instance in both the POST and GET cases.

Specifying widgets to use in the inline form

inlineformset_factory uses modelformset_factory and passes most of its arguments to modelformset_factory. This means you can use the widgets parameter in much the same way as passing it to modelformset_factory. See Specifying widgets to use in the form with widgets above.

Back to Top