1. 程式人生 > >Django基礎(11): 表單集合Formset的高階用法詳解

Django基礎(11): 表單集合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的驗證。希望本文對你有所幫助。原創不易,歡迎點贊轉發。

接下來我會講下Django的Permission系統,歡迎關注我的微訊號。