forms 组件是 django 提供的一种可以快速校验前端发送的数据的格式以及渲染相关信息和标签的机制,使用 forms 组件可以大大提高开发中的效率。
我们模拟实现一个用户注册信息的例子,来研究一下使用forms组件如何去实现对数据的校验功能。
2.1 普通版 1 2 3 4 5 6 7 8 from django.conf.urls import urlfrom app01 import views urlpatterns = [ url(r'^reg/' , views.reg), ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from django.shortcuts import render, HttpResponsedef reg (request ): if request.method == "POST" : print (request.POST) """ 对接收到数据分别进行校验,比如: 密码长度是不是大于4 电话号码是否是有效的 邮箱格式是否正确等 """ return HttpResponse("OK" ) return render(request, "reg.html" )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <form action ="" method ="post" > {% csrf_token %} <p > 用户名:<input type ="text" name ="name" > </p > <p > 密 码:<input type ="text" name ="pwd" > </p > <p > 确认密码:<input type ="text" name ="r_pwd" > </p > <p > 邮 箱:<input type ="text" name ="email" > </p > <p > 电话号码:<input type ="text" name ="tel" > </p > <p > <input type ="submit" value ="提交" > </p > </form > </body > </html >
我们在后台的视图中要自己编写对应的规则去匹配用户输入的数据是否合法,显然这是十分复杂和困难的,我们可以使用 django 提供的forms机制来实现对数据的校验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 from django.shortcuts import render, HttpResponsefrom django import formsclass UserForm (forms.Form): name = forms.CharField(min_length=4 ) pwd = forms.CharField(min_length=4 ) email = forms.EmailField() def reg (request ): if request.method == "POST" : print (request.POST) name = request.POST.get("name" ) pwd = request.POST.get("pwd" ) r_pwd = request.POST.get("r_pwd" ) email = request.POST.get("email" ) tel = request.POST.get("tel" ) form = UserForm({"name" : name, "pwd" : pwd, "email" : email}) print (form.is_valid()) if form.is_valid(): print (form.cleaned_data) else : print (form.cleaned_data) print (form.errors) return HttpResponse("OK" ) return render(request, "reg.html" )
首先要自定义一个检验的类,该类必须继承 forms.Form,在类中我们可以对相关的字段做一些条件的约束。当获取到前端的数据后,可以根据自定义的校验类来对数据进行校验。使用时需要注意以下几点:
2.2.1 关于自定义校验类中的字段 在传入校验数据的时候,必须要与类中的字段都能匹配上,只能多不能少,否则校验结果为 False
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class UserForm (forms.Form): name = forms.CharField(min_length=4 ) pwd = forms.CharField(min_length=4 ) email = forms.EmailField() form = UserForm({"names" : name, "pwd" : pwd, "email" : email}) form.is_valid() form = UserForm({"name" : name, "pwd" : pwd}) form.is_valid() form = UserForm({"name" : name, "pwd" : pwd, "r_pwd" :pwd, "tel" :tel, "email" : email}) form.is_valid()
2.2.2 关于数据的校验 is_valid() 方法就是自动校验的方法,结果是一个布尔值:
结果为False:校验时字段传入有问题,或者数据不符合规则
结果为True:校验时传入的字段没问题,且数据都符合规则
form.cleaned_data 存放校验通过的字段,是一个字典的数据格式,存放的值是校验通过的数据键值对
1 2 {"name" : “cdchello, "pwd" : "123456" }
form.errors 存放校验失败的内容部分,是一个字典的数据格式,但是跟我们常见的字典类型不一样,字典的键是校验失败的字段,值是一个列表,存放的是错误信息。
1 2 3 4 5 6 7 8 9 10 11 if form.is_valid(): print (form.cleaned_data) else : print (form.cleaned_data) print (form.errors) print (type (form.errors)) print (form.errors.get("name" )) print (type (form.errors.get("name" ))) print (form.errors.get("name" )[0 ])
使用 form.errors.get(“字段值”)[0] 从 form.errors 中获取错误信息。
在上一节中我们提到,我们可以直接将request.POST中获取到的内容进行校验,但是这就要求前端的form表单的name属性值应该与forms组件字段名称一致,如果不一致校验就会失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from django.shortcuts import render, HttpResponsefrom django import formsclass UserForm (forms.Form): name = forms.CharField(min_length=4 ) pwd = forms.CharField(min_length=4 ) email = forms.EmailField() def reg (request ): if request.method == "POST" : print (request.POST) form = UserForm(request.POST) print (form.is_valid()) if form.is_valid(): print (form.cleaned_data) else : print (form.cleaned_data) print (form.errors) return HttpResponse("OK" ) return render(request, "reg.html" )
但是每次要保证前后端的一致性是十分麻烦的,所以 forms 组件还给我们提供了简单渲染标签的功能,会自动在前端根据我们设置的字段名生成对应的input标签,主要有以下三种方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 from django.shortcuts import render, HttpResponsefrom django import formsclass UserForm (forms.Form): name = forms.CharField(min_length=4 ) pwd = forms.CharField(min_length=4 ) r_pwd = forms.CharField(min_length=4 ) email = forms.EmailField() tel = forms.EmailField() def reg (request ): if request.method == "POST" : form = UserForm(request.POST) print (form.is_valid()) if form.is_valid(): print (form.cleaned_data) else : print (form.cleaned_data) print (form.errors) return HttpResponse("OK" ) form = UserForm() return render(request, "reg.html" , locals ())
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <h3 > forms渲染标签1</h3 > <form action ="" method ="post" > {% csrf_token %} <p > 用户名:{{ form.name }}</p > <p > 密 码:{{ form.pwd }}</p > <p > 确认密码:{{ form.r_pwd }}</p > <p > 邮 箱:{{ form.email }}</p > <p > 电话号码:{{ form.tel }}</p > <p > <input type ="submit" value ="提交" > </p > </form > </body > </html >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <h3 > forms渲染标签2</h3 > <form action ="" method ="post" > {% csrf_token %} {% for field in form %} <p > {{ field }}</p > {% endfor %} <p > <input type ="submit" value ="提交" > </p > </form > </body > </html >
以上方式有一个问题,虽然输入狂生成了,但是没有前面的填写信息的提示,forms中在设置字段规则的时候还有一个label属性,我们可以通过设置各个字段 label 的值。如果不设置,默认值为字段名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from django.shortcuts import render, HttpResponsefrom django import formsclass UserForm (forms.Form): name = forms.CharField(min_length=4 , label="用户名" ) pwd = forms.CharField(min_length=4 , label="密码" ) r_pwd = forms.CharField(min_length=4 , label="确认密码" ) email = forms.EmailField(label="邮箱" ) tel = forms.EmailField(label="电话" ) def reg (request ): if request.method == "POST" : form = UserForm(request.POST) print (form.is_valid()) if form.is_valid(): print (form.cleaned_data) else : print (form.cleaned_data) print (form.errors) return HttpResponse("OK" ) form = UserForm() return render(request, "reg.html" , locals ())
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <h3 > forms渲染标签2</h3 > <form action ="" method ="post" > {% csrf_token %} {% for field in form %} <p > <label for ="" > {{ field.label }}</label > {{ field }} </p > {% endfor %} <p > <input type ="submit" value ="提交" > </p > </form > </body > </html >
通过 forms.as_p 属性,不推荐使用,一旦使用这种方式,前端自动生成的标签就固定住了,灵活性差
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <h3 > forms渲染标签3</h3 > <form action ="" method ="post" > {% csrf_token %} {{ form.as_p }} <p > <input type ="submit" value ="提交" > </p > </form > </body > </html >
4.1 渲染错误信息 用户填写相关信息时,如果填写错误,需要给用户提示错误的原因。要实现这个功能,我们首先要对视图函数进行修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from django.shortcuts import render, HttpResponsefrom django import formsdef reg (request ): if request.method == "POST" : form = UserForm(request.POST) print (form.is_valid()) if form.is_valid(): print (form.cleaned_data) else : print (form.cleaned_data) print (form.errors) return render(request, "reg.html" , locals ()) form = UserForm() return render(request, "reg.html" , locals ())
上面的代码中,我们实例化出了两个 forms 对象,但是两者是完全不同的。对于 get 请求的使用的 forms 对象(未绑定数据的forms对象),它的作用主要就是用来渲染生成标签;对于 post 请求中的forms 对象,除了有渲染标签的功能,它还能绑定用户在输入框中输入的数据,确保刷新页面时,数据不丢失。
对于前端,我们只需要在标签后显示对应的错误信息即可,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <h3 > forms渲染标签1</h3 > <form action ="" method ="post" > {% csrf_token %} <p > 用户名:{{ form.name }} <span > {{ form.name.errors.0 }}</span > </p > <p > 密 码:{{ form.pwd }} <span > {{ form.pwd.errors.0 }}</span > </p > <p > 确认密码:{{ form.r_pwd }} <span > {{ form.r_pwd.errors.0 }}</span > </p > <p > 邮 箱:{{ form.email }} <span > {{ form.email.errors.0 }}</span > </p > <p > 电话号码:{{ form.tel }} <span > {{ form.tel.errors.0 }}</span > </p > <p > <input type ="submit" value ="提交" > </p > </form > <hr > </body > </html >
4.2 相关参数的设置 渲染完错误信息后,我们发现提示语都是django内置的内容,我们要如何显示我们自己定义的内容呢?并且前端使用forms组件方式来进行渲染标签,我们想要给标签添加对应的属性又要怎么操作呢?使用forms组件渲染的标签都是text类型的输入标签,怎样设置其他格式的标签呢(单选框等)?这些都可以在定义forms组件时进行设置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from django.shortcuts import render, HttpResponsefrom django import formsfrom django.forms import widgetsclass UserForm (forms.Form): name = forms.CharField(min_length=4 , label="用户名" , error_messages={"required" : "信息不能为空" , "invalid" : "数据格式错误" }, widget=widgets.TextInput()) pwd = forms.CharField(min_length=4 , label="密码" , widget=widgets.PasswordInput(attrs={"class" : "form-control" , "id" : "pwd" })) r_pwd = forms.CharField(min_length=4 , label="确认密码" , widget=widgets.PasswordInput()) email = forms.EmailField(label="邮箱" , widget=widgets.EmailInput()) tel = forms.CharField(label="电话" , widget=widgets.TextInput)
我们在自定义forms组件的类的时候,只能简单的进行一些规则的定制,如果想要实现更加负责的操作,可以使用forms组件提供的钩子机制来完成。由于钩子机制是forms组件内部封装好的,所以在使用前,有必要对forms的源码进行简单的了解,看一下forms的检验机制究竟是怎样实现的。
我们从 forms 的校验函数 form.is_valid() 进入查看:
1 2 3 4 5 6 7 8 def is_valid (self ): """ Returns True if the form has no errors. Otherwise, False. If errors are being ignored, returns False. """ return self .is_bound and not self .errors
该方法最后只返回了两个值,我们需要注意的是第二个返回值,这个 errors 就是存放校验失败内容的字典,我们再查看一下 errors 是怎么实现的
1 2 3 4 5 6 7 8 @property def errors (self ): "Returns an ErrorDict for the data provided for the form" if self ._errors is None : self .full_clean() return self ._errors
在errors内部执行了一个 full_clean() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def full_clean (self ): """ Cleans all of self.data and populates self._errors and self.cleaned_data. """ self ._errors = ErrorDict() if not self .is_bound: return self .cleaned_data = {} if self .empty_permitted and not self .has_changed(): return self ._clean_fields() self ._clean_form() self ._post_clean()
我们需要重点关注的就是 full_clean() 方法中调用的 self._clean_fields() 和 self._clean_form(),这是实现钩子的核心。我们一个一个来看
5.1 局部钩子 局部钩子函数用于实现对某一个字段进行更加复杂的校验操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def _clean_fields (self ): for name, field in self .fields.items(): if field.disabled: value = self .get_initial_for_field(field, name) else : value = field.widget.value_from_datadict(self .data, self .files, self .add_prefix(name)) try : if isinstance (field, FileField): initial = self .get_initial_for_field(field, name) value = field.clean(value, initial) else : value = field.clean(value) self .cleaned_data[name] = value if hasattr (self , 'clean_%s' % name): value = getattr (self , 'clean_%s' % name)() self .cleaned_data[name] = value except ValidationError as e: self .add_error(name, e)
局部钩子的逻辑是这样的,先对数据按照我们在类中定义的规则进行校验,如果校验成功,会把数据存入 cleaned_data 字典中,紧接着,再通过反射去寻找有没有以 clean_ 开头的函数方法,如果有就再去执行,并把刚刚存数据的操作重新覆盖执行一遍;如果校验失败,会引发一个 ValidationError 异常,并把字段和异常存入 error 字典。
因此,我们想要对哪个字段使用钩子函数,只需要在自定义的类中去定义再去定义一个 clean_字段名 的函数即可,把复杂的操作在函数中完成。例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from django.shortcuts import render, HttpResponsefrom django import formsfrom django.forms import widgetsfrom app01.models import Userfrom django.core.exceptions import ValidationErrorclass UserForm (forms.Form): name = forms.CharField(min_length=4 , label="用户名" , error_messages={"required" : "信息不能为空" , "invalid" : "数据格式错误" }, widget=widgets.TextInput()) pwd = forms.CharField(min_length=4 , label="密码" , widget=widgets.PasswordInput(attrs={"class" : "form-control" , "id" : "pwd" })) r_pwd = forms.CharField(min_length=4 , label="确认密码" , widget=widgets.PasswordInput()) email = forms.EmailField(label="邮箱" , widget=widgets.EmailInput()) tel = forms.CharField(label="电话" , widget=widgets.TextInput) def clean_name (self ): val = self .cleaned_data.get("name" ) ret = User.objects.filter (name=val) if not ret: raise val else : return ValidationError("用户名已存在" )
5.2 全局钩子 全局钩子函数用于实现对多个字段间的复杂校验操作
1 2 3 4 5 6 7 8 9 10 def _clean_form (self ): try : cleaned_data = self .clean() except ValidationError as e: self .add_error(None , e) else : if cleaned_data is not None : self .cleaned_data = cleaned_data
对于全局钩子来说,最重要的就是执行了 clean 方法
1 2 3 4 5 6 7 8 def clean (self ): """ Hook for doing any extra form-wide cleaning after Field.clean() has been called on every field. Any ValidationError raised by this method will not be associated with a particular field; it will have a special-case association with the field named '__all__'. """ return self .cleaned_data
clean 方法中只是给了简单的描述,并没有实际的逻辑代码,这就代表着 clean 方法只是开放了一个功能,具体的实现需要我们自己来完成。但是有一点需要注意的是,由于全局操作的是多个字段之间的关系校验,因此如果校验失败,想把错误放到 errors 字典中,键应该填什么呢?clean 方法规定了,在这里键可以填 _all _
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 from django.shortcuts import render, HttpResponsefrom django import formsfrom django.forms import widgetsfrom app01.models import Userfrom django.core.exceptions import ValidationErrorclass UserForm (forms.Form): name = forms.CharField(min_length=4 , label="用户名" , error_messages={"required" : "信息不能为空" , "invalid" : "数据格式错误" }, widget=widgets.TextInput()) pwd = forms.CharField(min_length=4 , label="密码" , widget=widgets.PasswordInput(attrs={"class" : "form-control" , "id" : "pwd" })) r_pwd = forms.CharField(min_length=4 , label="确认密码" , widget=widgets.TextInput()) email = forms.EmailField(label="邮箱" , widget=widgets.EmailInput()) tel = forms.CharField(label="电话" , widget=widgets.TextInput) def clean (self ): pwd = self .cleaned_data.get("pwd" ) r_pwd = self .cleaned_data.get("r_pwd" ) if pwd and r_pwd: if pwd == r_pwd: return self .cleaned_data else : raise ValidationError("两次密码不一致" ) else : return self .cleaned_data def reg (request ): if request.method == "POST" : print (request.POST) form = UserForm(request.POST) print (form.is_valid()) if form.is_valid(): print (form.cleaned_data) else : print (form.cleaned_data) print (form.errors) print (form.errors.get("__all__" )[0 ]) errors = form.errors.get("__all__" ) return render(request, "reg.html" , locals ()) form = UserForm() return render(request, "reg.html" , locals ())
补充:由于forms 组件的功能比较多,我们一般会把forms组件相关的内容单独放到一个py文件中,比如上述操作都属于 app01 模块的forms组件,可以在模块中新建一个 app01_forms.py 的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 from django import formsfrom django.forms import widgetsfrom app01.models import Userfrom django.core.exceptions import ValidationErrorclass UserForm (forms.Form): name = forms.CharField(min_length=4 , label="用户名" , error_messages={"required" : "信息不能为空" , "invalid" : "数据格式错误" }, widget=widgets.TextInput()) pwd = forms.CharField(min_length=4 , label="密码" , widget=widgets.PasswordInput(attrs={"class" : "form-control" , "id" : "pwd" })) r_pwd = forms.CharField(min_length=4 , label="确认密码" , widget=widgets.TextInput()) email = forms.EmailField(label="邮箱" , widget=widgets.EmailInput()) tel = forms.CharField(label="电话" , widget=widgets.TextInput) def clean_name (self ): val = self .cleaned_data.get("name" ) ret = User.objects.filter (name=val) if not ret: return val else : raise ValidationError("用户名已存在" ) def clean (self ): pwd = self .cleaned_data.get("pwd" ) r_pwd = self .cleaned_data.get("r_pwd" ) if pwd and r_pwd: if pwd == r_pwd: return self .cleaned_data else : raise ValidationError("两次密码不一致" ) else : return self .cleaned_data
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from django.shortcuts import render, HttpResponsefrom app01.app01_forms import *def reg (request ): if request.method == "POST" : print (request.POST) form = UserForm(request.POST) print (form.is_valid()) if form.is_valid(): print (form.cleaned_data) else : print (form.cleaned_data) print (form.errors) print (form.errors.get("__all__" )[0 ]) errors = form.errors.get("__all__" ) return render(request, "reg.html" , locals ()) form = UserForm() return render(request, "reg.html" , locals ())