Django Formsets总结

formset是将多个表单用在同一个页面上的抽象层。

我们有:

from django import forms
class ArticleForm(forms.Form):
    title=forms.CharField()
    pub_date=forms.DateField()

为允许一次性创建几个articles,可以创建一个ArticleForm的formset类ArticleFormSet

>>>from django.forms import formset_factory
>>>ArticleFormSet=formset_factory(ArticleForm)

ArticleFormSet实例化,然后遍历其实例,就可以像一般的表单全部显示出来了:

>>>formset=ArticleFormSet()
>>>for form in formset::
        print(form.as_table())
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title"></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></td></tr>

可以看到只有一个空表单,空表单的数量是由extra参数控制的,默认地,formset_factory()定义了一个extra form

对formset使用初始值

初始值的使用是formset的一大用法,对FormSet类的实例使用参数initial,其值为包含有字典的列表,字典的键就是form种定义的字段名,字典的数量就是初始化的表单的数量。

>>>import datetime
>>>ArticleFormSet=formset_factory(ArticleForm,extra=2)
>>>formset=ArticleFormSet(initial=[{'title':'Django is now open source','pub_date':datetime.date.today()}])
>>>for form in formset:
    print(form.as_table())
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Django is now open source" id="id_form-0-title"></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-12" id="id_form-0-pub_date"></td></tr>
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" id="id_form-1-title"></td></tr>
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" id="id_form-1-pub_date"></td></tr>
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title"></td></tr>
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></td></tr>

可以看到一共显示了3个表单,1个是初始化的,2个空表单。

限制表单的最大数量

formset_factorymax_num参数用来限制显示的表单数

>>>ArticleFormSet=formset_factory(ArticleForm,extra=2,max_num=1)
>>>formset=ArticleFormSet()
>>>for form in formset:
    print(form.as_table())
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title"></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></td></tr>

