从模型创建表单

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

模型字段 表单字段
AutoField 不呈现在表单中
BigAutoField 不呈现在表单中
BigIntegerField IntegerFieldmin_value 设置为-9223372036854775808,将 max_value 设置为9223372036854775807。
BinaryField CharField ,如果在模型字段上的 editable 被设置为 True ,则不在表单中显示。
BooleanField BooleanField, 或 NullBooleanField (如果 null=True )。
CharField CharFieldmax_length 设置为模型字段的 max_length ,如果模型中设置了 null=True ,会将 empty_value 设置为 None
DateField DateField
DateTimeField DateTimeField
DecimalField DecimalField
DurationField DurationField
EmailField EmailField
FileField FileField
FilePathField FilePathField
FloatField FloatField
ForeignKey ModelChoiceField (见下文)
ImageField ImageField
IntegerField IntegerField
IPAddressField IPAddressField
GenericIPAddressField GenericIPAddressField
JSONField JSONField
ManyToManyField ModelMultipleChoiceField (见下文)
NullBooleanField NullBooleanField
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.full_clean() )在表单验证步骤中紧随表单的 clean() 方法被调用后触发。

警告

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

覆盖clean()方法

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

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

警告

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 传递给表单),等等。更多相关信息,请参阅 将上传的文件绑定到表单中

选择要使用的字段

强烈建议您使用 fields 属性来显式设置所有应在表单中编辑的字段。如果不这样做,当一张表单不慎允许用户设置某些字段,尤其是在将新字段添加到模型中时,很容易导致安全问题。根据表单渲染方式的不同,甚至可能不会在网页上显示问题。

The alternative approach would be to include all fields automatically, or remove only some. This fundamental approach is known to be much less secure and has led to serious exploits on major websites (e.g. 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 给该模型创建的表单都不会包含这个字段。

注解

任何没在上面逻辑中包含的表单字段都会不被表单的 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 字典接受任何 widget 实例(例如 Textarea(...) )或类(例如 Textarea)。注意,对于具有非空 choices 属性的模型字段,widgets 字典会被忽略。在这个例子里,你必须覆盖表单字段来使用不同的 widget 。

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

例如您想自定义 name 字段中所有面向用户的字符文本:

from django.utils.translation import gettext_lazy as _

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ('name', 'title', 'birth_date')
        labels = {
            'name': _('Writer'),
        }
        help_texts = {
            'name': _('Some useful help text.'),
        }
        error_messages = {
            'name': {
                'max_length': _("This writer's name is too long."),
            },
        }

您还可以指定 field_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']

注解

当您像这样显式地实例化了一个表单字段,理解 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

regular formsets 一样,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-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">
<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() 来生成表单集。这意味着模型formset是一个知道如何与指定模型交互的普通formset的扩展。

注解

当使用多表继承( multi-table inheritance ),通过 formset factory 生成的表单将包含父链接字段(默认是 2_ptr )而不是 id 字段。

更改查询集

默认情况下,当您创建一个模型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__' ,则所有字段都将被本地化。

提供初始值

与常规表单集(formset)一样,在实例化 modelformset_factory() 返回的模型表单集类时,可以通过指定初始参数来指定表单集中表单的初始数据。然而,对于模型表单集,初始值只适用于额外的表单,不会附加到已存在的模型实例。如果 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() 方法设置。在选择使用字段里( 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() 之后,你的模型表单集将会有三个包含表单集更改的新属性:

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

限制可编辑对象的数量

和普通表单集一样,你可以使用 max_numextra 参数来 modelformset_factory() 来让 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 的值大于现有关联对象的数量,只要总表单数不超过 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)张表单,其实这相当于没有限制。

在视图中使用模型表单集

模型表单集与表单集非常相似。假设我们想要一个表单集来编辑 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() 方法将验证表单集中没有任何项目违反模型的唯一约束( unique, unique_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.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})

注意我们在这个例子里的 POST and GET 情况下传递了 queryset 参数。

在模板中使用表单集

有三种办法在 Django 模板中渲染表单集。

首先,你可以让表单集完成大部分工作:

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

其次,你可以手工渲染表单集,但需要让表单自己处理:

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

当你手工渲染表单时,确保渲染了如上所述的管理表单。请查看文档的“管理”部分 (management form documentation

第三,你可以手工渲染每个字段:

<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's prefix is 'book_set' (<model name>_set ). 如果 Book 的 指向 AuthorForeignKey 有一个 related_name ,则使用它。

注解

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

参见

手动渲染 can_delete 和 can_order 。( Manually rendered can_delete and can_order

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

请注意我们如何将 instance 传递给 POSTGET 的。

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

inlineformset_factory 使用 modelformset_factory,并传递大部分参数给 modelformset_factory 。这意味着你可以使用 widgets 参数,就像将它传递给 modelformset_factory 一样。请查看 Specifying widgets to use in the form with widgets

Back to Top