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模式下呢?

这里主要会经过几个步骤:

  1. 当一个请求转发至我们定义的类时,首先执行的是类的dispath方法。
  2. 子类中没有定义dispath方法,那么就会去父类中寻找该方法(View)
  3. 该方法中通过截取request.method中请求方式,通过反射(getattr)在我们定义的类中去查找对应的方法。
  4. 执行对应的方法,然后返回数据给用户
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)
父类中的dispath

注意:
  这里我们定义的get/post等函数必须是小写,因为django会通过反射(getattr)的方式找到 class 的函数, 可以查看Views的dispath函数,其找的时候是先把用户的请求类型变成小写,然后才进行查找的。
  所以:

  1. 请求先通过父类的dispath函数,然在去子类中利用反射查找get和post 或者其他请求方法对应的函数。
  2. 如果子类定义了dispath函数,如果直接返回数据,那么class 的get 和 post 将永远不会执行。
  3. 如果要定义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

补充流程:

  1. 执行父类的dispath方法,这时父类中的self 其实就是子类实例化的对象,然后通过反射执行实例化对象的 get/post方法,然后把结果进行返回
  2. 接受get/post 执行的结果,然后返回。
  3. 其实是在父类中就直接调用了 子类的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/')
CBV下的CSRF使用

2 序列化

当后台返回数据给ajax的时候,我们可以返回序列化的数据给前端进行展示,那么问题来了。如果我要返回的是QuerySet对象呢?JS里面可并没有这个数据类型啊。Python的json.dumps也不能转换QuerySet类型呢,这该怎么办呢?

针对这种需求有两种方式

    1. 利用django提供的序列化工具serializers
    2. 自定义序列化

序列化工具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模版验证完成后会产出三个信息。

  1. 是否验证成功
  2. 所有的正确信息
  3. 所有的错误信息

创建一个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模版会返回三个对象:

    1. 验证的结果:formobj.as_valid()
    2. 验证成功的数据:formobj.cleaned_data
    3. 验证失败的数据: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})
利用initial初始化Form数据

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})
传递FORM对象

使用以上两种方式:用户的输入的正确数据都会被保留。是不是很屌。不屌,因为只能输出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
Django内置其他插件
# 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的数据来源

这个时候需要来一波需求了,当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']
Email字段正则表达式

那么现在有一个需求,我要验证手机号,可是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开头')],
    )

注意:

  1. 多个条件并存的时候,顺序为从左到右
  2. 由于指定传递正则表达式对象,所以可定制性不高
  3. 多用于简单的场合下

通过函数来自定义验证规则

语法格式:

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即可
RegexValidator内部信息

匹配手机号示例:

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完成添加的,这里其实也可以直接写错误信息,但是推荐和源码保持一致。

 

posted @ 2017-09-11 13:53  SpeicalLife  阅读(525)  评论(0编辑  收藏  举报