显示的数量其实是由initial,max_num,extra参数共同决定的:

  • initialextra之和(可以理解为应该显示的表单数量),小于或者等于max_num时,全部显示

  • initialextra之和大于max_num时,保证初始化的都显示,即:

    • 如果initial小于max_num时,初始化的全部显示,显示max_numinitial之差数量的空表单
    • 如果initial大于max_num时,初始化的全部显示,没有空表单。

    max_num的值默认为None,即显示1000个表单,在实际应用种,这基本等于没有限制了。

    默认地,max_num仅影响有多少表单显示,而不影响校验。如果validate_max=True,那么max_num就影响校验。

    FormSet校验

    formset的校验与form的校验基本相同。调用is_valid()就可以校验formset的所有表单。

    >>> data={ 
    ... 'form-TOTAL_FORMS':'2',
    ... 'form-INITIAL_FORMS':'0',
    ... 'form-MAX_NUM_FORMS':'',
    ... 'form-0-title':'test',
    ... 'form-0-pub_date':'1904-06-06',
    ... 'form-1-title':'TEST',
    ... 'form-1-pub_date':'',
    ... }
    >>> formset=ArticleFormSet(data)
    >>> formset.errors
    [{}, {'pub_date': ['This field is required.']}]
    >>> ss=ArticleFormSet(data)
    >>> ss.errors
    [{}, {'pub_date': ['This field is required.']}]
    >>> ss.is_valid()
    False
    

    可以看到formset.errors是对应于formset表单的列表,列表中每个字典的键是出现异常的字段名,需要注意的是,不需要调用is_valid(),就有errors属性。

    Form用法类似,formset的每个form可以包含maxlength等的HTML使用的属性作为浏览器的校验。然而formsetform不能包含required属性,因为添加或者删除表单时,校验有可能出错。

    为了得到formset的校验错误数量,可以用total_error_count

    >>> len(ss.errors)
    2
    >>> formset.total_error_count()
    1
    

    还可以用has_changed()来判断是否初始值被改变了。

    >>> data={
    ... 'form-TOTAL_FORMS':'1',
    ... 'form-INITIAL_FORMS':'0',
    ... 'form-MAX_NUM_FORMS':'',
    ... 'form-0-title':'',
    ... 'form-0-pub_date':''}
    >>> k=ArticleFormSet(data)
    >>> k.has_changed() #k的初始值是空字符,现在还是空字符
    False
    >>> ss.has_changed() 
    True
    

    理解ManagementForm

    form-TOTAl_FORMS,form-INITIAL_FORMSform-MAX_NUM_FORMS就是ManagementForm的字段。

    这个表单就在专门用来管理formset中的所有表单的,如果不提供这个管理数据,就会抛出错误。

    它是用来追踪有多少显示的表单。如果通过JavaScript添加了新表单,那么也应该相应的增加ManagementForm中表单的数量。

    management_form也是可以作为formset本身的属性,在template中,可以通过{{ my_formset.management_form }}来包含其管理数据。

    所以,在写template时候,必须包含{{ my_formset.management }} 否则,myform=formset(request.POST)中就缺失ManagementForm的数据,就会抛出异常:

    image-20201101115034148

    total_form_count,initial_form_count

    对于ManagementForm,有2个方法与之密切相连:total_form_count,intial_form_count

    >>> ss.initial_form_count()
    0
    >>> ss.total_form_count()
    2
    

    定制formset 校验

    formset有一个与Form类似的clean方法,这就是可以定制校验的地方。可以通过重载

    #forms.py
    DRINKS=((None,'Please select a drink type'),(1,'Mocha'),(2,'Espresso'),(3,'Latte'))
    SIZES=((None,'Please select a drink size'),('s','Small'),('m','Medium'),('1','Large'))
    
    class DrinkForm(forms.Form):
        name=forms.ChoiceField(choices=DRINKS,initial=0)
        size=forms.ChoiceField(choices=SIZES,initial=0)
        amount=forms.ChoiceField(choices=[(None,'Amount of drinks')]+[(i,i) for i in range(1,10)])
    
    from django.forms import BaseFormSet 
    class BaseDrinkFormSet(BaseFormSet):
        def clean(self):
            if any(self.errors):
                return None 
            name_size_tuples=[]
            for form in self.forms:
                name_size=(form.cleaned_data['name'],form.cleaned_data['size'])
                if name_size in name_size_tuples:
                    raise forms.ValidationError('UPs! You have multiple %s %s items in your order'%(dict(SIZES)[name_size[1]],dict(DRINKS)[int(name_size[0])]))
                name_size_tuples.append(name_size)
    
    

    以上继承并重载了BaseFormSet类,在BaseDrinkFormSet中,重载了类方法clean(),在该方法中,若发现有某个表单有异常(即any(self.errors),直接结束,如果在所有表单都是正确的情况下,对所有表单的name,size字段进行核查,有这两个字段都相同的,抛出异常。

    >>> from testapp.forms import *
    >>> from django.forms import formset_factory
    >>> a=formset_factory(DrinkForm,formset=BaseDrinkFormSet)
    >>> data={
    ...     'form-TOTAL_FORMS':'2',
    ...     'form-INITIAL_FORMS':'0',
    ...     'form-MAX_NUM_FORMS':'',
    ...     'form-0-name':1,
    ...     'form-0-size':'s',
    ...     'form-0-amount':1,
    ...     'form-1-name':1,
    ...     'form-1-size':'s',
    ...     'form-1-amount':2,}
    >>> formset=a(data)
    >>> formset.errors
    [{}, {}]
    >>> formset.non_form_errors()
    ['UPs! You have multiple Small Mocha items in your order']
    

    formset的clean()在所有的Form.clean()调用后,被调用(所有可以在formset的clean()方法中遍历formset,并使用每个form的cleaned_data,用non_form_errors()来发现找到的错误。

    校验formset中的表单数量

    validate_max

    如果validate_max=True传入formset_factory(),也会校验formset中的表单数减去标记为删减的表单是否超过了max_num

    >>> from django.forms import formset_factory
    >>> from testapp.forms import * 
    >>> drinkformset=formset_factory(DrinkForm,max_num=1,validate_max=True)
    >>> data={
    ...     'form-TOTAL_FORMS':'2',
    ...     'form-INITIAL_FORMS':'0',
    ...     'form-MIN_NUM_FORMS':'',
    ...     'form-MAX_NUIM_FORMS':'',
    ...     'form-0-name':2,
    ...     'form-0-size':'s',
    ...     'form-0-amount':1,
    ...     'form-1-name':3,
    ...     'form-1-size':'m',
    ...     'form-1-amount':2}
    >>> formset=drinkformset(data)
    >>> formset.is_valid()
    False
    >>> formset.errors
    [{}, {}]
    >>> formset.non_form_errors()
    ['Please submit 1 or fewer forms.']
    

    validate_min

    validate_max类似。

    >>> drinkformset=formset_factory(DrinkForm,min_num=3,validate_min=True)
    >>> data={
    ...     'form-TOTAL_FORMS':'2',
    ...     'form-INITIAL_FORMS':'0',
    ...     'form-MIN_NUM_FORMS':'',
    ...     'form-MAX_NUIM_FORMS':'',
    ...     'form-0-name':2,
    ...     'form-0-size':'s',
    ...     'form-0-amount':1,
    ...     'form-1-name':3,
    ...     'form-1-size':'m',
    ...     'form-1-amount':2}
    >>> formset=drinkformset(data)
    >>> formset.is_valid()
    False
    >>> formset.non_form_errors()
    ['Please submit 3 or more forms.']
    >>> form.errors
    Traceback (most recent call last):
      File "<console>", line 1, in <module>
    NameError: name 'form' is not defined
    >>> formset.errors
    [{}, {}]
    
    >>> drinkformset=formset_factory(DrinkForm,min_num=3,validate_min=True)
    >>> data={
    ...     'form-TOTAL_FORMS':'2',
    ...     'form-INITIAL_FORMS':'0',
    ...     'form-MIN_NUM_FORMS':'',
    ...     'form-MAX_NUIM_FORMS':'',
    ...     'form-0-name':2,
    ...     'form-0-size':'s',
    ...     'form-0-amount':1,
    ...     'form-1-name':3,
    ...     'form-1-size':'m',
    ...     'form-1-amount':2}
    >>> formset=drinkformset(data)
    >>> formset.is_valid()
    False
    >>> formset.non_form_errors()
    ['Please submit 3 or more forms.']
    >>> formset.errors
    [{}, {}]
    

    表单序号(Ordering)和删除(Deletion)

    can_order

    默认是False.

    >>> articleFormset=formset_factory(ArticleForm,can_order=True)
    >>> import datetime
    >>> formset=articleFormset(initial=[
    ... {'title':'Article #1','pub_date':datetime.date(2008,5,10)},
    ... {'title':'Article #2','pub_date':datetime.date(2008,5,11)}
    ... ])
    >>> for form in formset:
    ...     print(form.as_table())
    ... 
    <tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" 
    value="Article #1" id="id_form-0-title"></td></tr>
    <tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date"></td></tr>
    <tr><th><label for="id_form-0-ORDER">Order:</label></th><td><input type="number" name="form-0-ORDER" value="1" id="id_form-0-ORDER"></td></tr>
    <tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" 
    value="Article #2" id="id_form-1-title"></td></tr>
    <tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date"></td></tr>
    <tr><th><label for="id_form-1-ORDER">Order:</label></th><td><input type="number" name="form-1-ORDER" value="2" id="id_form-1-ORDER"></td></tr>
    <tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" 
    id="id_form-2-title"></td></tr>
    <tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></td></tr>
    <tr><th><label for="id_form-2-ORDER">Order:</label></th><td><input type="number" name="form-2-ORDER" id="id_form-2-ORDER"></td></tr>
    

    可见,自动为每个form添加了order字段。

    can_delete

    默认为False

    >>> from django.forms import formset_factory
    >>> from myapp.forms import ArticleForm
    >>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True)
    >>> formset = ArticleFormSet(initial=[
    ...     {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
    ...     {'title': 'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
    ... ])
    >>> for form in formset:
    ...     print(form.as_table())
    <tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title"></td></tr>
    <tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date"></td></tr>
    <tr><th><label for="id_form-0-DELETE">Delete:</label></th><td><input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE"></td></tr>
    <tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title"></td></tr>
    <tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date"></td></tr>
    <tr><th><label for="id_form-1-DELETE">Delete:</label></th><td><input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE"></td></tr>
    <tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title"></td></tr>
    <tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></td></tr>
    <tr><th><label for="id_form-2-DELETE">Delete:</label></th><td><input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE"></td></tr>
    

    可见,can_delete=True为每个form添加了个删除校对框。

    如果在使用ModelFormSet,对于选中的删除的表单,在执行formset.save()时,该表单的实例将会被删除。但是如果调用formset.save(commit=False),将不会删除,需要用formset.deleted_objects来删除:

    >>> instances = formset.save(commit=False)
    >>> for obj in formset.deleted_objects:
    ...     obj.delete()
    

    在formset里添加额外字段

    添加额外自定义的字段,只需要继承BaseFormSet类,然后重载其函数add_fields(self.form,index)即可。

    class ArticleForm(forms.Form):
        title=forms.CharField()
        pub_date=forms.DateField()
    
    class BaseArticleFormSet(BaseFormSet):
        def add_fields(self,form,index):
            super().add_fields(form,index)
            form.fields['my_field']=forms.CharField()
    
    >>> from django.forms import formset_factory
    >>> from testapp.forms import *
    >>> articleFormset=formset_factory(ArticleForm,formset=BaseArticleFormSet)
    >>> formset=articleFormset()
    >>> for form in formset:
    ...     print(form.as_table())
    ... 
    <tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" 
    id="id_form-0-title"></td></tr>
    <tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></td></tr>
    <tr><th><label for="id_form-0-my_field">My field:</label></th><td><input type="text" name="form-0-my_field" id="id_form-0-my_field"></td></tr>
    

    可以发现已经添加了自定义字段my field.

    自定义formset的前缀

    默认前缀都是form,比如:

    <label for="id_form-0-title">Title:</label>
    <input type="text" name="form-0-title" id="id_form-0-title">
    
    >>> formset=formset_factory(ArticleForm)
    >>> articleFormset=formset_factory(ArticleForm)
    >>> formset=articleFormset(prefix='article')
    >>> for form in formset:
    ...     print(form.as_table())
    ... 
    <tr><th><label for="id_article-0-title">Title:</label></th><td><input type="text" name="article-0-title" id="id_article-0-title"></td></tr>
    <tr><th><label for="id_article-0-pub_date">Pub date:</label></th><td><input type="text" name="article-0-pub_date" id="id_article-0-pub_date"></td></tr>
    
##### 愿你一寸一寸地攻城略地,一点一点地焕然一新 #####
原文地址:https://www.cnblogs.com/johnyang/p/13917176.html