在业务开发中,表单验证是较为重要的一环,经过验证后的数据才能存储进数据库。其中,表单验证不仅包括对恶意数据的检测,还包括了对业务逻辑上的一些检测。
之前笔者对Django Form的源码进行了一些浅显的分析,经过一段时间的使用以后,积累了一些使用的经验因此写下这篇博客来备忘。
关于Django Form源码分析的传送门如下:
Django Form源码分析之BaseForm验证逻辑
Django Form源码分析之Field验证逻辑
使用Django Form进行表单验证
一般来说,笔者在Django CBV中使用Django Form比较喜欢如下的代码流程:
class ExampleView(View):
def get(self, request, *args, **)
form = GetDataForm(data=request.GET)
if form.is_valid():
# run business logic code
else:
# form.error_detail()返回包含数据错误的字典
return JsonResponse(form.error_detail())
def post(self, request, *args, **)
form = PostDataForm(data=request.POST)
if form.is_valid():
# run business logic code
else:
# form.error_detail()返回包含数据错误的字典,自行定义
return JsonResponse(form.error_detail())
这个流程非常简单清晰,将根据不同HTTP方法传输过来的数据作为参数实例化一个DataForm,调用is_valid方法验证数据,如果数据验证准确,则可以运行业务逻辑,如果数据验证错误,需要以JSON格式返回错误信息给前端。
PS:笔者比较倾向于将业务逻辑绑定在具体的Form上面,作为一个Form的方法来实现,因为业务逻辑是跟传输过来的数据是紧耦合的,在业务逻辑中可以直接通过self.cleaned_data直接获得相关数据。在StackOverFlow还有关于业务逻辑代码安排在哪里的讨论,有兴趣的同学可以去看一下:Separation of business logic and data access in django
构造业务的BaseForm
在开发的时候,后端通常需要给出前端当表单验证错误的时候错误信息的数据格式。
举个例子,假设有一个用户表单对提交的两个数据进行验证:
# Form.py
from django import forms
class UserForm(forms.Form):
name = forms.CharField()
age = forms.IntegerField()
然后返回的JSON数据格式要求如下:
# Json Data Format
{
"status": 2,
"msg": "form validate error",
"data": {
'name': ['This field is required.'],
'age': ['Enter a number.']
}
}
很显然,Form.errors字典不能满足这种数据格式的需求。
所以需要开发者自行定义一个作为基类的BaseForm,并在BaseForm中定义error_detail方法,相当于对Form.erros的信息再进行一次封装并且按照规定格式返回。
所有的业务流程中定义的Form都应该继承于这一个BaseForm而并非django.forms.Form。
from django import forms
class BaseForm(forms.Form):
def error_detail(self):
error_response = {}
error_response['status'] = 2
error_response['msg'] = 'form validate error'
error_response['data'] = self.errors
return error_response
对Form.errors属性不熟悉的同学可以参考Django官方文档或者笔者之前写的对BaseForm的源码分析。
>>> from User.forms import UserForm
>>> not_valid_data = dict(name='more than 5 characters', age='')
>>> form = UserForm(data=not_valid_data)
>>> form.is_valid()
False
>>> response = form.error_detail()
>>> response
{'msg': 'form validate error', 'status': 1001, 'data': {'name': ['Ensure this value has at most 5 characters (it has 22).'], 'age': ['This field is required.']}}
数据的层次验证
Django Form提供了许多钩子供开发者做不同层次的验证,在Form.full_clean()方法中主要包括了三个层次的验证,并且这三个验证依次进行:
- Form._clean_fields() 字段层次验证
- Form._clean_form() 表单层次验证
- Form._post_clean() ModelForm的额外验证
字段层次验证
字段层次的验证包括四个验证(前三个步骤在Field.clean中调用,最后一个步骤在Form._clean_fields里面调用),这四个步骤也是依次进行,但是一旦其中一个步骤抛出ValidationError异常,步骤终止,并被Form._clean_fields()捕捉该异常:
- Field.to_python() 转换数据为Python对象
- Field.validate() 检验数据是否为空值,除非允许该数据为空值
- Field.run_validators() 运行该Field上被设置的validators,Django提供的扩展field大多在这里添加验证逻辑
- Form.clean_{field_name} Django提供的钩子供开发者自行定义字段上的逻辑验证,比如业务逻辑
表单层次验证
表单层次的验证通常用于需要多个字段关联的数据验证(比如业务逻辑),实现方式非常简单,直接重写父类的Form.clean()方法即可。
在进行表单层次验证之前,已经经过了单个字段层次的验证,但是如果字段层次验证出现了错误,并不会阻止Form.clean()的执行。(有兴趣的同学看一下Form.full_clean方法的源码就知道了)
一般来说,如果字段层次验证错误,表单层次的验证应该不再执行,所以这里需要开发者留心一下。
举个简单的例子,仍以UserForm为例,假设如果用户的名字是young,则他的年龄不得超过20岁(谜之业务需求。。。)
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
class UserForm(BaseForm):
name = forms.CharField()
age = forms.IntegerField()
def clean(self):
# 通过errors属性判定字段层次是否有错误,如果有则跳过表单层次验证
if not self.errors:
name = self.cleaned_data.get('name')
age = self.cleaned_data.get('age')
# 进行多个字段关联的数据验证
if name == 'young' and age > 20:
raise ValidationError(_("you are too old."), code='old')
return self.cleaned_data
面向开发和面向用户的错误信息
当数据验证错误之后,后端需要返回一定格式的错误信息,这些错误信息一部分是直接向用户展示,一部分是向前端展示方便调试开发。
假设一个应用场景,修改一个课程的Model实例,用户提交必要的字段经过表单验证以后存入Model中:
# models.py
from django.db import models
# Create your models here.
NAME_LENGTH_LIMIT = 50
INTRO_LENGTH_LIMIT = 200
class Courses(models.Model):
name = models.CharField(max_length=NAME_LENGTH_LIMIT)
intro = models.CharField(max_length=INTRO_LENGTH_LIMIT)
前端需要发送三个字段来修改一个Courses的Model实例,分别是课程的id,课程的名字,课程的介绍。笔者根据这三个字段给出检验数据的Form的实现。
# constants.py
SUCCESS = 0
FIELD_FORMAT_ERROR = 1
COURSES_NOT_EXIST = 2
error_message = {
0: "操作成功",
1: 'Field格式错误',
2: '课程不存在',
}
# forms.py
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from Courses.models import Courses
from Courses.models import INTRO_LENGTH_LIMIT, NAME_LENGTH_LIMIT
from Courses.constants import error_message
from Courses.constants import COURSES_NOT_EXIST, FIELD_FORMAT_ERROR
class CoursesInfoChangeForm(BaseForm):
courses_id = forms.IntegerField()
name = forms.CharField(max_length=NAME_LENGTH_LIMIT, error_messages={'max_length': '名字长度不得超过50字'})
intro = forms.CharField(max_length=INTRO_LENGTH_LIMIT, error_messages={'max_length': '介绍长度不得超过200字'})
def clean_courses_id(self):
# field validation of business logic
courses_id = self.cleaned_data.get('courses_id')
try:
Courses.objects.get(pk=courses_id)
except Courses.DoesNotExist:
raise ValidationError(_(error_message[COURSES_NOT_EXIST]), code=COURSES_NOT_EXIST)
return courses_id
def clean(self):
if not self.errors:
# Do your form validation of business logic
pass
return self.cleaned_data
- courses_id字段上的数据验证可以视为是业务逻辑上的验证,如果在clean_courses_id中发生了ValidationError可以直接把信息展示给用户。
- name,intro字段上的数据验证,比如超过最大长度,这些错误信息应该作为前端调试和开发使用,不能直接展示给用户。
为了满足上面的需求,需要通过构造BaseForm来自定义返回的Json数据格式,规定Json格式如下:
# Json Data Error Example
{
"status": 1,
"msg": "field格式错误",
'data': {
'intro': ['介绍长度不得超过200字'],
'name': ['名字长度不得超过50字']
}
# Json Data Success Example
{
"status": 0,
"msg": "操作成功",
"data": {}
}
- 当status为1时表示Field格式错误,此时后端在data字段中给出字段的错误信息供前端调试和开发。
- 当status不为1时,则表示业务逻辑上的验证(比如要修改的课程的id不存在),此时前端直接抓去msg信息展示给用户。
BaseForm源码实现如下:
# forms.py
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from Courses.models import Courses
from Courses.models import INTRO_LENGTH_LIMIT, NAME_LENGTH_LIMIT
from Courses.constants import error_message
from Courses.constants import COURSES_NOT_EXIST, FIELD_FORMAT_ERROR
class BaseForm(forms.Form):
def error_detail(self):
error_response = {}
# only return the first validation error
validation_error_list = list(self.errors.as_data().values())[0]
validation_error = validation_error_list[0]
# if error is not about business logic, show it to the frontend
# else show it to the user
if validation_error.code not in error_message.keys():
error_response['status'] = FIELD_FORMAT_ERROR
error_response['msg'] = error_message[FIELD_FORMAT_ERROR]
error_response['data'] = self.errors
else:
error_response['status'] = validation_error.code
error_response['msg'] = error_message[validation_error.code]
error_response['data'] = {}
return error_response
在constants.py中建立一个含有所有表示业务逻辑状态码的字典,通过对一个ValidationError实例中的code属性是否包含在字典中进行判断,该错误信息应该展示给用户还是展示给前端。(只需要返回第一个错误即可)
代码测试结果:
# tests.py
def generate_string(length):
s = ''
for i in range(length):
s += 'a'
return s
# field error: name length overflow
not_valid_data1 = {
'courses_id': 1,
'name': generate_string(51),
'intro': generate_string(201)
}
# business logic: courses_id not exist
not_valid_data2 = {
'courses_id': 2,
'name': 'less than 50 characters',
'intro': 'less than 200 characters'
}
# Django manage.py shell
>>> from Courses.models import Courses
>>> Courses.objects.create(name='courses1', intro='courses1 introduction')
<Courses: Courses object>
>>> from Courses.forms import CoursesInfoChangeForm
>>> from Courses.tests import not_valid_data1, not_valid_data2
>>> first_form = CoursesInfoChangeForm(data=not_valid_data1)
>>> first_form.is_valid()
False
>>> first_form.error_detail()
{'msg': 'Field格式错误', 'status': 1, 'data': {'intro': ['介绍长度不得超过200字'], 'name': ['名字长度不得超过50字']}}
>>> second_form = CoursesInfoChangeForm(data=not_valid_data2)
>>> second_form.is_valid()
False
>>> second_form.error_detail()
{'msg': '课程不存在', 'status': 2, 'data': {}}
github传送门:github.com/9394974/lea…
以上是笔者使用Django Form的一点小小经验,有不足之处敬请指教。