Python自动化运维 - Django(四)FBV/CBV-序列化-Form表单
1 FBV & CBV
FBV和CBV是Django 提供的两种编程方式。
- FBV:Functions Basic View,在视图里使用函数处理请求
- CBV:Class Basic View,在视图里使用类处理请求。
FBV就是我们前面一直介绍使用的,那么CBV该如何使用
CBV的使用
在views视图函数中定义class来接受请求:
from django.shortcuts import render from django.views import View # 注意这里必须要引入View类 # Create your views here. class Index(View): def get(self,request): return render(request,'index.html')
注意:Views类中包含了很多类的调用方法,以及对请求数据的处理,所以这里需要引入并继承View类
定义urls接受请求:
from django.conf.urls import url from django.contrib import admin from app01 import views urlpatterns = [ url(r'^index/', views.Index.as_view()), ] # 通过调用类的as_view()方法来 转发请求进行处理,所以我们不继承View类,是没有这个方法的。
CBV模式下是如何处理请求的
相信很多人会有疑问,FBV模式下,请求会被WSGI封装成一个request直接交给函数进行处理了,那么CBV模式下呢?
这里主要会经过几个步骤:
- 当一个请求转发至我们定义的类时,首先执行的是类的dispath方法。
- 子类中没有定义dispath方法,那么就会去父类中寻找该方法(View)
- 该方法中通过截取request.method中请求方式,通过反射(getattr)在我们定义的类中去查找对应的方法。
- 执行对应的方法,然后返回数据给用户
1 def dispatch(self, request, *args, **kwargs): 2 # Try to dispatch to the right method; if a method doesn't exist, 3 # defer to the error handler. Also defer to the error handler if the 4 # request method isn't on the approved list. 5 if request.method.lower() in self.http_method_names: 6 handler = getattr(self, request.method.lower(), self.http_method_not_allowed) # 获取方法的小写形式,然后通过反射的方式进行返回 7 else: 8 handler = self.http_method_not_allowed 9 return handler(request, *args, **kwargs)
注意:
这里我们定义的get/post等函数必须是小写,因为django会通过反射(getattr)的方式找到 class 的函数, 可以查看Views的dispath函数,其找的时候是先把用户的请求类型变成小写,然后才进行查找的。
所以:
- 请求先通过父类的dispath函数,然在去子类中利用反射查找get和post 或者其他请求方法对应的函数。
- 如果子类定义了dispath函数,如果直接返回数据,那么class 的get 和 post 将永远不会执行。
- 如果要定义dispath,那么需要调用父类的方法 然后增加额外的功能:
1 def dispath(self,request,*args,**kwargs) 2 3 print('before') 4 result=super(Home,self).dispath(request,*args,**kwargs) # 父类的dispath执行结果其实就是调用了子类的 get/post方法,然后把结果进行返回,这里的result其实就是子类中 get/post 返回的结果 5 print('after') 6 7 return result # 把结果再赶回给用户
补充流程:
- 执行父类的dispath方法,这时父类中的self 其实就是子类实例化的对象,然后通过反射执行实例化对象的 get/post方法,然后把结果进行返回
- 接受get/post 执行的结果,然后返回。
- 其实是在父类中就直接调用了 子类的get/post方法。
CBV模式下的用户cookie验证实例
针对CBV模式下的用户cookie验证,利用所学的知识,我们可以用三种方式来编写:1、装饰器。2、类的单继承。3、类的多继承。
由于已经知道了dispath函数的功能,所以我们可以定制化dispath,进行类继承来完成验证cookie的验证。
装饰器方式
定义装饰器对类进行cookie验证
from django.shortcuts import render,HttpResponse,redirect from django.views import View from django.utils.decorators import method_decorator def auth(func): def inner(request,*args,**kwargs): if request.COOKIES.get('username'): return func(request,*args,**kwargs) else: return redirect('/login/') return inner @method_decorator(auth,name='get') class Index(View): def get(self,request): return render(request,'index.html') class Login(View): def get(self,request): return render(request,'login.html') def post(self,request): username = request.POST.get('username') password = request.POST.get('password') print(username,password) if username == 'daxin' and password == '123123': response = redirect('/index/') response.set_cookie('username',username) return response else: return redirect('/login/')
解读:
1、在该中模式下django使用method_decorator的方式来装饰类或者类的方法。
2、首先需要引入,from django.utils.decorators import method_decorator
3、使用方式有两种,如果直接作用在类上,需要指定装饰的方法 @method_decorator(auth,name='get'),这里name指定的就是方法。这里是get方法
4、作用在类的某个方法上,则不需要传递name变量
1 class Hello(View): 2 3 @method_decorator(auth) 4 def get(self,request): 5 6 return render(request,'hello.html')
单继承的方式
定义基类修改dispath函数来对cookie进行验证
from django.shortcuts import render,HttpResponse,redirect from django.views import View class BaseAuth(View): def dispatch(self, request, *args, **kwargs): if request.COOKIES.get('username'): return super(BaseAuth, self).dispatch(request, *args, **kwargs) else: return redirect('/login/') class Index(BaseAuth): def get(self,request): return render(request,'index.html') class Hello(BaseAuth): def get(self,request): return render(request,'hello.html') class Login(View): def get(self,request): return render(request,'login.html') def post(self,request): username = request.POST.get('username') password = request.POST.get('password') print(username,password) if username == 'daxin' and password == '123123': response = redirect('/index/') response.set_cookie('username',username) return response else: return redirect('/login/')
过程:
1、自定义类,用于添加认证
2、子类继承定义的类,这样执行dispath会在我们自定义的类中寻找。
3、而我们自定义的类中,显示的调用了父类中的dispath,又额外添加了对session的验证。
4、其他页面只需要继承我们自定义的类即可完成session验证。
5、注意这里的 在 View 中的 self 是请求的视图,最后还是会执行我们定义类的get/post方法。
多继承的方式
多继承的方式,其实和单继承是一样的,只不过我们定义的基类继承object,子类进行多继承,先继承我们自定义的类(添加cookie认证功能),再继承Views类
1 class BaseAuth(object): 2 3 def dispatch(self, request, *args, **kwargs): 4 5 if request.COOKIES.get('username'): 6 7 return super(BaseAuth, self).dispatch(request, *args, **kwargs) 8 9 else: 10 return redirect('/login/') 11 12 # @method_decorator(auth,name='get') 13 class Index(BaseAuth,View): 14 15 def get(self,request): 16 17 return render(request,'index.html')
过程与单继承类似,需要注意的是,我们自己写的类,就是普通的类,继承object对象,并且写在,子类的最左边(继承的顺序),虽然我们定义的类 没有dispath 方法,但是 在 子类中的super函数会在 其继承的所有父类中寻找,所以我们需要在后面继续继承View。
CSRF的坑
在类中使用csrf装饰器,来排除csrf或者使用csrf的认证的时候。
只能在dispath中进行装饰才会生效!如果我们在类中没有定义dispath,需要定义(就算引用父类的dispath也可以)才可以使用
1 from django.shortcuts import render,HttpResponse,redirect 2 from django.views import View 3 from django.utils.decorators import method_decorator 4 from django.views.decorators.csrf import csrf_exempt,csrf_protect 5 6 class Login(View): 7 8 @method_decorator(csrf_exempt) 9 def dispatch(self, request, *args, **kwargs): 10 return super(Login, self).dispatch(request, *args, **kwargs) 11 12 13 def get(self,request): 14 15 return render(request,'login.html') 16 17 def post(self,request): 18 19 username = request.POST.get('username') 20 password = request.POST.get('password') 21 print(username,password) 22 23 if username == 'daxin' and password == '123123': 24 response = redirect('/index/') 25 response.set_cookie('username',username) 26 27 return response 28 else: 29 return redirect('/login/')
2 序列化
当后台返回数据给ajax的时候,我们可以返回序列化的数据给前端进行展示,那么问题来了。如果我要返回的是QuerySet对象呢?JS里面可并没有这个数据类型啊。Python的json.dumps也不能转换QuerySet类型呢,这该怎么办呢?
针对这种需求有两种方式
- 利用django提供的序列化工具serializers
- 自定义序列化
序列化工具serializers
利用django提供的序列化工具serializers对QuerySet数据类型进行序列化
from django.shortcuts import render,HttpResponse,redirect from django.views import View from app01 import models from django.core import serializers # Create your views here. class User(View): def get(self,request): return render(request,'userinfo.html') def post(self,request): user_list =models.UserInfo.objects.all() data = serializers.serialize('json',user_list) return HttpResponse(data)
序列化前的数据格式为:
<QuerySet [<UserInfo: UserInfo object>, <UserInfo: UserInfo object>]>
序列化后的格式为:
[{"model": "app01.userinfo", "pk": 1, "fields": {"username": "dachenzi", "password": "123456", "email": "123@abc.com", "role": 1, "bs": [1, 2]}}, {"model": "app01.userinfo", "pk": 2, "fields": {"username": "daxin", "password": "123456", "email": "123@abv.co", "role": 2, "bs": []}}]
可以看到serializers.serialize,会把QuerySet,以及数据对象转换成对应的数据。
其中:
- model表示表的名称
- pk表示主键
- fields 表示的是字段,后面的字典,是有字段的名称和数据组成。
由于每一条明细都包含表名等其他数据,并且还不能定制,所以在数据量大的时候不推荐使用。
自定义序列化
经过观察发现,json序列化的数据很多情况下是 列表嵌套字典,那么我们能否把返回的数据变成json支持的格式进行dumps呢?答案是可以的
通过数据库获取的数据是这样的:
<QuerySet [<UserInfo: UserInfo object>, <UserInfo: UserInfo object>]>
内部是一个个的userinfo对象,那么如何获取对象的数据呢,还记得values吗?
user_list =models.UserInfo.objects.values('id','username')
这里通过values获取到的数据格式如下:
<QuerySet [{'id': 1, 'username': 'dachenzi'}, {'id': 2, 'username': 'daxin'}]>
这样看起来很像了,但是数据类型是QuerySet,那么接下来我们只需要把QuerySet使用list()转换成列表即可
完整的代码如下:
1 from django.shortcuts import render,HttpResponse,redirect 2 from django.views import View 3 from app01 import models 4 from django.core import serializers 5 import json 6 # Create your views here. 7 8 9 10 class User(View): 11 12 def get(self,request): 13 14 return render(request,'userinfo.html') 15 16 17 def post(self,request): 18 19 user_list =models.UserInfo.objects.all().values('id','username') 20 data = list(user_list) 21 22 return HttpResponse(json.dumps(data))
前端展示代码如下:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body> 8 9 <h1>用户列表</h1> 10 <p><button id="upload">点击获取用户列表</button></p> 11 <ul id = 'user'> 12 13 </ul> 14 15 <script src="/static/jquery-3.2.1.min.js"></script> 16 <script> 17 $('#upload').click(function () { 18 $.ajax({ 19 url:'/user/', 20 type:'post', 21 dataType:'json', /* 把对象转换为JSON对象 */ 22 data:{'csrfmiddlewaretoken':'{{ csrf_token }}'}, 23 success:function (data) { 24 $.each(data,function (k,v) { /* 对返回的字典进行循环 */ 25 var tag = document.createElement('li'); /* 创建li标签 */ 26 tag.textContent = v.username; /* 对标签进行赋值 */ 27 $('#user').append(tag); /* 把标签添加到页面上去 */ 28 }) 29 30 } 31 }) 32 }) 33 </script> 34 </body> 35 </html>
如果就这么简单,我还说个屁啊。当数据库字段类型为datetime的时候,会出现神奇的问题哦
user_list =models.UserInfo.objects.all().values('id','username','time') data = list(user_list) print(data)
结果为:
[{'time': datetime.datetime(2015, 2, 21, 12, 12, 21, tzinfo=<UTC>), 'username': 'dachenzi', 'id': 1}, {'time': datetime.datetime(2015, 2, 21, 12, 12, 21, tzinfo=<UTC>), 'username': 'daxin', 'id': 2}]
时间格式被默认转换格式了。(当然这里用serializers也可以,但是输出的时间格式会变成类似:"2015-02-21T12:12:21Z",为了方便起见,这里也不使用)
虽然输出的不对,但是是否可以用json.dumps进行序列化吗? 当然是不可以的。会提示:
datetime.datetime(2015, 2, 21, 12, 12, 21, tzinfo=<UTC>) is not JSON serializable
它不支持这个格式。那么怎么办呢,这里引入JsonCustomEncoder类
什么是JSONEncoder
json.dumps在对数据进序列化的时候,会默认帮我们传递一个类
json.dumps(obj,cls=None) # 当cls等于None的时候,会把JSONEncoder赋值给cls
PS:JSONEncoder定义了数据序列的方式。而JSONEncoder中的default函数对传入的数据的类型进行了检查,并对异常进行了上报。不能序列化的原因是json.JSONEncode 中没有支持对 datetime字段的序列化
这里我们改写default函数,添加对datetime类型的数据进行序列化。
import json import datetime class JsonBaseEncoder(json.JSONEncoder): def default(self, o): if isinstance(o,datetime): return o.strftime('%Y-%m-%d %H:%M:%S') elif isinstance(o,date): return field.strftime('%Y-%m-%d') else: return json.JSONEncoder.default(self,o) # 序列化的方式是每次读入一个字段然后进行序列化,这里的o就代表读入的字段 # 对读取的datetime字段的数据进行格式化成字符串,这样json.dumps就能按照字符串的序列化规则进行序列化。 # 最后注意引用默认的 json.JSONEncoder.default(self,o),对其他我们未定义的类型,继续交给 json.JSONEncoder.default(self,o) 进行处理
在进行json.dumps的时候把该类传入即可
return HttpResponse(json.dumps(data,cls=JsonBaseEncoder))
前端收到的数据就变成了如下所示了,可以进行循环处理了。
[{id: 1, username: "dachenzi", time: "2015-02-21 12:12:21"}, {id: 2, username: "daxin", time: "2015-02-21 12:12:21"}]
3 Form表单验证
什么是form?django 中的 form 组件 专门用来做对用户通过form表单形式提交的数据的格式进行验证的,还可以提供诸如form表单生成等牛逼的功能。
使用方式为:首先我们定义一个Form模版(可以理解为是匹配数据的模版),其中对字段进行了规范。接下来请求发过来后(request.POST),我们把request.POST的数据交给form模版,进行验证,form模版验证完成后会产出三个信息。
- 是否验证成功
- 所有的正确信息
- 所有的错误信息
创建一个Form表单对象
使用From模版和model一样,是一个类,使用时需要引入django的form组件,然后继承Form类来获得一些功能。(可以定义在models.py中)
# 基本使用 from django.forms import Form from django.forms import fields class FM(Form): username = fields.CharField() # 创建一个验证对象,用于验证用户提交的数据中的name属性为username的数据 password = fields.CharField() email = forms.EmailField()
注意: 这里定义的Form字段名称 必须和 前端form表单中的 input 框的 name 属性的值一致,才能进行验证。
利用Form表单对象进行验证
当用户使用POST方法提交数据的时候,应该首先交给Form模版进行验证,这时Form模版会返回三个对象:
- 验证的结果:formobj.as_valid()
- 验证成功的数据:formobj.cleaned_data
- 验证失败的数据:formobj.errors
使用form对象,那么需要实例化,在实例化时,传入需要验证的数据,这里传入request.POST
def post(self,request): obj = FM(data=request.POST) # 创建一个FM与用户提交数据的一个对象 print(obj.is_valid()) # 对这个对象进行验证,匹配成功会返回True,否则会返回False print(obj.cleaned_data) # 存放匹配成功的数据 print(obj.errors.as_json()) # 存放错误的数据(默认情况下会输出一个字符串,包含了HTML标签,我们还可以使用Form提供的as_json方法对输出的字符串进行格式化显示)
- data参数:表示要验证的数据
- Initial参数(可选):表示的是初始化数据(数据类型为字典嵌套元组)。
1 class Login(View): 2 3 def get(self,request): 4 5 if request.GET.get('uid',None): # 判断用户是否提交数据 6 7 userid = request.GET.get('uid') # 获取用户请求的数据 8 9 userinfo = models.UserInfo.objects.filter(id=userid).first() # 在数据库中找到对应的数据 10 11 obj = FM(initial={'username':userinfo.username,'password':userinfo.password,'email':userinfo.email}) # 对Form表单进行初始化赋值 12 13 else: 14 15 obj = FM() # 否则不进行初始化 16 17 return render(request,'login.html',{'data':obj})
formobj.as_valid()的返回结果为:
True/False
formobj.cleaned_data的返回结果为:
# 验证成功会存放通过验证的数据 {'password': 'asasas', 'username': 'asas', 'email': 'asas@abc.com'}
formobj.errors的结果为:
# 默认的输出格式
<ul class="errorlist"><li>pwd<ul class="errorlist"><li>This field is required.</li></ul></li><li>email<ul class="errorlist"><li>This field is required.</li></ul></li><li>user<ul class="errorlist"><li>This field is required.</li></ul></li></ul> # 可以使用 as_json()格式化为字典 {"email": [{"message": "This field is required.", "code": "required"}], "pwd": [{"message": "This field is required.", "code": "required"}], "user": [{"message": "This field is required.", "code": "required"}]}
由于前端提交了空的数据,所以这里message表示错误提示信息, This field is required.(这个字段不能为空),code 表示的错误代码,required 表示必须的(必填项)。
有些同学会说,django返回的是英文的,我看不懂,那么其实我们可以自己进行定义,即针对code字段的类型进行自定义。(在error_message字典中,根据code的类型,定义不同的提示信息即可)
class FM(Form): username = fields.CharField(error_messages={'required':'用户名不能为空'}) password = fields.CharField(max_length=12,min_length=6,error_messages={'required':'密码不能为空','min_length':'密码长度不能小于6位','max_length':'密码的长度不能超过12位'}) email = fields.EmailField(error_messages={'required':'邮箱不能为空','invalid':'邮箱格式错误'})
这时,如果错误代码等于我们定义的,那么就会输出对应的错误信息。
扩展:由于formobj.erros中包含了所有的错误信息,那么我们可以把它传递到前端,然后再进行取值
1 def post(self,request): 2 print(request.POST) 3 obj = FM(data=request.POST) # 创建一个FM 4 return render(request,'login.html',{'data':obj.errors})
<form id = 'form' method="post"> {% csrf_token %} <h1>Hello 用户登陆</h1> <p>用户名: <input type="text" name="username"> {{ data.username.0 }}</p> <p> 密码:<input type="text" name="password"> {{ data.password.0 }} </p> <p> 邮箱:<input type="text" name="email"> {{ data.email.0 }} </p> <input type="submit" value="提交"> </form>
注意:这里用.0 取数据是因为 错误信息 message,其实是个列表,会存放所有的错误信息,对于空的请求,只会返回 非空的 错误信息,但是数据类型也是列表,所以这里取第一个。
点击提交后,用户输入正确的数据,会消失,这不是我们想要的,并且form标签看起来也很乱,有没有一种方式直接来生成呢,各位看官,请继续往下看。
利用Form对象手动生成前端标签
前面可以看到,我们创建FM类的时候,指定了三个Form字段,那么我们可以利用这三个Form字段来生成对应的input输入框。(Form字段其实本身不具有生成HTML标签的功能,它只有验证的功能,真正生成HTML标签的操作,是由Form字段类型(CharField)中的widgets属性定义了textinput插件完成的,详细信息,可以查看widgets的原码,继续了解)
{{ Form对象.字段名 }} # 前端渲染时会渲染出对应的HTML标签
对例子进行改写:
# get方式要把FM对象传递给前端,否则无法生成HTML def get(self,request): obj = FM() return render(request,'login.html',{'data':obj}) # 前端调用 <form method="post" novalidate> {% csrf_token %} <p>{{ data.username }} {{ data.errors.username.0 }}</p> <p>{{ data.password }} {{ data.errors.password.0 }}</p> <p>{{ data.email }} {{ data.errors.email.0 }}</p> <input type="submit" value="提交"> </form> # 默认情况下高版本浏览器会对错误提示进行小窗提示,这里的为了避免浏览器这个行为,所以给form表单添加 novalidate 属性。
利用Form对象自动生成前端标签
Form提供了三种自动生成前端标签的方式:as_p,as_ul,as_table,由于是自动生成的,所以灵活性对比手动,要稍差点。
as_p:生成的标签为p类型
as_ul:生成的标签格ul类型
as_table:生成的标签为tbody类型(注意,需要手动添加外层的<table></table>标签)
<!-- 1 as_table --> <form method="post" novalidate> {% csrf_token %} <table> {{ data.as_table }} </table> <input type="submit" value="提交"> </form> # 注意 as_table 的方式需要我们手动的添加table标签 <!-- 2 as_p --> <form method="post" novalidate> {% csrf_token %} {{ data.as_p }} <input type="submit" value="提交"> </form> <!-- 2 as_ul --> <form method="post" novalidate> {% csrf_token %} {{ data.as_ul }} <input type="submit" value="提交"> </form>
注意:as_p,as_ul,as_table这种自动生成标签的方式来自于我们创建的FM对象,所以如果要让Form自动创建,那么需要在get请求模式下把Form对象,传递给Templates,才能引用
1 def get(self,request): 2 obj = FM() 3 return render(request,'login.html',{'data':obj})
使用以上两种方式:用户的输入的正确数据都会被保留。是不是很屌。不屌,因为只能输出input标签,还不能定制HTML标签的属性,真是太low了。No、No、No 你想到的,开发人员早就已经想到了。
Form字段的widget属性
由于前面已经说过,生成的标签类型是由widgets来控制的,那么我们就可以对其进行定制,生成我们想要的标签类型,并给标签添加属性信息。
from django.forms import widgets #导入插件库 class FM(Form): username = fields.CharField( error_messages={'required':'用户名不能为空'}, widget=widgets.Textarea(attrs={'class':'c1'}) # 设置字段的类型为Textarea,并且添加class属性的值为cl ) password = fields.CharField( max_length=12, min_length=6, error_messages={'required':'密码不能为空','min_length':'密码长度不能小于6位','max_length':'密码的长度不能超过12位'}, widget=widgets.PasswordInput(attrs={'class':'c1'}) # 设置字段类型为Password类型,添加class属性为c1 )
这里我们修改了username字段的 标签格式,输出的就是一个textarea标签,并且利用了attr参数给标签添加了 class 属性(多个属性可以在attr中继续添加),这个插件里面几乎包含了所有的输入框比如input、radio、select、等等。
Form字段其他的属性
一大波属性来袭:
Field required=True, 是否允许为空 widget=None, HTML插件 label=None, 用于生成Label标签或显示内容 initial=None, 初始值 help_text='', 帮助信息(在标签旁边显示) error_messages=None, 错误信息 {'required': '不能为空', 'invalid': '格式错误'} show_hidden_initial=False, 是否在当前插件后面再加一个隐藏的且具有默认值的插件(可用于检验两次输入是否一直) validators=[], 自定义验证规则 localize=False, 是否支持本地化(比如时区,在存储时区数据时,会匹配本地时区,不常用) disabled=False, 是否可以编辑 label_suffix=None Label内容后缀(比如在前端使用formobj.as_table生成表单时,label和input中间的分隔符,默认是冒号,不常用) CharField(Field) max_length=None, 最大长度 min_length=None, 最小长度 strip=True 是否移除用户输入空白 IntegerField(Field) max_value=None, 最大值 min_value=None, 最小值 FloatField(IntegerField) ... DecimalField(IntegerField) max_value=None, 最大值 min_value=None, 最小值 max_digits=None, 总长度 decimal_places=None, 小数位长度 BaseTemporalField(Field) input_formats=None 时间格式化 DateField(BaseTemporalField) 格式:2015-09-01 TimeField(BaseTemporalField) 格式:11:12 DateTimeField(BaseTemporalField)格式:2015-09-01 11:12 DurationField(Field) 时间间隔:%d %H:%M:%S.%f ... RegexField(CharField) regex, 自定制正则表达式 max_length=None, 最大长度 min_length=None, 最小长度 error_message=None, 忽略,错误信息使用 error_messages={'invalid': '...'} EmailField(CharField) ... FileField(Field) allow_empty_file=False 是否允许空文件 ImageField(FileField) ... 注:需要PIL模块,pip3 install Pillow 以上两个字典使用时,需要注意两点: - form表单中 enctype="multipart/form-data" - view函数中 obj = MyForm(request.POST, request.FILES) URLField(Field) ... BooleanField(Field) ... NullBooleanField(BooleanField) ... ChoiceField(Field) ... choices=(), 选项,如:choices = ((0,'上海'),(1,'北京'),) required=True, 是否必填 widget=None, 插件,默认select插件 label=None, Label内容 initial=None, 初始值 help_text='', 帮助提示 ModelChoiceField(ChoiceField) ... django.forms.models.ModelChoiceField queryset, # 查询数据库中的数据 empty_label="---------", # 默认空显示内容 to_field_name=None, # HTML中value的值对应的字段 limit_choices_to=None # ModelForm中对queryset二次筛选 ModelMultipleChoiceField(ModelChoiceField) ... django.forms.models.ModelMultipleChoiceField TypedChoiceField(ChoiceField) coerce = lambda val: val 对选中的值进行一次转换 empty_value= '' 空值的默认值 MultipleChoiceField(ChoiceField) ... TypedMultipleChoiceField(MultipleChoiceField) coerce = lambda val: val 对选中的每一个值进行一次转换 empty_value= '' 空值的默认值 ComboField(Field) fields=() 使用多个验证,如下:即验证最大长度20,又验证邮箱格式 fields.ComboField(fields=[fields.CharField(max_length=20), fields.EmailField(),]) MultiValueField(Field) PS: 抽象类,子类中可以实现聚合多个字典去匹配一个值,要配合MultiWidget使用 SplitDateTimeField(MultiValueField) input_date_formats=None, 格式列表:['%Y--%m--%d', '%m%d/%Y', '%m/%d/%y'] input_time_formats=None 格式列表:['%H:%M:%S', '%H:%M:%S.%f', '%H:%M'] FilePathField(ChoiceField) 文件选项,目录下文件显示在页面中(下拉框显示path指定目录下的文件),提交时 提交的是 文件的路径 path, 文件夹路径 match=None, 正则匹配 recursive=False, 递归下面的文件夹 allow_files=True, 允许文件 allow_folders=False, 允许文件夹 required=True, widget=None, label=None, initial=None, help_text='' GenericIPAddressField protocol='both', both,ipv4,ipv6支持的IP格式 unpack_ipv4=False 解析ipv4地址,如果是::ffff:192.0.2.1时候,可解析为192.0.2.1, PS:protocol必须为both才能启用 SlugField(CharField) 数字,字母,下划线,减号(连字符) ... UUIDField(CharField) uuid类型 ... Django字段属性
1 TextInput(Input) 2 NumberInput(TextInput) 3 EmailInput(TextInput) 4 URLInput(TextInput) 5 PasswordInput(TextInput) 6 HiddenInput(TextInput) 7 Textarea(Widget) 8 DateInput(DateTimeBaseInput) 9 DateTimeInput(DateTimeBaseInput) 10 TimeInput(DateTimeBaseInput) 11 CheckboxInput 12 Select 13 NullBooleanSelect 14 SelectMultiple 15 RadioSelect 16 CheckboxSelectMultiple 17 FileInput 18 ClearableFileInput 19 MultipleHiddenInput 20 SplitDateTimeWidget 21 SplitHiddenDateTimeWidget 22 SelectDateWidget
# label 属性的用法举例 <form action="/test/" method="post" novalidate> {% csrf_token %} <p>{{ obj.username.label }} {{ obj.username }} {{ obj.username.errors.0 }}</p> <p>{{ obj.password.label }} {{ obj.password }} {{ obj.password.errors.0 }}</p> <input type="submit" value="提交"> </form>
特别补充-ChoiceField
ChoiceField表示下拉选择类型,针对不同的标签,不同的效果有多种搭配,需要注意的是,当输入框为非ChoiceField类型时,可以使用widget来添加choices选项。如果是ChoiceField类型,那么就可以直接添加choices属性。
# 单radio,值为字符串 user = fields.CharField( initial=2, # 初始数据数量为2 widget=widgets.RadioSelect(choices=((1,'上海'),(2,'北京'),)) ) # 单radio,值为字符串 user = fields.ChoiceField( choices=((1, '上海'), (2, '北京'),), # 定义要展示的数据信息,1表示ID initial=2, widget=widgets.RadioSelect ) # 单select,值为字符串 user = fields.CharField( initial=2, widget=widgets.Select(choices=((1,'上海'),(2,'北京'),)) ) # 单select,值为字符串 user = fields.ChoiceField( choices=((1, '上海'), (2, '北京'),), initial=2, widget=widgets.Select ) # 多选select,值为列表 user = fields.MultipleChoiceField( choices=((1,'上海'),(2,'北京'),), initial=[1,], widget=widgets.SelectMultiple ) # 单checkbox user = fields.CharField( widget=widgets.CheckboxInput() ) # 多选checkbox,值为列表 user = fields.MultipleChoiceField( initial=[2, ], choices=((1, '上海'), (2, '北京'),), widget=widgets.CheckboxSelectMultiple )
ChoiceField类型的字段表示的是下拉单选框,它的choice选项表示要提供给用户选择的选项。
# choice 字段格式 city = fields.ChoiceField( choices=[(0,'上海'),(1,'北京')] )
PS:可以看到choices接收的数据类型为列表嵌套元组,是不是看起来很熟悉,没错,使用value_list获取到的数据,就是这个格式。
1 list(models.UserInfo.objects.filter(id=userid).values_list('id','username'))
这个时候需要来一波需求了,当choices的数据来源于数据库的时候,我新加了一个城市,ChoiceField 并不!会!动!态!的!重!新!读!取!,为什么呢?
这个和类的实例化有关,严格来说的话这些字段都属于类的属性,类属性不会在实例化的时候进行重新读取,那么想要类属性在实例化的时候重新读取该咋办,是的, 没错,定义在__init__ 函数中(每次实例化都会执行)。
class UserForm(Form): username = fields.CharField( required=True, error_messages={'required':'用户名不能为空','invaild':'用户名格式不正确'} ) password = fields.CharField( required=True, error_messages={'required':'密码不能为空','invalid':'密码格式不正确'} ) ut_id = fields.ChoiceField(choices=[]) # 下面每次初始化都会执行,所以这里可以写空,避免二次查库 # choices=[(1,'上海'),(2,'北京'),(3,'河南')] 这里的choices接受这种格式的数据,和values_list的结果很像 def __init__(self,*args,**kwargs): super(UserForm, self).__init__(*args,**kwargs) self.fields['ut_id'].choices =models.UserType.objects.values_list('id','title') # 由于每次实例化的时候都会执行__init__方法,就可以动态的实时更新了。
小技巧:针对下拉列表这种可能会变得数据,让其每次实例化的时候都动态的生成,否则会造成数据不能实时显示的问题
自定义验证规则
我们知道Form表单对象内置了许多字段类型,根据这些字段类型我们可以知道,其内部是包含了正则表达式的。其中源码如下:
1 class EmailValidator(object): 2 message = _('Enter a valid email address.') 3 code = 'invalid' 4 user_regex = _lazy_re_compile( 5 r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z" # dot-atom 6 r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)', # quoted-string 7 re.IGNORECASE) 8 domain_regex = _lazy_re_compile( 9 # max length for domain name labels is 63 characters per RFC 1034 10 r'((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\Z', 11 re.IGNORECASE) 12 literal_regex = _lazy_re_compile( 13 # literal form, ipv4 or ipv6 address (SMTP 4.1.3) 14 r'\[([A-f0-9:\.]+)\]\Z', 15 re.IGNORECASE) 16 domain_whitelist = ['localhost']
那么现在有一个需求,我要验证手机号,可是django Form没有内置这个正则表达式,我们该怎么办? 能否自定义?答案是可以的。主要有以下三种方式:
1.通过validators属性来定义(类型为列表):主要分为下面两种方式
- 对象
- 函数
2.通过在当前类的中定义 clean_字段名 的方法 来实现正则验证
通过对象来自定义验证规则
语法格式:
validators=[RegexValidator(r'正则表达式','错误提示')]
匹配手机号的简单实例:
from django.forms import fields from django.forms import Form from django.core.validators import RegexValidator # 注意需要导入RegexValidator对象 class UserInfo(Form): username = fields.EmailField() phone = fields.CharField( validators=[RegexValidator(r'^[0-9]+$', '请输入数字'), RegexValidator(r'^159[0-9]+$', '数字必须以159开头')], )
注意:
- 多个条件并存的时候,顺序为从左到右
- 由于指定传递正则表达式对象,所以可定制性不高
- 多用于简单的场合下
通过函数来自定义验证规则
语法格式:
from django.core.exceptions import ValidationError # 需要导入ValidationError def 函数名(value): '''匹配规则若干''' 如果不匹配: raise ValiadationError('error message') validators=[函数名,] # 在字段的validators参数中表示正则匹配使用函数 # 在函数中定义正则表达式进行返回 # 匹配成功可以不进行返回 # 匹配失败需要抛出ValidationError异常
为什么要使用ValidationError来抛出异常呢?这里来自于源码,有人要问,为啥老看源码,小白看不懂啊,我想所,人家django都写好了,你不去学习,还好意思问。神经病啊你
1 def __call__(self, value): 2 """ 3 Validate that the input contains a match for the regular expression 4 if inverse_match is False, otherwise raise ValidationError. 5 """ 6 if not (self.inverse_match is not bool(self.regex.search( 7 force_text(value)))): 8 raise ValidationError(self.message, code=self.code) 9 10 # 可以看到 RegexValidator 正则表达式对象 匹配不成功的时候会使用raise 主动的抛出ValidationError 异常,并且ValidationError对象接受两个信息,但是code已经有默认值了,所以我们这里只返回错误message即可
匹配手机号示例:
import re from django.forms import fields from django.forms import Form from django.core.exceptions import ValidationError def my_re(value): mobile_re = re.compile(r'^(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$') if not mobile_re.match(value): raise ValidationError('手机号码格式错误') class User(Form): username = fields.CharField() phone = fields.CharField( validators=[my_re,] # 调用我们定义的my_re函数进行验证 )
通过在当前类的中定义 clean_字段名 的方法 来实现正则验证
为什么要定义 clean_字段名 来进行正则验证呢?这里通过form.is_valid() 入口查看form字段的验证规则。
# is_valid 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 # 最后调用了 self.errors # 查看self.errors函数 def errors(self): "Returns an ErrorDict for the data provided for the form" if self._errors is None: self.full_clean() return self._errors # 这里又返回了 full_clean() # 查看full_clean()函数 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: # Stop further processing. return self.cleaned_data = {} # If the form is permitted to be empty, and none of the form data has # changed from the initial data, short circuit any validation. if self.empty_permitted and not self.has_changed(): return self._clean_fields() # 这里执行了 clean_fields() 函数,只要定义了clean_fieldsname 就会自动被执行 self._clean_form() self._post_clean()
所以这里方法名称有特殊要求,必须为:clean_当前字段,自动会去执行这个方法,定义字段的时候不用显示的调用这个方法。
from django import forms from django.forms import fields from django.forms import widgets from django.core.exceptions import ValidationError from django.core.validators import RegexValidator def clean_phone(self): # 去取用户提交的值:可能是错误是的,也可能是正确的 value = self.cleaned_data['phone'] mobile_re = re.compile(r'^(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$') if not mobile_re.match(value): raise ValidationError('手机号码格式错误') if models.UserInfo.objects.filter(phone=value).count(): raise ValidationError('手机号码已存在') # 可以利用数据库对数据唯一性进行验证 else: return value # 这里必须要返回正确的值,因为返回值会被重新赋值给cleaned_data中的Phone字段的值 # 先通过form基础验证,才会继续匹配。 比如字段是必填的,如果没有填写,则不会执行clean_phone函数
**切记:
1、不要瞎取值,因为不是按照顺序取值的,所以在你取得时候,可能还没有赋值。
2、对象.__dict__:通过这种方法去取得值,因为每次顺序都是不一样的。所以可能你取的值,还没有被赋值。
小结:
1、对象,只能添加正则表达式
2、函数,不常用
3、当前类方法中自定义,可以添加对数据库的操作,注意必须要有返回值。
4、对象方法和类方法中定义,是可以同时存在的,先执行对象中定义的,然后再执行类方法中自定义的。
例子:注册的时候,两次密码一致性的验证
由于不能再一个验证规则里面取其他字段的值,所以前面的方法并不可行,这里django提供了其他的form表单的整体验证(细心的人会看到刚刚的full_clean后有一个clean_form函数)
定义clean函数,来对form进行整体验证(注意这里的数据,表示前面验证都有通过)
from django.core.exceptions import ValidationError def clean(self): pwd = self.cleaned_data['pwd'] pwd_confirm = self.cleaned_data['pwd_confirm'] if pwd == pwd_confirm: return self.cleaned_data # 必须有返回值,事关再次取cleaned_data是否有值 else: self.add_error('pwd',ValidationError('密码输入不一致')) # 根据源码中的操作,把错误信息添加到 self.add_errors self.add_error('pwd_confirm',ValidationError('密码输入不一致')) # 添加错误信息必须是ValidationError对象 return self.cleaned_data # 如果有人添加错误,那么就不能通过
扩展:
1、由于在full_clean()函数中会执行钩子函数(clean_fields,clean_form,_post_clean),所以钩子函数存在,就执行,不存在就跳过。【clean_form,_post_clean功能类似,这里不在赘述】
2、当对象和类方法 同时存在时,先匹配对象,然后再匹配类方法。
强制触发异常
在某些场景下比如用户登录,如果用户名或者密码错误,我们需要手动触发一个异常,并提示:‘用户名或者密码错误’。这时可以利用form.add_error() 来进行触发。
from django.core.exceptions import ValidationError form.add_error('password',ValidationError('用户名或者密码错误')) # 这样在前端的password字段后面会主动触发异常
当然,这里用了ValidationError对象,是因为django提供的 form.is_valid中最后也是用过ValidationError完成添加的,这里其实也可以直接写错误信息,但是推荐和源码保持一致。