Django 中的 form 相关功能,即是为了解决围绕用户输入产生的表单提交的一系列问题。
从用户视角看表单提交
- 浏览器收到 HTML 中的
<form>和各类<input>,渲染出表单供用户填写 - 用户填写后,点提交按钮,浏览器发送 POST 请求到服务端
- 如果提交的表单数据有误,页面刷新后仍在表单页,并且显示哪些字段出错
- 如果提交的表单数据正常,页面重定向到成功后的页面
这里 Django 用了 Post/Redirect/Get 的 pattern。因为假如没有 redirect 过程且提交后的页面 URL 不变,此时用户点刷新按钮时,一些老的浏览器会重新发 POST 请求。
Django 提供的能力
官方 文档 提到 Form 体系解决了这些问题:
- preparing and restructuring data to make it ready for rendering,大体上是指字段的定义
- creating HTML forms for the data,指定义好的字段如何渲染成 HTML 表单,同时生成的表单也会做出错提示
- receiving and processing submitted forms and data from the client,指检验和清洗用户提交的数据
Django 提供了这些机制:
- Form class 和 Forms API,用来表示表单的结构化数据,同时提供框架给用户定义检验和清理逻辑
- FormView,封装了一个标准的处理用户 POST/GET 表单页面的过程
- 模板中的
form变量,用来生成实际的 HTML 表单 - FormSet?
Form class
Basic Example
一个最简单的例子:
# forms.py
from django import forms
class NameForm(forms.Form):
your_name = forms.CharField(label='Your name', max_length=100)类似于 Django 的 Model 类在定义数据库数据的 schema,Form 类是在定义数据的 HTML 表单中展现。Model 中的字段对应的是数据库的列,而 Form 中的字段对应的是它的 HTML 展现,即各种 <input> 标签。
当你将上面的 NameForm 生成一个空的 Form 实例传给模板引擎:
<form action="/your-name/" method="post">
{{ form }}
<input type="submit" value="Submit">
</form>最终会被 Django 转换成 HTML 表单:
<form action="/your-name/" method="post">
<label for="your_name">Your name: </label>
<input id="your_name" type="text" name="your_name" maxlength="100">
<input type="submit" value="OK">
</form>可以观察到:
CharField在 HTML 上的默认展现是一个 text 类型的<input>label字段变成<label>标签max_length属性变成<input>中的一个长度限制的属性;同时当用户提交这个字段的数据时,Django 也会对其长度做校验
这便是 Form 类所解决的核心问题。
Bound v.s. unbound
当我们将 Form 实例传到模板中时,大部分情况下这个 Form 实例都是未填充数据(unbound)的,比如说 name 字段的值为空。这是因为出现表单的场景中,大部分是我们希望用户来填。但是有一些情况,适合预先将值填充到表单中:
- 在编辑一个已有的数据时,比如作为管理员编辑帖子标题
- 有一些数据是从别的地方获取的,并不需要用户填写时
- 当用户之前在这个页面已经有提交过数据时,
- 比如用户新写了一篇帖子,点击保存后仍然希望处于编辑状态
- 比如用户上一次提交的表单数据有误时,后台在它提交后,应该返回原有的数据及相应的错误提示
这引出了 bound 和 unbound 的概念。一个 Form 实例只能处在 bound 或者 unbound 状态:
>>> f = ContactForm()
>>> f.is_bound
False
>>> f = ContactForm({'subject': 'hello'})
>>> f.is_bound
True差异在于:
- Bound 的 form,它已经有了数据,因此可以做数据校验,同时渲染成
<input>标签时也是带有值的 - Unbound 的 form,它还没有数据,无法做数据校验,渲染成
<input>时值为空或者是指定的默认值
Validation
Form 提供了校验数据正确性的方法 is_valid(),以及一系列自定义校验、清洗规则的能力,具体看 官方文档。
Widget
Form 中定义的每一种 Field,都有对应的 Widget 类代表它的展现,比如 CharField 默认对应的是 TextInput。你可以修改你的字段所用的是 widget 类,比如让你的 message 字段使用 Textarea widget 而不是 TextInput widget,这样它渲染成 HTML 时会变成 <textarea> 而不是 <input type="text">。
The View
如果用传统的 function-based view,一个典型的表单 view 像这样:
def contact_us(request):
if request.method == 'POST':
form = forms.ContactForm(request.POST)
if form.is_valid():
form.send_mail()
return HttpResponseRedirect('/')
else:
form = forms.ContactForm()
return render(request, 'contact_form.html', {'form': form})但 Django 提供了一个 FormView 简化了这个过程。上面的代码可以完整地换成 FormView 来这样表达:
class ContactUsView(FormView):
template_name = "contact_form.html"
form_class = forms.ContactForm
success_url = "/"
def form_valid(self, form):
form.send_mail()
return super().form_valid(form)Template
官网文档 给出了简单易懂的说明。
在渲染成表单的 HTML 过程中,主要的问题是需要跟 CSS 框架结合,因此 Django 提供了几个能力:
- 默认的
{{ form }}会生成仅带<label>和<input>的 HTML。可以用{{ form.as_table }}{{ form.as_p }}{{ form.as_ul }}分别生成被<tr><p><li>包裹的每个字段。我觉得这个功能用途一般 - 可以把每个字段的标签 ID、帮助信息和错误信息等,单独拿出来做展示。官网有文档
Forms from models
直接将 Model 用于 Form 的一种机制,省去了重复定义字段的过程:
>>> 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)Form Assets
一些 widget 往往需要额外的 CSS 和 JS 配合。Django 提供了机制让你定义这些额外依赖,并且让你方便在模板中使用它:
from django import forms
class CalendarWidget(forms.TextInput):
class Media:
css = {
'all': ('pretty.css',)
}
js = ('animations.js', 'actions.js')>>> w = CalendarWidget()
>>> print(w.media)
<link href="http://static.example.com/pretty.css" type="text/css" media="all" rel="stylesheet">
<script type="text/javascript" src="http://static.example.com/animations.js"></script>
<script type="text/javascript" src="http://static.example.com/actions.js"></script>你可以在模板中用 {{ form.media }} 来生成上面的三行 <link> / <script>。{{ form.media }} 似乎没有官网上有明确的文档,不知道是不是 Django 团队的疏忽。
Formset
Formset 即是多个 form 组合起来的一个对象,而多个 form 意味着你可以同时在页面操作多个 model 实例。Practical Django 2 and Channels 2 中有一个例子,是实现一个购物车页面,上面有多件商品,你可以同时修改所需的数量。