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系統,歡迎關注我的微訊號。