介绍Django的表单设计、校验与渲染。
表单用于处理用户输入。 在任何 Web 应用程序或网站中,这都是一项非常常见的任务。 执行此操作的标准方法是通过 HTML 表单,用户在其中输入一些数据,将其提交给服务器,然后服务器对其进行处理。
表单处理是一项相当复杂的任务,因为它涉及与应用程序的许多层进行交互。 还有很多问题需要处理。 例如,提交给服务器的所有数据都是字符串格式,因此必须在对其进行任何操作之前将其转换为适当的数据类型(整数、浮点数、日期等)。 必须验证有关应用程序业务逻辑的数据。 还必须正确地清理、清洁数据,以避免 SQL 注入、 XSS 攻击、CSRF 攻击等安全问题。本文将对CSRF(Cross-Site Request Forgery,跨站请求伪造)进行进一步的阐述。
Django Forms API
使整个过程变得更加容易,自动化了大部分工作。 其比大多数程序员自己实现的代码更安全。 因此,无论 HTML 表单多么简单,请始终使用Django Forms API
。
默认情况下,Django提供了中间件django.middleware.csrf.CsrfViewMiddleware
, 它能保护所有表单免受跨站请求伪造
的攻击。配置如下:
...
MIDDLEWARE_CLASSES=(
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
),
...
恶意网站把请求发送到被攻击者已登录的网站时就会引发 CSRF
攻击。为了实现 CSRF 保护,Django需要程序设置一个密钥,而在表单设计中使用这个密钥生成加密令牌,再用令牌验证请求中表单数据的真伪。
在这里使用城市景点
案例中的新增城市
的表单模板设计:
new_city.html
{% extends 'base.html' %}
{% block title %}开启一个新城市 - {{ block.super }} {% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'cityspot:home' %}">城市列表</a></li>
<li class="breadcrumb-item active">新建城市</li>
{% endblock %}
{% block content %}
<form method="post" novalidate>
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-success">发布</button>
<a href="{% url 'cityspot:home' %}" class="btn btn-outline-secondary" role="button">取消</a>
</form>
{% endblock %}
在 <form>
标记中,必须定义method
方法属性。 这指示浏览器希望如何与服务器通信。 HTTP
协议规范定义了几种请求方法。 但在大多数情况下,只会使用 GET
和 POST
请求方法类型。
GET
可能是最常见的请求类型。 它用于从服务器检索数据。 每次单击链接或直接在浏览器中键入 URL
时,都在创建一个 GET
请求。
POST
是在想要更改服务器上的数据时使用 。 因此,一般来说,每次向服务器发送数据会导致资源状态发生变化时,都应该始终通过 POST
请求发送。
Django 使用 CSRF令牌
(跨站点请求伪造令牌)保护所有 POST 请求。 这是一种避免外部站点或应用程序向我们的Web
应用程序提交数据的安全措施。 应用程序每次收到 POST 时,都会先查找 CSRF令牌
。 如果请求没有令牌,或者令牌无效,则会丢弃发布的数据。
csrf_token
模板标签:
{% csrf_token %}
其结果是与其他表单数据一起提交的隐藏字段:
<input type="hidden" name="csrfmiddlewaretoken" value="jG2o6aWj65YGaqzCpl0TYTg5jn6SctjzRZ9KmluifVx0IVaxlwh97YarZKs54Y32">
是时候使用 Forms API
了,Forms API
在模块 django.forms
中可用。 Django 使用两种类型的表单:forms.Form
和 forms.ModelForm
。 Form
类是一个通用的表单实现,可以使用它来处理与应用程序中的模型没有直接关联的数据。 ModelForm
是 Form
的子类,它与模型类相关联。
让我们在城市景点
项目案例的 cityspot
的文件夹中创建一个名为 forms.py 的新文件:
cityspot/forms.py
from django import forms
from django.utils.translation import gettext_lazy as _
from .models import City
class CityForm(forms.ModelForm):
name = forms.CharField(
label=_('名称'),
required=True,
error_messages={'required':'这是必填栏。'},
widget=forms.TextInput(attrs={'class': 'form-control'}))
description = forms.CharField(
label=_('说明'),
required=True,
error_messages={'required':'这是必填栏。'},
widget=forms.TextInput(attrs={'class': 'form-control'}))
class Meta:
model = City
fields = ['name', 'description']
这是我们的一个表格。 它是与 City
模型关联的 ModelForm
。 Meta
类中的字段列表中的name
和description
是指 City
类中的属性字段。 这是指我们要保存的 City
中的信息。
现在来看一下views.py
中的视图控制类的实现:
from django.shortcuts import render, redirect
from django.views import View
from .models import City
from .forms import CityForm
class CityView(View):
def get(self, request):
form = CityForm()
return render(request, 'new_city.html', {'form': form })
def post(self, request):
form = CityForm(request.POST)
if form.is_valid():
city = form.save(commit=False)
city = city.save()
return redirect('cityspot:home')
else:
return render(request, 'new_city.html', {'form': form })
首先,视图控制类的父类View
的对象检查请求是 POST
还是 GET
。 如果请求来自 POST
,则意味着用户正在向服务器提交一些数据。 所以实例化一个表单实例,将 POST
数据传递给表单:form = CityForm(request.POST)
。
然后,我们要求 Django 验证数据,检查表单是否有效if form.is_valid():
,来决定我们可以将其保存在数据库中。 如果表单有效,我们继续使用 city.save()
将数据保存在数据库中。 save()
方法返回保存到数据库中的模型实例。 之后,重定向到主页列表,既可以避免用户重新提交表单,也可以保持应用程序的流畅。
现在,如果数据无效,Django 会在表单中添加错误列表。 之后,视图什么也不做,并在最后一条语句中返回:
return render(request, 'new_city.html', {'form': form })
这意味着我们必须更新 new_city.html
以更正显示错误。
如果请求是 GET,我们只需使用 form = CityForm()
初始化一个新的空表单。
Django Forms API
不仅仅是处理和验证数据。 它还为我们生成 HTML。
{{ form.as_p }}
该表单具有三个呈现选项:form.as_table
、form.as_ul
和 form.as_p
。 这是呈现表单所有字段的快速方法。 顾名思义,as_table
使用表格标记来格式化输入, as_ul
创建输入的 HTML 列表等。
这里只需键入 {{ form.as_p }}
即可呈现所有字段。而且,使用 Forms API
,Django 将验证数据并向每个字段添加错误消息。
另外,Django Forms API
它还可以处理帮助文本help_text
,可以在 Form
类或 Model
类中定义:
class CityForm(forms.ModelForm):
name = forms.CharField(
label=_('名称'),
required=True,
error_messages={'required':'这是必填栏。'},
widget=forms.TextInput(attrs={'class': 'form-control'}))
description = forms.CharField(
label=_('说明'),
required=True,
error_messages={'required':'这是必填栏。'},
help_text='最大长度不超过200字。',
widget=forms.TextInput(attrs={'class': 'form-control'}))
class Meta:
model = City
fields = ['name', 'description']
为了使得表单设计变得更加漂亮,在使用 Bootstrap 前端库框架时,使用了名为django-widget-tweaks
的 Django
包。 它能够更好地控制渲染过程,保持默认设置并在其之上添加额外的自定义。
从安装这个包开始:
pip install django-widget-tweaks
现在将其添加到 INSTALLED_APPS
:
...
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'widget_tweaks', # <--添加应用 APP
'cityspot',
]
...
现在开始在模板设计中使用它:
templates/new_city.html
{% extends 'base.html' %}
{% load widget_tweaks %}
{% block title %}开启一个新城市 - {{ block.super }} {% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'cityspot:home' %}">城市列表</a></li>
<li class="breadcrumb-item active">新建城市</li>
{% endblock %}
{% block content %}
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
{{ field.label_tag }}
{% render_field field class="form-control" %}
{% if field.help_text %}
<small class="form-text text-muted">
{{ field.help_text }}
</small>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn btn-success">发布</button>
<a href="{% url 'cityspot:home' %}" class="btn btn-outline-secondary" role="button">取消</a>
</form>
{% endblock %}
就在这里! 正在使用 django-widget-tweaks
。 首先,使用 {% load widget_tweaks %}
模板标签指令将其加载到模板中。 然后的用法是:
{% render_field field class="form-control" %}
render_field
标签指令不是 Django
的一部分; 它位于刚刚安装的软件包中。 要使用它,必须传递一个表单字段实例作为第一个参数,然后可以添加任意 HTML 属性来补充它。 这将很有用,因为这样就可以根据某些条件分配样式类
。
render_field
模板标签的一些举例:
{% render_field form.name class="form-control" %}
{% render_field form.description class="form-control" placeholder=form.description.label %}
{% render_field field class="form-control" placeholder="描写说明!" %}
{% render_field field style="font-size: 20px" %}
现在要实现 Bootstrap
的验证标签,可以进一步更改 new_city.html
模板:
....
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
{{ field.label_tag }}
{% if form.is_bound %}
{% if field.errors %}
{% render_field field class="form-control is-invalid" %}
{% for error in field.errors %}
<div class="invalid-feedback">
{{ error }}
</div>
{% endfor %}
{% else %}
{% render_field field class="form-control is-valid" %}
{% endif %}
{% else %}
{% render_field field class="form-control" %}
{% endif %}
{% if field.help_text %}
<small class="form-text text-muted">
{{ field.help_text }}
</small>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn btn-success">发布</button>
<a href="{% url 'cityspot:home' %}" class="btn btn-outline-secondary" role="button">取消</a>
</form>
....
所以,我们有三种不同的渲染状态:
初始状态
:表单没有数据(未绑定)无效的状态
:添加 .is-invalid
CSS 样式类,并在具有 .invalid-feedback
样式类的元素中添加错误消息。 表单字段和消息以红色呈现。有效的状态
:添加了 .is-valid
CSS 样式类,以便将表单字段绘制为绿色,向用户反馈该字段可以使用。表单模板代码看起来有点复杂,如何可以在整个项目中重用这个片段。在templates
文件夹中,创建一个名为includes
的新文件夹:
scenic_spot/
| -- cityspot/
| | -- migrations/
| | + -- __init__.py
| | -- __init__.py
| | -- apps.py
| | -- models.py
| | -- forms.py
| | -- views.py
| + -- urls.py
| -- static/
| | -- css/
| | -- img/
| | -- js/
| -- templates/
| | -- includes/ <-- 这里
| | + -- form.html <-- 这里
| | -- base.html
| | -- home.html
| | -- new_city.html
| | -- new_spot.html
| | -- city_spots.html
| -- scenic_spot.py
+ -- urls.py
现在在includes
文件夹中,创建一个名为 form.html
的文件:
templates/includes/form.html
{% load widget_tweaks %}
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert">
{% for error in form.non_field_errors %}
<p {% if forloop.last %} class="mb-0"{% endif %}>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
{% for field in form %}
<div class="form-group">
{{ field.label_tag }}
{% if form.is_bound %}
{% if field.errors %}
{% render_field field class="form-control is-invalid" %}
{% for error in field.errors %}
<div class="invalid-feedback">
{{ error }}
</div>
{% endfor %}
{% else %}
{% render_field field class="form-control is-valid" %}
{% endif %}
{% else %}
{% render_field field class="form-control" %}
{% endif %}
{% if field.help_text %}
<small class="form-text text-muted">
{{ field.help_text }}
</small>
{% endif %}
</div>
{% endfor %}
现在来更改 new_city.html
模板:
templates/new_city.html
{% extends 'base.html' %}
{% block title %}开启一个新城市 - {{ block.super }} {% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'cityspot:home' %}">城市列表</a></li>
<li class="breadcrumb-item active">新建城市</li>
{% endblock %}
{% block content %}
<form method="post" novalidate>
{% csrf_token %}
{% include 'includes/form.html' %}
<button type="submit" class="btn btn-success">发布</button>
<a href="{% url 'cityspot:home' %}" class="btn btn-outline-secondary" role="button">取消</a>
</form>
{% endblock %}
顾名思义,{% include %}
模板标签用于在另一个模板中包含 HTML
模板。 这是在项目中重用 HTML
组件的一种非常有用的方法。
实现的下一个新添景点
表单,可以简单地使用 {% include 'includes/form.html' %}
来呈现它。
博文最后更新时间: