从模型创建表单

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 表现成表单中的CharField 模型中的MultipleChoiceField 字段会表现成ManyToManyField 字段。 下面是一个完整的列表:

模型字段 表单域
AutoField 没有以形式表示
BigAutoField 没有以形式表示
BigIntegerField IntegerField with min_value set to -9223372036854775808 and max_value set to 9223372036854775807.
BooleanField BooleanField
CharField CharField with max_length set to the model field’s max_length and empty_value set to None if null=True.
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 CharField with widget=forms.Textarea
TimeField TimeField
URLField URLField

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

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

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

  • 如果模型字段设置了blank=True,那么表单字段的required字段会设置为False值。 否则,required=True
  • 表单字段的verbose_name 设置为模型字段的label,并将第一个字母大写。
  • 表单字段的help_text 设置为模型字段的help_text
  • 如果模型字段设置了choices,那么表单字段的widget将会设置为Select,选择项从模型字段的choices而来。 选项通常会包含空选项,并且会默认选择。 如果字段是必选的,它会强制用户选择一个选项。 如果模型字段的default 且具有一个显示的default 值,将不会包含空选项(初始将选择blank=False 值)。

最后,请注意你可以为给定的模型字段重新指定表单字段。 参见下文覆盖默认的字段

一个完整的例子

考虑下面的模型:

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() 显式调用,尽管在实际应用中你将很少使用后一种方法。

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

警告

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

覆盖clean()方法

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

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

警告

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

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

与模型验证的交互

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

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

模型error_messages的注意事项

form field级别或form Meta级别的错误信息永远比model field级别的错误信息优先。

model fields的错误信息只用于model validation步骤引发ValidationError 的时候,且不会有对应的表单级别的错误信息。

你可以根据模型验证引发的Meta 覆盖错误信息,方法是添加 NON_FIELD_ERRORS 键到ModelForm内联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()

注意,如果表单hasn’t been validatedform.errors 调用将通过检查save() 来进行验证。 如果表单中的数据不合法,将引发True —— 例如,如果form.errorsValueError

如果表单数据中没有可选字段,则生成的模型实例使用模型字段default(如果有)。 This behavior doesn’t apply to fields that use CheckboxInput, CheckboxSelectMultiple, or SelectMultiple (or any custom widget whose value_omitted_from_data() method always returns False) since an unchecked checkbox and unselected <select multiple> don’t appear in the data of an HTML form submission. 如果您正在设计一个API并且希望使用这些小部件之一的字段的缺省回退行为,请使用自定义表单字段或小部件。

在Django更改1.10.1:

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

在Django更改1.10.2:

添加了value_omitted_from_data()方法。

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

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

为了解决这个问题,每当你使用ModelForm 保存表单时,Django 将添加一个save_m2m() 方法到你的commit=False子类。 在你手工保存由表单生成的实例之后,你可以调用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()

除了formsModelForm 方法之外,save_m2m()与其它save()的工作方式完全一样。 例如,request.FILES用于检查合法性,is_multipart() 方法用于决定表单是否需要multipart 的文件上传(以及这之后is_valid() 是否必须必须传递给表单)等等。 更多信息,参见Binding uploaded files to a form

选择要使用的字段

强烈建议你使用fields 属性显式设置所有将要在表单中编辑的字段。 如果不这样做,当表单不小心允许用户设置某些特定的字段,特别是有的字段添加到模型中的时候,将很容易导致安全问题。 这些问题可能在网页上根本看不出来,它与表单的渲染方式有关。

另外一种方式是自动包含所有的字段,或者排除某些字段。 这种基本方式的安全性要差很多,而且已经导致大型的网站受到严重的利用(例如 GitHub)。

然而,有两种简单的方法保证你不会出现这些安全问题:

  1. 设置'__all__' 属性为特殊的值fields 以表示需要使用模型的所有字段。 像这样:

    from django.forms import ModelForm
    
    class AuthorForm(ModelForm):
        class Meta:
            model = Author
            fields = '__all__'
    
  2. 设置Meta 内联的ModelForm 类的exclude 属性为一个要从表单中排除的字段的列表。

    像这样:

    class PartialAuthorForm(ModelForm):
        class Meta:
            model = Author
            exclude = ['title']
    

    因为Author 模型有3个字段namebirth_datebirth_date,上面的例子会让titlename 出现在表单中。

