模型表单

优质
小牛编辑
142浏览
2023-12-01

ModelForm

class ModelForm

如果你正在构建一个数据库驱动的应用,那么你应该会有与Django 的模型紧密映射的表单。举个例子,你也许会有个BlogComment 模型,并且你还想创建一个表单让大家提交评论到这个模型中。 在这种情况下,在表单中定义字段将是冗余的,因为你已经在模型中定义了字段。

基于这个原因,Django 提供一个辅助类来让你可以从Django 的模型创建表单

例如:

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

字段类型

生成的表单类中将具有和指定的模型字段对应的表单字段,顺序为fields 属性中指定的顺序。

每个模型字段有一个对应的默认表单字段。比如,模型中的CharField 表现成表单中的CharField。模型中的ManyToManyField 字段会表现成MultipleChoiceField 字段。下面是一个完整的列表:

Model fieldForm field
AutoFieldNot represented in the form
BigIntegerFieldIntegerField with min_value set to -9223372036854775808 and max_value set to 9223372036854775807.
BooleanFieldBooleanField
CharFieldCharField with max_length set to the model field’s max_length
CommaSeparatedIntegerFieldCharField
DateFieldDateField
DateTimeFieldDateTimeField
DecimalFieldDecimalField
EmailFieldEmailField
FileFieldFileField
FilePathFieldFilePathField
FloatFieldFloatField
ForeignKeyModelChoiceField (see below)
ImageFieldImageField
IntegerFieldIntegerField
IPAddressFieldIPAddressField
GenericIPAddressFieldGenericIPAddressField
ManyToManyFieldModelMultipleChoiceField (see below)
NullBooleanFieldNullBooleanField
PositiveIntegerFieldIntegerField
PositiveSmallIntegerFieldIntegerField
SlugFieldSlugField
SmallIntegerFieldIntegerField
TextFieldCharField with widget=forms.Textarea
TimeFieldTimeField
URLFieldURLField

可能如你所料,ForeignKeyManyToManyField 字段类型属于特殊情况:

  • ForeignKey 表示成django.forms.ModelChoiceField,它是一个ChoiceField,其选项是模型的查询集
  • ManyToManyField 表示成django.forms.ModelMultipleChoiceField,它是一个MultipleChoiceField,其选项是模型的查询集

此外,生成的每个表单字段都有以下属性集:

  • 如果模型字段设置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):              # __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())

模型表单的验证

验证模型表单主要有两步:

  1. 验证表单
  2. 验证模型实例

与普通的表单验证类型类似,模型表单的验证在调用is_valid() 或访问errors 属性时隐式调用,或者通过full_clean() 显式调用,尽管在实际应用中你将很少使用后一种方法。

模型的验证(Model.full_clean())在表单验证这一步的内部触发,紧跟在表单的clean() 方法调用之后。

警告

Clean 过程会以各种方式修改传递给模型表单构造函数的模型实例。例如,模型的日期字段将转换成日期对象。验证失败可能导致模型实例处于不一致的状态,所以不建议重新使用它。

重写clean() 方法

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

模型表单实例包含一个instance 属性,表示与它绑定的模型实例。

警告

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

如果你需要覆盖clean() 方法并维持这个验证行为,你必须调用父类的clean() 方法。

与模型验证的交互

作为验证过程的一部分,模型表单将调用与表单字段对应的每个模型字段的clean() 方法。如果你已经排除某些模型字段,这些字段不会运行验证。关于字段clean 和验证是如何工作的,参见表单字段的文档。

模型的clean() 方法在任何唯一性检查之前调用。关于模型clean() 钩子的更多信息,参见验证对象

模型error_messages 的注意事项

表单字段级别或表单级别的错误信息永远比模型字段级别的错误信息优先。

模型字段的错误信息只用于模型验证步骤引发ValidationError 的时候,且不会有对应的表单级别的错误信息。

New in Django 1.7.

你可以根据模型验证引发的NON_FIELD_ERRORS 覆盖错误信息,方法是添加 NON_FIELD_ERRORS 键到模型表单内联Meta 类的error_messages 字典:

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

每个模型表单还具有一个save() 方法。这个方法根据表单绑定的数据创建并保存数据库对象。模型表单的子类可以用关键字参数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.errorsTrue

