Django: Form

20th August 2020 at 2:19pm
Django

Django 中的 form 相关功能,即是为了解决围绕用户输入产生的表单提交的一系列问题。

从用户视角看表单提交

  1. 浏览器收到 HTML 中的 <form> 和各类 <input>,渲染出表单供用户填写
  2. 用户填写后,点提交按钮,浏览器发送 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 classForms 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 中有一个例子,是实现一个购物车页面,上面有多件商品,你可以同时修改所需的数量。