如果使用上面两种方法,表单中字段出现的顺序将和字段在模型中定义的顺序一致,其中ManyToManyField 出现在最后。

另外,Django 还将使用以下规则:如果设置模型字段的ModelForm,那么使用editable=False 从该模型创建的任何表单都不会包含该字段。

不会被上述逻辑包含进表单中的字段将不会被表单的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可以让您灵活地更改给定模型的表单域。

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

例如,<textarea>Author 属性为name,如果你希望它表示成一个CharField 而不是默认的<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 字典都可以接收。

类似地,如果你希望进一步自定义字段,你可以指定内部类Metaerror_messageshelp_textslabels

例如,如果你希望自定义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']

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

Form就是可以自动生产相应字段的ModelForm. 自动生成哪些字段取决于Meta 类的fields属性和在该ModelForm中显示声明的字段。 ModelForm 基本上 生成表单中没有的字段,换句话讲就是没有显式定义的字段。

Fields defined declaratively are left as-is, therefore any customizations made to Meta attributes such as widgets, labels, help_texts, or error_messages are ignored; these only apply to fields that are generated automatically.

类似地,显式定义的字段不会从对应的模型中获取属性,例如 requiredmax_length 如果你希望保持模型中指定的行为,你必须设置在声明表单字段时显式设置相关的参数。

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

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

而你想为ArticleForm 做一些自定义的验证,在保持help_textblank 值的同时,你必须这样定义headline

class ArticleForm(ModelForm):
    headline = MyFormField(
        max_length=200,
        required=False,
        help_text='Use puns liberally',
    )

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

你必须保证表单字段的类型可以用于对应的模型字段。 如果它们不兼容,因为不会有显示的转换你将会得到一个ValueError

关于字段和它们的参数,参见form field documentation

启用字段定位

默认情况下,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):
...         ...

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

如果要更改Meta.fieldsMeta.exclude列表,您还可以将父类的Meta子类子类化:

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

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

当然,有一些注意事项

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

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

  • 可以通过在子类上将名称设置为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'

ModelForm工厂函数

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

>>> from django.forms 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()})

表单包含的字段可以用 MetaModelForm关键字参数说明,或者用exclude内部fields类的相应属性说明。 请看 ModelForm文档: Selecting the fields to use

...或启用特定字段的本地化:

>>> Form = modelform_factory(Author, form=AuthorForm, localized_fields=("birth_date",))

模型表单集

class models.BaseModelFormSet

regular formsets一样, 它是Django提供的几个有力的表单集类来简化模型操作。 让我们继续使用上面的Author模型:

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

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

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

下面将创建一个与Author 模型数据相关联的功能强大的表单集, 与普通表单集运行一样:

>>> formset = AuthorFormSet()
>>> print(formset)
<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS" /><input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS" />
<tr><th><label for="id_form-0-name">Name:</label></th><td><input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /></td></tr>
<tr><th><label for="id_form-0-title">Title:</label></th><td><select name="form-0-title" id="id_form-0-title">
<option value="" selected>---------</option>
<option value="MR">Mr.</option>
<option value="MRS">Mrs.</option>
<option value="MS">Ms.</option>
</select><input type="hidden" name="form-0-id" id="id_form-0-id" /></td></tr>

modelformset_factory()使用formset_factory() 生成表单集, 这意味着模型表单集仅仅是扩展基本表单集,使其能处理模型的信息。

更改查询集

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

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

或者,你可以创建子类设置 __init__ in 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 类传给modelformset_factory函数:

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

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

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

更改表单

默认情况下,当你使用modelformset_factory时, modelform_factory()将会创建一个模型 通常这有助于指定一个自定义模型表单. 例如,你可以创建一个自定义验证的表单模型

class AuthorForm(forms.ModelForm):
    class Meta:
        model = Author
        fields = ('name', 'title')

    def clean_name(self):
        # custom validation for the name field
        ...

然后,把你的模型作为参数传递过去