save() 接受一个可选的commit 关键字参数,其值为TrueFalse。如果save()commit=False,那么它将返回一个还没有保存到数据库的对象。这种情况下,你需要调用返回的模型实例的save()。 如果你想在保存之前自定义一些处理,或者你想使用特定的模型保存选项,可以这样使用。commit 默认为True

使用commit=False 的另外一个副作用是在模型具有多对多关系的时候。如果模型具有多对多关系而且当你保存表单时指定commit=False,Django 不会立即为多对多关系保存表单数据。这是因为只有实例在数据库中存在时才可以保存实例的多对多数据。

为了解决这个问题,每当你使用commit=False 保存表单时,Django 将添加一个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_m2m() 只在你使用save(commit=False) 时才需要。当你直接使用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() 方法之外,模型表单与其它表单的工作方式完全一样。例如,is_valid()用于检查合法性,is_multipart() 方法用于决定表单是否需要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 从该模型创建的任何表单都不会包含该字段。

Changed in Django 1.8:

在旧的版本中,同时省略fieldsexclude 字段将导致模型的所有字段出现在表单中。现在这样做将引发一个ImproperlyConfigured 异常。

不会被上述逻辑包含进表单中的字段将不会被表单的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 还提供更多的灵活性,让你可以改变给定的模型字段对应的表单字段的类型和Widget。

使用内部类Metawidgets 属性可以指定一个字段的自定义Widget。它是映射字段名到Widget 类或实例的一个字典。

例如,Authorname 属性为CharField,如果你希望它表示成一个<textarea> 而不是默认的<input type="text">,你可以覆盖字段默认的Widget:

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

不管是Widget 实例(Textarea(...))还是Widget 类(Textarea),widgets 字典都可以接收。

类似地,如果你希望进一步自定义字段,你可以指定内部类Metalabelshelp_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."),
            },
        }

最后,如果你希望完全控制字段 —— 包括它的类型、验证器等等,你可以像在普通的表单那样显式指定字段。

例如,如果你想为slug 字段使用MySlugFormField ,可以像下面这样:

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

class ArticleForm(ModelForm):
    slug = MySlugFormField()

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

如果想要指定字段的验证器,可以显式定义字段并设置它的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']

当你像这样显式实例化表单字段时,需要理解ModelForm 和普通的Form 的关系是怎样的。

ModelForm就是可以自动生产相应字段的Form.自动生成哪些字段取决于Meta 类的fields属性和在该ModelForm中显示声明的字段。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来扩展和重用他们。当你的form是通过models生成的,而且需要在父类的基础上声明额外的field和method,这种继承是方便的。例如,使用以前的ArticleForm 类:

>>> class EnhancedArticleForm(ArticleForm):
...     def clean_pub_date(self):
...         ...

以上创建了一个与 ArticleForm非常类似的form,除了一些额外的验证和pub_date 的cleaning

你也可以在子类中继承父类的内部类 Meta来重写它的属性列表,比如 Meta.fields 或者Meta.excludes :

>>> class RestrictedArticleForm(EnhancedArticleForm):
...     class Meta(ArticleForm.Meta):
...         exclude = ('body',)

上例从父类EnhancedArticleForm继承后增加了额外的方法,并修改了 ArticleForm.Meta 排除了一个字段

当然,有一些注意事项

  • 应用正常的Python名称解析规则。如果你有多个基类声明一个Meta内部类,只会使用第一个。这意味着孩子的Meta(如果存在),否则第一个父母的Meta等。

Changed in Django 1.7.

  • 它可以同时继承FormModelForm,但是,必须确保ModelForm首先出现在MRO中。这是因为这些类依赖于不同的元类,而一个类只能有一个元类。

New in Django 1.7.

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

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

提供初始值

作为一个有参数的表单, 在实例化一个表单时可以通过指定initial字段来指定表单中数据的初始值. 这种方式指定的初始值将会同时替换掉表单中的字段和值. 例如:

>>> article = Article.objects.get(pk=1)
>>> article.headline
'My headline'
>>> form = ArticleForm(initial={'headline': 'Initial headline'}, instance=article)
>>> form['headline'].value()
'Initial headline'

模型表单的factory函数

