如果你正在构建一个数据库驱动的应用,那么你应该会有与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 field | Form field |
---|---|
AutoField | Not represented in the form |
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 |
CommaSeparatedIntegerField | CharField |
DateField | DateField |
DateTimeField | DateTimeField |
DecimalField | DecimalField |
EmailField | EmailField |
FileField | FileField |
FilePathField | FilePathField |
FloatField | FloatField |
ForeignKey | ModelChoiceField (see below) |
ImageField | ImageField |
IntegerField | IntegerField |
IPAddressField | IPAddressField |
GenericIPAddressField | GenericIPAddressField |
ManyToManyField | ModelMultipleChoiceField (see below) |
NullBooleanField | NullBooleanField |
PositiveIntegerField | IntegerField |
PositiveSmallIntegerField | IntegerField |
SlugField | SlugField |
SmallIntegerField | IntegerField |
TextField | CharField with widget=forms.Textarea |
TimeField | TimeField |
URLField | URLField |
可能如你所料,ForeignKey 和 ManyToManyField 字段类型属于特殊情况:
此外,生成的每个表单字段都有以下属性集:
最后,请注意你可以为给定的模型字段重新指定表单字段。参见下文覆盖默认的字段。
考虑下面的模型:
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())
验证模型表单主要有两步:
与普通的表单验证类型类似,模型表单的验证在调用is_valid() 或访问errors 属性时隐式调用,或者通过full_clean() 显式调用,尽管在实际应用中你将很少使用后一种方法。
模型的验证(Model.full_clean())在表单验证这一步的内部触发,紧跟在表单的clean() 方法调用之后。
警告
Clean 过程会以各种方式修改传递给模型表单构造函数的模型实例。例如,模型的日期字段将转换成日期对象。验证失败可能导致模型实例处于不一致的状态,所以不建议重新使用它。
可以重写模型表单的clean() 来提供额外的验证,方法和普通的表单一样。
模型表单实例包含一个instance 属性,表示与它绑定的模型实例。
警告
ModelForm.clean() 方法设置一个标识符, 使得模型验证 这一步验证标记为unique、 unique_together 或unique_for_date|month|year 的模型字段的唯一性。
如果你需要覆盖clean() 方法并维持这个验证行为,你必须调用父类的clean() 方法。
作为验证过程的一部分,模型表单将调用与表单字段对应的每个模型字段的clean() 方法。如果你已经排除某些模型字段,这些字段不会运行验证。关于字段clean 和验证是如何工作的,参见表单字段的文档。
模型的clean() 方法在任何唯一性检查之前调用。关于模型clean() 钩子的更多信息,参见验证对象 。
表单字段级别或表单级别的错误信息永远比模型字段级别的错误信息优先。
模型字段的错误信息只用于模型验证步骤引发ValidationError 的时候,且不会有对应的表单级别的错误信息。
你可以根据模型验证引发的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.",
}
}
每个ModelForm还具有一个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.errors 为True。
save() 接受一个可选的commit 关键字参数,其值为True 或False。如果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)。
然而,有两种简单的方法保证你不会出现这些安全问题:
设置fields 属性为特殊的值'__all__' 以表示需要使用模型的所有字段。例如:
from django.forms import ModelForm
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = '__all__'
设置ModelForm 内联的Meta 类的exclude 属性为一个要从表单中排除的字段的列表。
例如:
class PartialAuthorForm(ModelForm):
class Meta:
model = Author
exclude = ['title']
因为Author 模型有3个字段name、title 和 birth_date,上面的例子会让name 和 birth_date 出现在表单中。
如果使用上面两种方法,表单中字段出现的顺序将和字段在模型中定义的顺序一致,其中ManyToManyField 出现在最后。
另外,Django 还将使用以下规则:如果设置模型字段的editable=False,那么使用ModelForm 从该模型创建的任何表单都不会包含该字段。
在旧的版本中,同时省略fields 和exclude 字段将导致模型的所有字段出现在表单中。现在这样做将引发一个ImproperlyConfigured 异常。
注
不会被上述逻辑包含进表单中的字段将不会被表单的save() 方法保存。另外,如果你手工添加排除的字段到表单中,它们也不会从模型实例初始化。
Django 将阻止保存不完全的模型,所以如果模型不允许缺失的字段为空且没有提供默认值,带有缺失字段的ModelForm 的save()将失败。为了避免这种失败,实例化模型时必须带有缺失的字段的初始值:
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。
使用内部类Meta 的widgets 属性可以指定一个字段的自定义Widget。它是映射字段名到Widget 类或实例的一个字典。
例如,Author 的name 属性为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 字典都可以接收。
类似地,如果你希望进一步自定义字段,你可以指定内部类Meta 的labels、help_texts 和error_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 属性中任何自定义的属性例如 widgets、labels、help_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 做一些自定义的验证,在保持blank 和help_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 排除了一个字段
当然,有一些注意事项
It’s possible to declaratively remove a Field inherited from a parent class by setting the name to be None on the subclass.
You can only use this technique to opt out from a field defined declaratively by a parent class; it won’t prevent the ModelForm metaclass from generating a default field. To opt-out from default fields, see 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_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()})
表单包含的字段可以用 fields或exclude关键字参数说明,或者用ModelForm内部Meta类的相应属性说明。请看 ModelForm文档: 选择使用的字段。
... 或者为指定的字段启用本地化功能。
>>> Form = modelform_factory(Author, form=AuthorForm, localized_fields=("birth_date",))
与普通表单集一样, 它是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',))
在旧版本中,同时省略fields 和exclude 的结果是表单集使用模型的所有字段。现在这么做将引发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>
Note
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())
默认情况下,当你使用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 参数,可以用字典值自定义ModelForm列出字段的widget类。这与 widgets字典在 ModelForm 的内部Meta类作用式一样。
>>> AuthorFormSet = modelformset_factory(
... Author, fields=('name', 'title'),
... widgets={'name': Textarea(attrs={'cols': 80, 'rows': 20})})
使用 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()方法返回储存入数据库中的实例。 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:
与普通表单集一样,你可以用在modelformset_factory()中使用 max_num 和 extra 参数,来控制额外表单的显示数量。
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个表单。实际上相当于没有限制。
模型表单集与表单集十分类似,Let’s say we want to present a formset to edit Author model instances:
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,
})
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. (上面已经描述了在保存窗体集中的对象。)
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.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
...
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.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
As stated earlier, you can override the default queryset used by the model 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,
})
Note that we pass the queryset argument in both the POST and GET cases in this example.
There are three ways to render a formset in a Django template.
第一种方式,你可以让表单集完成大部分的工作
<form method="post" action="">
{{ formset }}
</form>
Second, you can manually render the formset, but let the form deal with itself:
<form method="post" action="">
{{ 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" action="">
{{ 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" action="">
{{ 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 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)
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.models import inlineformset_factory
>>> BookFormSet = inlineformset_factory(Author, Book, fields=('title',))
>>> author = Author.objects.get(name='Mike Royko')
>>> formset = BookFormSet(instance=author)
Note
inlineformset_factory() uses modelformset_factory() and marks can_delete=True.
When overriding methods on InlineFormSet, you should subclass BaseInlineFormSet rather than BaseModelFormSet.
For example, if you want to override 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
...
See also Overriding clean() on a ModelFormSet.
Then when you create your inline formset, pass in the optional argument 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)
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, related_name='from_friends')
to_friend = models.ForeignKey(Friend, 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'))
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_to_response("manage_books.html", {
"formset": formset,
})
Notice how we pass instance in both the POST and GET cases.
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.
May 13, 2015