• Django表单集合Formset的高级用法


    Formset(表单集)是多个表单的集合。Formset在Web开发中应用很普遍,它可以让用户在同一个页面上提交多张表单,一键添加多个数据,比如一个页面上添加多个用户信息。今天小编我就介绍下Django Formset的基础知识,Formset的分类以及如何使用Formset。

    为什么要使用Django Formset

    我们先来下看下Django中不使用Formset情况下是如何在同一页面上一键提交2张或多张表单的。我们在模板中给每个表单取不同的名字,如form1和form2(如下面代码所示)。注: form1和form2分别对应forms.py里的Form1()和Form2()。

    <form >
        {{ form1.as_p }}
        {{ form2.as_p }}
    </form>
    

    用户点击提交后,我们就可以在视图里了对用户提交的数据分别处理。

    if request.method == 'POST':
            form1 = Form1( request.POST,prefix="form1")
            form2 = Form2( request.POST,prefix="form2")
            
            if form1.is_valid() or form2.is_valid(): 
                pass
    else:
            form1 = Form1(prefix="form1")
            form2 = Form2(prefix="form2")
    

    这段代码看似并不复杂,然而当表单数量很多或不确定时,这个代码会非常冗长。我们希望能控制表单的数量,这是我们就可以用Formset了。

    Formset的分类

    Django针对不同的formset提供了3种方法: formset_factory, modelformset_factory和inlineformset_factory。我们接下来分别看下如何使用它们。

    如何使用formset_factory

    对于继承forms.Form的自定义表单,我们可以使用formset_factory。我们可以通过设置extra和max_num属性来确定我们想要展示的表单数量。注意: max_num优先级高于extra。比如下例中,我们想要显示3个空表单(extra=3),但最后只会显示2个空表单,因为max_num=2。

    from django import forms
    
    
    class BookForm(forms.Form):
        name = forms.CharField(max_length=100)
        title = forms.CharField()
        pub_date = forms.DateField(required=False)
    
    
    # forms.py - build a formset of books
    
    from django.forms import formset_factory
    from .forms import BookForm
    
    # extra: 想要显示空表单的数量
    # max_num: 表单显示最大数量,可选,默认1000
    
    BookFormSet = formset_factory(BookForm, extra=3, max_num=2)
    

    在视图文件views.py里,我们可以像使用form一样使用formset。

    # views.py - formsets example.
    from .forms import BookFormSet
    from django.shortcuts import render
    
    def manage_books(request):
        if request.method == 'POST':
            formset = BookFormSet(request.POST, request.FILES)
            if formset.is_valid():
                # do something with the formset.cleaned_data
                pass
        else:
            formset = BookFormSet()
        return render(request, 'manage_books.html', {'formset': formset})
    

    模板里可以这样使用formset。

    <form action=”.” method=”POST”>
    {{ formset }}
    </form>
    

    也可以这样使用。

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

    如何使用modelformset_factory

    Formset也可以直接由模型model创建,这时你需要使用modelformset_factory。你可以指定需要显示的字段和表单数量。

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

    当然上面方法我并不推荐,因为对单个表单添加验证方法非常不方便。我更喜欢的方式先创建自定义的ModelForm,添加单个表单验证,然后再利用modelformset_factory创建formset。

    class AuthorForm(forms.ModelForm):
        class Meta:
            model = Author
            fields = ('name', 'title')
    
        def clean_name(self):
            # custom validation for the name field
            ...
    

    由ModelForm创建formset:

    AuthorFormSet = modelformset_factory(Author, form=AuthorForm)
    

    在模板和视图里使用formset的方法与前面的例子是一样的。

    如何使用inlineformset_factory

    试想我们有如下recipe模型,Recipe与Ingredient是单对多的关系。一般的formset只允许我们一次性提交多个Recipe或多个Ingredient。但如果我们希望同一个页面上添加一个菜谱(Recipe)和多个原料(Ingredient),这时我们就需要用使用inlineformset了。

    from django.db import models
    
    
    class Recipe(models.Model):
        title = models.CharField(max_length=255)
        description = models.TextField()
    
    
    class Ingredient(models.Model):
        recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, related_name='ingredient')
        name = models.CharField(max_length=255)
    

    利用inlineformset_factory创建formset的方法如下所示。该方法的第一个参数和第二个参数都是模型,其中第一个参数必需是ForeignKey。

    # forms.py
    from django.forms import ModelForm
    from django.forms import inlineformset_factory
    
    from .models import Recipe, Ingredient, Instruction
    
    
    class RecipeForm(ModelForm):
        class Meta:
            model = Recipe
            fields = ("title", "description",)
    
    
    IngredientFormSet = inlineformset_factory(Recipe, Ingredient, fields=('name',),
                                              extra=3, can_delete=False, max_num=5)
    

    views.py中使用formset创建和更新recipe的代码如下。在对IngredientFormSet进行实例化的时候,必需指定recipe的实例。

    def recipe_update(request, pk):
        recipe = get_object_or_404(Recipe, pk=pk)
        if request.method == "POST":
            form = RecipeForm(request.POST, instance=recipe)
    
            if form.is_valid():
                recipe = form.save()
                ingredient_formset = IngredientFormSet(request.POST, instance=recipe)
    
                if ingredient_formset.is_valid():
                    ingredient_formset.save()
    
            return redirect('/recipe/')
        else:
            form = RecipeForm(instance=recipe)
            ingredient_formset = IngredientFormSet(instance=recipe)
    
        return render(request, 'recipe/recipe_update.html', {'form': form,
                                                             'ingredient_formset': ingredient_formset,
                                                          })
    
    def recipe_add(request):
        if request.method == "POST":
            form = RecipeForm(request.POST)
    
            if form.is_valid():
                recipe = form.save()
                ingredient_formset = IngredientFormSet(request.POST, instance=recipe)
    
                if ingredient_formset.is_valid():
                    ingredient_formset.save()
    
            return redirect('/recipe/')
        else:
            form = RecipeForm()
            ingredient_formset = IngredientFormSet()
    
        return render(request, 'recipe/recipe_add.html', {'form': form,
                                                          'ingredient_formset': ingredient_formset,
                                                          })
    

    模板recipe/recipe_add.html代码如下。

    <h1>Add Recipe</h1>
    <form action="." method="post">
        {% csrf_token %}
        
        {{ form.as_p }}
        
        <fieldset>
            <legend>Recipe Ingredient</legend>
            {{ ingredient_formset.management_form }}
            {{ ingredient_formset.non_form_errors }}
            {% for form in ingredient_formset %}
                    {{ form.name.errors }}
                    {{ form.name.label_tag }}
                    {{ form.name }}
                </div>
          {% endfor %}
        </fieldset>
    
        <input type="submit" value="Add recipe" class="submit" />
    </form>
    

    最后的效果如下图所示:

    整个formset的验证

    formset由多个表单组成,单个表单的验证可以通过自定义的clean方法来完成,然而有时我们需要对整个formset的数据进行验证。一个常见例子就是去重。

    比如下面例子中用户一次性提交多篇文章标题后,我们需要检查title是否已重复。我们先定义一个BaseFormSet,然后使用formset=BaseArticleFormSet添加formset的验证。

    from django.forms import BaseFormSet
    from django.forms import formset_factory
    from myapp.forms import ArticleForm
    
    class BaseArticleFormSet(BaseFormSet):
        def clean(self):
            """Checks that no two articles have the same title."""
    if any(self.errors):
                return
    
            titles = []
            for form in self.forms:
                title = form.cleaned_data['title']
                if title in titles:
                    raise forms.ValidationError("Articles in a set must have distinct titles.")
            titles.append(title)
    
    ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
    

    给Formset添加额外字段

    在BaseFormSet里我们不仅可以添加formset的验证,而且可以添加额外的字段,如下所示:

    from django.forms import BaseFormSet
    from django.forms import formset_factory
    from myapp.forms import ArticleForm
    
    class BaseArticleFormSet(BaseFormSet):
        def add_fields(self, form, index):
            super().add_fields(form, index)
            form.fields["my_field"] = forms.CharField()
              
    ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
    

    小结

    Formset真的非常有用,属于Django必备的基础知识之一。使用的时候先定义单个的form,然后利用factory生成formset。你需要根据不同应用场景选择不同的formset,并了解如何进行formset的验证。希望本文对你有所帮助。原创不易,欢迎点赞转发。

  • 相关阅读:
    libuv 中文编程指南(一)序言
    一些鲜为人知却非常实用的数据结构
    libuv 中文编程指南(二)libuv 基础
    Zookeeper 的 Lua 绑定(二)
    高度怀疑
    不能没有你
    第一次看流星雨记
    调侃下蓝网队 我还是比较极端的 不要好 那就要坏吧
    摇滚校园
    守法公民
  • 原文地址:https://www.cnblogs.com/floodwater/p/9994944.html
Copyright © 2020-2023  润新知