你可以用单独的函数 modelform_factory() 来代替使用类定义来从模型直接创建表单。这在不需要很多自定义的情况下应该是更方便的。

>>> from django.forms.models import modelform_factory
>>> from myapp.models import Book
>>> BookForm = modelform_factory(Book, fields=("author", "title"))

这个函数还能对已有的表单类做简单的修改,比如,对给出的字段指定 widgets :

>>> 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提供的几个有力的表单集类来简化模型操作。. 让我们继续使用上面的Author模型:

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

使用 fields限定表单集仅可以使用给出的字段,或者使用排除法,指定哪些字段被不被使用。

>>> AuthorFormSet = modelformset_factory(Author, exclude=('birth_date',))

Changed in Django 1.8:

在旧版本中,同时省略fieldsexclude 的结果是表单集使用模型的所有字段。现在这么做将引发ImproperlyConfigured 异常。

下面将创建一个与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="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() 生成表单集,这意味着模型表单集仅仅是扩展基本表单集,使其能处理模型的信息。

更改查询集

默认的, 如果你使用model生成formset,formset会使用一个包含模型全部对象的queryset(例如:Author.objects.all()). 你可以使用queryset参数重写这一行为:

>>> formset = AuthorFormSet(queryset=Author.objects.filter(name__startswith='O'))

或者,你可以创建子类设置 self.queryset in __init__:

from django.forms.models 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 类传给modelformset_factory函数:

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

如果想返回不包含任何已存在模型实例的表单集,可以指定一个空的查询集(QuerySet):

>>> AuthorFormSet(queryset=Author.objects.none())

更改form

默认情况下,当你使用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列出字段的widget类。这与 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__',将本地化所有字段。

提供初始化数据

与普通表单集一样,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()方法设置。您可以在选择要使用的字段中找到有关此限制的更多信息,这也适用于常规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

操作表单对像的数量限制

与普通表单集一样,你可以用在modelformset_factory()中使用 max_numextra 参数,来控制额外表单的显示数量。

max_num 不会限制已经存在的表单对像的显示:

>>> Author.objects.order_by('name')
[<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']

如果 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 值为f None (缺省)设置一个较高的限制可显示1000个表单。实际上相当于没有限制。

在视图中使用模型表单集

模型表单集与表单集十分类似,假设我们想要提供一个表单集来编辑Author模型实例:

from django.forms.models import modelformset_factory
from django.shortcuts import render_to_response
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_to_response("manage_authors.html", {
        "formset": formset,
    })

可以看到,模型表单集的视图逻辑与“正常”表单集的视图逻辑没有显着不同。唯一的区别是我们调用formset.save()将数据保存到数据库中。(如上所述,在Saving objects in the formset中的对象)。

ModelFormSet上覆盖clean()

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

from django.forms.models 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.models 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

使用自定义queryset

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

from django.forms.models import modelformset_factory
from django.shortcuts import render_to_response
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_to_response("manage_authors.html", {
        "formset": formset,
    })

请注意,我们在此示例中的POSTGET中传递queryset参数。

在模板中使用表单集

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

第一种方式,你可以让表单集完成大部分的工作

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

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

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

当您自己手动呈现表单时,请确保呈现如上所示的管理表单。请参阅management form documentation

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

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

如果您选择使用此第三种方法,并且不对{% for %} t0&gt; loop,你需要渲染主键字段。例如,如果您要渲染模型的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)
    title = models.CharField(max_length=100)

如果要创建允许您编辑属于特定作者的图书的表单集,您可以执行以下操作:

>>> from django.forms.models 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

也可以看看

Manually rendered can_delete and can_order

覆盖InlineFormSet上的方法

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

例如,如果要覆盖clean()

from django.forms.models 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
            ...

另请参见Overriding clean() on a ModelFormSet上的clean()。

然后,在创建内联表单集时,传递可选参数formset

>>> from django.forms.models 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, related_name='from_friends')
    to_friend = models.ForeignKey(Friend, 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_to_response("manage_books.html", {
        "formset": formset,
    })

注意我们如何在POSTGET例中传递instance

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

inlineformset_factory使用modelformset_factory并将其大部分参数传递给modelformset_factory。这意味着您可以使用widgets参数,将其传递到modelformset_factory。请参阅上面的指定要在窗体中使用的窗口小部件的窗口小部件。