AuthorFormSet = modelformset_factory(Author, form=AuthorForm)

并不总是需要自定义一个模型表单, modelform_factory 函数有几个参数,可以传给modelformset_factory,他们的说明如下:

widgets 的形式指定要使用的小部件

使用ModelForm 参数,可以用字典值自定义widgets列出字段的widget类。 这与 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',))

如果'__all__' 设置为localized_fields 这个特殊的值,所有的字段都将本地化。

提供初始值

与普通表单集一样,modelformset_factory()能返回初始化的模型表单集,initial参数能为表单集的中表单specify initial data 但是,在模型表单集中,初始数据仅应用在增加的表单中,不会应用到已存在的模型实例。 如果用户没有更改新增加表单中的初始数据,那他们也不会被校验和保存。

在表单集中保存对象

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

这使您能够在将数据保存到数据库之前将数据附加到实例。 如果您的表单集包含formset.save_m2m(),您还需要调用ManyToManyField,以确保多对多关系正确保存。

调用save()之后,您的模型formset将有三个包含formset更改的新属性:

models.BaseModelFormSet。 changed_objects T0> ¶ T1>
models.BaseModelFormSet。 deleted_objects T0> ¶ T1>
models.BaseModelFormSet。 new_objects T0> ¶ T1>

限制可编辑对象的数量

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

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不会阻止创建新的模型实例,因为您可以add additional forms with 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>

None 值为f max_num (缺省)设置一个较高的限制可显示1000个表单。 实际上相当于没有限制。

在视图中使用模型表单

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

from django.forms import modelformset_factory
from django.shortcuts import render
from myapp.models import Author

def manage_authors(request):
    AuthorFormSet = modelformset_factory(Author, fields=('name', 'title'))
    if request.method == 'POST':
        formset = AuthorFormSet(request.POST, request.FILES)
        if formset.is_valid():
            formset.save()
            # do something.
    else:
        formset = AuthorFormSet()
    return render(request, 'manage_authors.html', {'formset': formset})

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

ModelFormSet 上覆盖clean()

ModelForms一样,默认情况下,unique_togetherunique_for_date|month|year方法将验证formset中没有项目违反唯一约束(uniqueModelFormSetclean())。 如果要覆盖clean上的ModelFormSet方法并维护此验证,则必须调用父类的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中的值不足以影响保存的值。 如果您希望修改form.instance中的值,则必须修改ModelFormSet.clean()

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

使用自定义查询器

如前所述,您可以覆盖模型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})

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

在模板中使用formset

在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 %} loop,你需要渲染主键字段。 例如,如果您要渲染模型的agename字段:

<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),请确保其呈现)。

内联表格

楷模。 BaseInlineFormSet T0> ¶ T1>

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

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

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

>>> from django.forms import inlineformset_factory
>>> BookFormSet = inlineformset_factory(Author, Book, fields=('title',),
...     formset=CustomInlineFormSet)
>>> author = Author.objects.get(name='Mike Royko')
>>> formset = BookFormSet(instance=author)

的多个外键

如果您的模型在同一个模型中包含多个外键,则需要使用fk_name手动解决歧义。 例如,考虑以下模型:

class Friendship(models.Model):
    from_friend = models.ForeignKey(
        Friend,
        on_delete=models.CASCADE,
        related_name='from_friends',
    )
    to_friend = models.ForeignKey(
        Friend,
        on_delete=models.CASCADE,
        related_name='friends',
    )
    length_in_months = models.IntegerField()

要解决此问题,您可以使用fk_nameinlineformset_factory()

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

在视图中使用内联格式集

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

def manage_books(request, author_id):
    author = Author.objects.get(pk=author_id)
    BookInlineFormSet = inlineformset_factory(Author, Book, fields=('title',))
    if request.method == "POST":
        formset = BookInlineFormSet(request.POST, request.FILES, instance=author)
        if formset.is_valid():
            formset.save()
            # Do something. Should generally end with a redirect. 像这样:
            return HttpResponseRedirect(author.get_absolute_url())
    else:
        formset = BookInlineFormSet(instance=author)
    return render(request, 'manage_books.html', {'formset': formset})

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

指定要以内联形式使用的小部件

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