python 全栈开发,Day78(Django组件-forms组件)

一、Django组件-forms组件

forms组件

django中的Form组件有以下几个功能:

  • 生成HTML标签
  • 验证用户数据(显示错误信息)
  • HTML Form提交保留上次提交数据
  • 初始化页面显示内容

校验字段功能

之前写的视图函数,提交的数据,没有做校验,就添加到数据库里面了。这样是不对的!

比如:用户名,必须要符合一定的长度。密码复杂度,等等。

forms组件最大的作用,就是做数据校验。

 

普通做法,一个一个写校验规则,没有解耦。校验规则,都在视图函数里面。

新建项目formDemo

修改urls.py,新增路径index

from app01 import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
]
View Code

修改views.py,新增index视图函数

form组件先放到视图函数
单独起一个类,后续会分离出来

from django.shortcuts import render

# Create your views here.
from django import forms  # 必须导入模块
class DemoForm(forms.Form):  # 必须继承Form
    #限制数据为字符串,最大长度32
    name = forms.CharField(max_length=32)
    age = forms.IntegerField()  # 限制为数字
    email = forms.EmailField()  # 限制为邮箱格式

def index(request):
    return render(request,"index.html")
View Code

templates新增index.html,里面是空的

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>
View Code

打开Pycharm,点击左下角的Python Console

 

输入以下命令,导入视图函数的DemoForm类

from app01.views import DemoForm

效果如下:

如果有报错,请查看当前的python环境,是否加载了Django模块。

 

DemoForm类是用来做校验的,它接收一个字典。字典必须包含2个key,分别是name,age,email

测试一个字典数据

执行下面2个命令

form=DemoForm({"name":"xiao","age":"21","email":"123@163.com"})
form.is_valid()

效果如下:输出True

解释:

is_valid()表示执行校验,如果3个key都符合要求,输出True

 

测试1:age不符合

 

 3个必须同时成立才行

在DemoForm里面,等式左边对应的是key,等式右边对应校验规则

它有一个默认规则,不能为空

 测试2:少一个字段

 

测试3:加一个额外的key-value呢?

 从结果上来看,也是可以通过的。

 它只校验指定的字段,那么额外的键值,会忽略。

 

网页校验

修改urls.py,增加路径addbook

from app01 import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
    path('addbook/', views.addbook),
]
View Code

修改views.py,增加addbook视图函数,完整代码如下:

from django.shortcuts import render,HttpResponse

# Create your views here.
from django import forms  # 必须导入模块
class UserForm(forms.Form):  # 必须继承Form
    #限制数据为字符串,最小长度4,最大长度12
    name = forms.CharField(min_length=4,max_length=12)
    age = forms.IntegerField()  # 限制为数字
    #限制长度为11位
    tel = forms.CharField(min_length=11,max_length=11)

def index(request):
    return render(request,"index.html")

def addbook(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        if form.is_valid():  # 验证数据
            print("success")
        else:
            print("fail")
        return HttpResponse("ok")

    return render(request,"addbook.html")
View Code

templates新增addbook.html

做表单校验的时候,一定要注意,表单的name和class的属性必须一一对应

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>添加用户</h3>
<form action="" method="post">
    {% csrf_token %}
    <lable>姓名</lable><input type="text" name="name"/><br/>
    <lable>年龄</lable><input type="text" name="age"/><br/>
    <lable>邮箱</lable><input type="text" name="email"/><br/>
    <lable>手机号码</lable><input type="text" name="tel"/>
    <br/>
    <input type="submit">
</form>
</body>
</html>
View Code

网页访问添加页面,输出信息

 提交之后,效果如下:

Pycharm控制台输出:success

 

空表单直接提交

Pycharm控制台输出:fail

 

is_valid()

form.is_valid() 它做了2件事情:

1.将数据传给form
2.将验证数据拆分到2个容器中

self.cleaned_data= {} 表示干净的数据
self.error = {} 表示验证不通过的数据

self表示UserForm类的实例对象

 

addbook视图函数

def addbook(request):
    if request.method == "POST":
        print(request.POST)
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)
            print(form.errors)
        else:
            print("###fail###")
            print(form.cleaned_data)
            print(form.errors)
            print(type(form.errors))

        return HttpResponse("ok")

    return render(request,"addbook.html")
View Code

再次提交数据

Pycharm控制台输出:

<QueryDict: {'tel': ['12345678910'], 'email': ['123@qq.com'], 'name': ['xiao'], 'age': ['23'], 'csrfmiddlewaretoken': ['wv7VhRwG4YvEO7SqE9qsMnpO4RpH1ys1KdiOrwgnrN3WRgW0IH8prXSUMCgdMz7u']}>
###success###
{'tel': '12345678910', 'age': 23, 'name': 'xiao'}

<class 'django.forms.utils.ErrorDict'>
View Code

虽然POST发送了5个键值,但是UserForm只校验3个键值。

form.cleaned_data 输出了3个键值

form.errors 输出的内容空,它的类型为ErrorDict

只要有一个错误,就会走else 

 

错误数据演示

修改views.py,修改UserForm,增加邮箱

class UserForm(forms.Form):  # 必须继承Form
    #限制数据为字符串,最小长度4,最大长度12
    name = forms.CharField(min_length=4,max_length=12)
    age = forms.IntegerField()  # 限制为数字
    email = forms.EmailField()  # 限制为邮箱格式
    #限制长度为11位
    tel = forms.CharField(min_length=11,max_length=11)
View Code

输入一个错误的表单

Pycharm控制台输出:

###fail###
{'name': 'awew', 'age': 12, 'tel': '12345678910'}
<ul class="errorlist"><li>email<ul class="errorlist"><li>Enter a valid email address.</li></ul></li></ul>
View Code

form.errors输出了一段Html标签,提示邮箱格式错误

 

提取email错误信息

修改UserForm

def addbook(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)  # 所有干净的字段以及对应的值
            # ErrorDict : {"校验错误的字段":["错误信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
        else:
            print("###fail###")
            print(form.cleaned_data)
            print(form.errors)
            # 获取email错误信息,返回一个错误列表,可以切片
            print(form.errors.get("email"))
            # 获取第一个错误信息
            print(form.errors.get("email")[0])

        return HttpResponse("ok")

    return render(request,"addbook.html")
View Code

Pycharm控制台输出:

###fail###
[06/Jul/2018 22:33:41] "POST /addbook/ HTTP/1.1" 200 2
{'tel': '12345678910', 'age': 12, 'name': 'awew'}
<ul class="errorlist"><li>email<ul class="errorlist"><li>Enter a valid email address.</li></ul></li></ul>
<ul class="errorlist"><li>Enter a valid email address.</li></ul>
Enter a valid email address.
View Code

form.errors.get("email") 可以提取email的错误信息,它返回的是一个错误列表

通过切片,可以获取第一个错误信息

 

渲染标签功能 

渲染方式1

使用自带的模板属性渲染

上面讲的form表单里面的元素,是手动写的。form组件可以帮你实现渲染表单元素!

那么需要渲染哪些元素,取决于UserForm这个自定义类的属性来决定的

举例:

修改addbook视图函数

def addbook(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)  # 所有干净的字段以及对应的值
            # ErrorDict : {"校验错误的字段":["错误信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
        else:
            print("###fail###")
            print(form.cleaned_data)
            print(form.errors)
            # 获取email错误信息,返回一个错误列表,可以切片
            print(form.errors.get("email"))
            # 获取第一个错误信息
            print(form.errors.get("email")[0])

        return render(request, "adduser.html", {"form":form})

    else:
        form = UserForm()
        return render(request,"addbook.html",{"form":form})
View Code

修改addbook.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>添加用户</h3>
<form action="" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <br/>
    <input type="submit">
</form>
</body>
</html>
View Code

as_p是一个特殊的属性,常见的有:

  • {{ form.as_table }} 以表格的形式将它们渲染在<tr> 标签中
  • {{ form.as_p }} 将它们渲染在<p> 标签中
  • {{ form.as_ul }} 将它们渲染在<li> 标签中

 

访问页面,效果如下:

使用浏览器工具,查看html代码

它使用了P标签来包裹

lable的for属性和input的id属性是对应的。id的名字和UserForm类定义的属性是类似的,加了id_前缀。

lable的显示的文字和UserForm类定义的属性是一样的,首字母大写了!

input的name属性和UserForm类定义的属性是一样的

默认自带required属性,不允许内容为空。

minlength的属性来源于UserForm类的定义。

注意:form组件只能渲染表单里面的元素,比如input标签。除此之外,其他的需要手写!

它的样式,太丑了!

 

渲染方式2

使用自定义的标签来包裹form变量

举例:

更改addbook.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>添加用户</h3>
<form action="" method="post">
    {% csrf_token %}
    <div>
        <p>姓名</p>
        {{ form.name }}
    </div>
    <div>
        <p>年龄</p>
        {{ form.age }}
    </div>
    <div>
        <p>邮箱</p>
        {{ form.email }}
    </div>
    <div>
        <p>手机号码</p>
        {{ form.tel }}
    </div>
    <input type="submit">
</form>
</body>
</html>
View Code

刷新网页,效果如下:

 

渲染方式3

使用for循环渲染

修改addbook.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>添加用户</h3>
<form action="" method="post">
    {% csrf_token %}
    {% for field in form %}
    <div>
        <label for="">{{ field.label }}</label>
        {{ field }}
    </div>
    {% endfor %}
    <input type="submit">
</form>
</body>
</html>
View Code

刷新网页,效果如下:

field.label 表示UserForm类定义的属性名。注意:它不是html的label标签!

field 表示input输入框,由forms组件渲染

 

显示中文

将label换成中文,需要增加label属性

修改views.py里面的UserForm类

class UserForm(forms.Form):  # 必须继承Form
    #限制数据为字符串,最小长度4,最大长度12
    name = forms.CharField(min_length=4,max_length=12,label="姓名")
    age = forms.IntegerField(label="年龄")  # 限制为数字
    email = forms.EmailField(label="邮箱")  # 限制为邮箱格式
    #限制长度为11位
    tel = forms.CharField(min_length=11,max_length=11,label="手机号码")
View Code

刷新网页,效果如下:

 

 美化input输入框

需要使用bootstrap

修改urls.py,修改路径

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
    path('adduser/', views.adduser),
]
View Code

修改views.py,将addbook重名为adduser

def adduser(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)  # 所有干净的字段以及对应的值
            # ErrorDict : {"校验错误的字段":["错误信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
        else:
            print("###fail###")
            print(form.cleaned_data)
            print(form.errors)
            # 获取email错误信息,返回一个错误列表,可以切片
            print(form.errors.get("email"))
            # 获取第一个错误信息
            print(form.errors.get("email")[0])

        return render(request, "adduser.html", {"form":form})

    else:
        form = UserForm()
        return render(request,"adduser.html",{"form":form})
View Code

将addbook.html,重命名为adduser.html

引入bootstrap,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
        <h3>添加用户</h3>
            <form action="" method="post">
                {% csrf_token %}
                {% for field in form %}
                    <div>
                        <label for="">{{ field.label }}</label>
                        {{ field }}
                    </div>
                {% endfor %}
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

访问url: http://127.0.0.1:8000/adduser/

效果如下:

 

这里面input用的还是默认样式,只要给input标签增加class="form-group",就有美化效果!

由于input是form组件渲染的,不能直接添加class,需要在UserForm类里面,指定class

修改UserForm类之前,导入一个模块widgets

Widgets

Widget 是Django 对HTML 输入元素的表示。Widget 负责渲染HTML和提取GET/POST 字典中的数据。

如果你想让某个Widget 实例与其它Widget 看上去不一样,你需要在Widget 对象实例化并赋值给一个表单字段时指定额外的属性(以及可能需要在你的CSS 文件中添加一些规则)

修改views.py,完整代码如下:

from django.shortcuts import render,HttpResponse
from django import forms  # 必须导入模块
from django.forms import widgets
# Create your views here.

class UserForm(forms.Form):  # 必须继承Form
    #定义变量,专门给text类型的输入框添加class
    wid = widgets.TextInput(attrs={"class":"form-control"})
    #限制数据为字符串,最小长度4,最大长度12
    name = forms.CharField(min_length=4,max_length=12,label="姓名",widget=wid)
    age = forms.IntegerField(label="年龄",widget=wid)  # 限制为数字
    # 限制为邮箱格式
    email = forms.EmailField(label="邮箱",widget=widgets.EmailInput(attrs={"class":"form-control"}))
    #限制长度为11位
    tel = forms.CharField(min_length=11,max_length=11,label="手机号码",widget=wid)

def index(request):
    return render(request,"index.html")

def adduser(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)  # 所有干净的字段以及对应的值
            # ErrorDict : {"校验错误的字段":["错误信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
        else:
            print("###fail###")
            print(form.cleaned_data)
            print(form.errors)
            # 获取email错误信息,返回一个错误列表,可以切片
            print(form.errors.get("email"))
            # 获取第一个错误信息
            print(form.errors.get("email")[0])

        return render(request, "adduser.html", {"form":form})

    else:
        form = UserForm()
        return render(request,"adduser.html",{"form":form})
View Code

解释:

widget等式右边,可以指定多种类型的输入框,比如:TextInput,EmailInput,DateInput...

默认是TextInput

attrs 表示设置css样式,它接收一个字典,可以写多个css样式!

 

修改adduser.html

给div增加class="form-group",表示调整上下间距

col-md-offset-2 表示偏移距离

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
        <h3>添加用户</h3><br/>
            <form action="" method="post">
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }}
                    </div>
                {% endfor %}
                <br/>
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

 

刷新网页,效果如下:

 

显示错误与保存输入信息功能

保存输入信息功能

 比如博客园的注册页面,链接如下:

https://account.cnblogs.com/

直接提交空数据,页面会提示

那么form组件,也是可以实现这个效果

修改adduser视图函数

def adduser(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)  # 所有干净的字段以及对应的值
            # ErrorDict : {"校验错误的字段":["错误信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            # return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 获取email错误信息,返回一个错误列表,可以切片
            # print(form.errors.get("email"))
            # # 获取第一个错误信息
            # print(form.errors.get("email")[0])
        return render(request, "adduser.html")
        # return render(request, "adduser.html", {"form":form})

    else:  # 默认是get请求(地址栏输入访问时)
        form = UserForm()  # 没有表单数据的form
        return render(request,"adduser.html",{"form":form})
View Code

直接提交空数据,页面有错误提示

注意:这个提示是bootstrap做的,不是form组件

虽然jquery可以直接对表单进行验证,判断为空,或者其他规则。但是客户端浏览器的js代码,是可以跳过验证的。直接提交数据给服务器,如果服务器没有做数据校验,那么将面临风险!

 

修改adduser.html,在form标签后面增加novalidate,表示关闭bootstrap表单验证

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
        <h3>添加用户</h3><br/>
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }}
                    </div>
                {% endfor %}
                <br/>
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

刷新页面,填3个值,最后一个故意不填写

点击提交,效果如下:

发现刚刚增加的数据,没有了。这样用户体验不好!用户得小心翼翼的输入每一个数据!

查看Pycharm控制台输出:

###fail###
<ul class="errorlist"><li>tel<ul class="errorlist"><li>This field is required.</li></ul></li></ul>
View Code

发现它走了else的代码,使用render时,没有传值。导致页面为空!

 

修改views.py

给adduser.html传一个form。注意:此时的form变量是带有表单数据的!

def adduser(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)  # 所有干净的字段以及对应的值
            # ErrorDict : {"校验错误的字段":["错误信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 获取email错误信息,返回一个错误列表,可以切片
            # print(form.errors.get("email"))
            # # 获取第一个错误信息
            # print(form.errors.get("email")[0])
        return render(request, "adduser.html", {"form":form})

    else:  # 默认是get请求(地址栏输入访问时)
        form = UserForm()  # 没有表单数据的form
        return render(request,"adduser.html",{"form":form})
View Code

再次刷新页面,数据就回来了!

数据怎么就回来了呢?

因为既然是POST请求,而且携带了POST数据,必然执行了form.is_valid()

虽然没有验证通过,但是执行下面一句代码时

return render(request, "adduser.html", {"form":form})

此时的form是含有POST表单数据的,所以页面才会渲染出来!

注意:当input属性为password时,是不会渲染的!除此之外,其他的表单元素,是可以渲染的

 

提交一个正确的数据

提示添加成功

 

显示错误信息

约定俗成,使用span标签来显示错误信息

修改adduser.html,增加span标签

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
        <h3>添加用户</h3><br/>
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }}<span>{{ field.errors.0 }}</span>
                    </div>
                {% endfor %}
                <br/>
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

解释:

field.errors 表示错误列表。因为是列表,会有很多错误信息

field.errors.0 表示接收第一个错误信息。一般取第一个即可!

 

那么问题来了,get请求时,比如地址栏访问页面,它是取不到值的。那么span标签是空的,但是不影响页面展示

 

直接提交空数据,页面会有英文提示,它表示此字段不允许为空

 

显示黑色,不好看,加一个样式

修改adduser.html,增加样式。pull-right表示右对齐

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <style>
        .error {
            color: red;
        }
    </style>
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
        <h3>添加用户</h3><br/>
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }}<span class="error pull-right">{{ field.errors.0 }}</span>
                    </div>
                {% endfor %}
                <br/>
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

刷新页面,效果如下:

错误信息显示中文

显示中文需要在UserForm类中的字段增加error_message属性

class UserForm(forms.Form):  # 必须继承Form
    #定义变量,专门给text类型的输入框添加class
    wid = widgets.TextInput(attrs={"class":"form-control"})
    #定义字典,错误信息显示中文
    #限制数据为字符串,最小长度4,最大长度12
    error_hints = {"required":"该字段不能为空"}
    name = forms.CharField(min_length=4,max_length=12,label="姓名",widget=wid,error_messages=error_hints)
    # 限制为数字
    age = forms.IntegerField(label="年龄",widget=wid,error_messages=error_hints)
    # 限制为邮箱格式
    email = forms.EmailField(label="邮箱",widget=widgets.EmailInput(attrs={"class":"form-control"}),error_messages=error_hints)
    #限制长度为11位
    tel = forms.CharField(min_length=11,max_length=11,label="手机号码",widget=wid,error_messages=error_hints)
View Code

解释:

error_messages 用来定义错误信息,可以定义多个错误类型!它接收一个字典

required 表示为空时,输出This field is required. 

那么要定义中文时,重新赋值即可。需要显示日文,韩文,法文...的,自己定义吧!

 

重新访问页面,输入第一个值,提交。其它字段会有错误提示!

 邮箱输入字符串,提示一段英文信息。不行,得改!

修改UserForm类,修改这一行

error_hints = {"required":"该字段不能为空","invalid":"格式错误!"}

重新提示数据

核心问题,必须要明白,错误信息为什么会显示出来?

执行is_valid(),就会执行校验动作。如果不通过,那么form变量就会包含错误信息。
通过form组件渲染错误信息,页面就展示出来

 

局部钩子与全局钩子

上面提到的校验规则是forms组件自带的。 它做了一些简单的校验功能,比如判断字符串,纯数字,邮箱等等。

比如要求用户名,必须包含字母和数字。年龄必须要大于18岁,手机号码要以136,137开头...

这些需求,默认的校验规则是做不到的。

我们想要自行设计校验的规则,Django给我们提供了钩子。

先来看一段源码:

if hasattr(self, 'clean_%s' % name):
    value = getattr(self, 'clean_%s' % name)()
    self.cleaned_data[name] = value

这段源码能够设置钩子的来源。

 

局部钩子

导入模块

from django.core.exceptions import NON_FIELD_ERRORS, ValidationError

举例:

要求用户名不能是纯数字

def clean_name(self):
    val = self.cleaned_data.get("name")  # 获取输入的用户名

    if not val.isdigit():  # 判断不是数字类型
        return val
    else:
        raise ValidationError("用户名不能是纯数字")

注意:

clean_name  这个名字是有含义的,不能随便定义。name表示UserForm类的属性。clean表示校验

val 表示用户输入的用户名

val.isdigit() 表示判断输入的是否为数字,必须return 一个值

raise 表示主动报错,必须接ValidationError。

上面这些要求是源代码定义的,具体可以看源代码。

 

views.py,完整代码如下:

from django.shortcuts import render,HttpResponse
from django import forms  # 必须导入模块
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
# Create your views here.

class UserForm(forms.Form):  # 必须继承Form
    #定义变量,专门给text类型的输入框添加class
    wid = widgets.TextInput(attrs={"class":"form-control"})
    #定义字典,错误信息显示中文
    #限制数据为字符串,最小长度4,最大长度12
    error_hints = {"required":"该字段不能为空","invalid":"格式错误!"}
    name = forms.CharField(min_length=4,max_length=12,label="姓名",widget=wid,error_messages=error_hints)
    # 限制为数字
    age = forms.IntegerField(label="年龄",widget=wid,error_messages=error_hints)
    # 限制为邮箱格式
    email = forms.EmailField(label="邮箱",widget=widgets.EmailInput(attrs={"class":"form-control"}),error_messages=error_hints)
    #限制长度为11位
    tel = forms.CharField(min_length=11,max_length=11,label="手机号码",widget=wid,error_messages=error_hints)

    def clean_name(self):
        val = self.cleaned_data.get("name")  # 获取输入的用户名

        if not val.isdigit():  # 判断数字类型
            return val
        else:
            raise ValidationError("用户名不能是纯数字")

def index(request):
    return render(request,"index.html")

def adduser(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)  # 所有干净的字段以及对应的值
            # ErrorDict : {"校验错误的字段":["错误信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 获取email错误信息,返回一个错误列表,可以切片
            # print(form.errors.get("email"))
            # # 获取第一个错误信息
            # print(form.errors.get("email")[0])
        return render(request, "adduser.html", {"form":form})

    else:  # 默认是get请求(地址栏输入访问时)
        form = UserForm()  # 没有表单数据的form
        return render(request,"adduser.html",{"form":form})
View Code

 

验证一下,输入4位数字,提交之后,页面提示如下:

手机号码必须11位

修改UserForm类,增加clean_tel方法,完整代码如下:

from django.shortcuts import render,HttpResponse
from django import forms  # 必须导入模块
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
# Create your views here.

class UserForm(forms.Form):  # 必须继承Form
    #定义变量,专门给text类型的输入框添加class
    wid = widgets.TextInput(attrs={"class":"form-control"})
    #定义字典,错误信息显示中文
    #限制数据为字符串,最小长度4,最大长度12
    error_hints = {"required":"该字段不能为空","invalid":"格式错误!"}
    name = forms.CharField(min_length=4,max_length=12,label="姓名",widget=wid,error_messages=error_hints)
    # 限制为数字
    age = forms.IntegerField(label="年龄",widget=wid,error_messages=error_hints)
    # 限制为邮箱格式
    email = forms.EmailField(label="邮箱",widget=widgets.EmailInput(attrs={"class":"form-control"}),error_messages=error_hints)
    #限制长度为11位
    tel = forms.CharField(max_length=11,label="手机号码",widget=wid,error_messages=error_hints)

    def clean_name(self):  # 校验name值
        val = self.cleaned_data.get("name")  # 获取输入的用户名

        if not val.isdigit():  # 判断数字类型
            return val
        else:
            raise ValidationError("用户名不能是纯数字")

    def clean_tel(self):
        val = self.cleaned_data.get("tel")
        if len(val) == 11:  # 判断长度
            return val
        else:
            raise ValidationError("手机号码必须11位")


def index(request):
    return render(request,"index.html")

def adduser(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)  # 所有干净的字段以及对应的值
            # ErrorDict : {"校验错误的字段":["错误信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 获取email错误信息,返回一个错误列表,可以切片
            # print(form.errors.get("email"))
            # # 获取第一个错误信息
            # print(form.errors.get("email")[0])
        return render(request, "adduser.html", {"form":form})

    else:  # 默认是get请求(地址栏输入访问时)
        form = UserForm()  # 没有表单数据的form
        return render(request,"adduser.html",{"form":form})
View Code

注意:要去除tel里面的min_length,这是不严谨的写法!

 

重新访问页面,测试一下

 

 年龄必须18岁以上

增加clean_age方法

    def clean_age(self):
        val = self.cleaned_data.get("age")
        if int(val) > 18:  # input输入的值为字符串,必须转换为int
            return val
        else:
            raise ValidationError("年龄必须满18岁以上!")
View Code

重新访问页面,测试一下

注意:

is_valid执行时,才会执行校验。

这里有2层校验。第一层校验是UserForm定义的那些属性,比如判断字符串或者数字的。

第二次校验是clean_属性名 定义的这些方法。只有通过第一层校验时,才会进入第二层校验。

不论式第一层还是第二层,通过校验后,将key_value放到 cleaned_data容器里面。不通过校验时,将key-value放到errors容器里面

 

查看源代码

先找到views.py里面的is_valid,使用Ctrl+鼠标左键,点击is_valid。它会调转到is_valid方法的源代码

点击self.errors-->full_clean()-->self._clean_fields()

_clean_fields源代码如下:

    def _clean_fields(self):
        for name, field in self.fields.items():
            # value_from_datadict() gets the data from the data dictionaries.
            # Each widget type knows how to retrieve its own data, because some
            # widgets split data over several HTML fields.
            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)
View Code

它会对表单的每一个数据,使用for循环处理。field指的是UserForm定义的那些属性

self.fields.items() 这里的self.fields数据,可能是这样的。

self.fields={"name":name的field对象,"age":age的field对象...}

self.fields.items() ,就能拿到一个field对象。这个field是是一个规则对象.

 

self.cleaned_data[name] = value 表示通过第一层校验,将干净的数据放到cleaned_data容器里

if hasattr(self, 'clean_%s' % name): 表示进入第二校验,name表示UserForm定义的那些属性。通过后,也会放到cleaned_data容器里

except ValidationError as e  表示接收ValidationError错误,add_error表示添加到error容器里。

所以在UserForm定义clean时,必须使用raise ValidationError(xxx)

 

假设通过第一层校验,但是没有通过第二层校验时。它会执行add_error方法,那么原来的cleaned_data容器的数据怎么办?

add_error源代码如下:

    def add_error(self, field, error):
        """
        Update the content of `self._errors`.

        The `field` argument is the name of the field to which the errors
        should be added. If it's None, treat the errors as NON_FIELD_ERRORS.

        The `error` argument can be a single error, a list of errors, or a
        dictionary that maps field names to lists of errors. An "error" can be
        either a simple string or an instance of ValidationError with its
        message attribute set and a "list or dictionary" can be an actual
        `list` or `dict` or an instance of ValidationError with its
        `error_list` or `error_dict` attribute set.

        If `error` is a dictionary, the `field` argument *must* be None and
        errors will be added to the fields that correspond to the keys of the
        dictionary.
        """
        if not isinstance(error, ValidationError):
            # Normalize to ValidationError and let its constructor
            # do the hard work of making sense of the input.
            error = ValidationError(error)

        if hasattr(error, 'error_dict'):
            if field is not None:
                raise TypeError(
                    "The argument `field` must be `None` when the `error` "
                    "argument contains errors for multiple fields."
                )
            else:
                error = error.error_dict
        else:
            error = {field or NON_FIELD_ERRORS: error.error_list}

        for field, error_list in error.items():
            if field not in self.errors:
                if field != NON_FIELD_ERRORS and field not in self.fields:
                    raise ValueError(
                        "'%s' has no field named '%s'." % (self.__class__.__name__, field))
                if field == NON_FIELD_ERRORS:
                    self._errors[field] = self.error_class(error_class='nonfield')
                else:
                    self._errors[field] = self.error_class()
            self._errors[field].extend(error_list)
            if field in self.cleaned_data:
                del self.cleaned_data[field]
View Code

注意:看最后一行,它会将cleaned_data容器里,没通过的数据给删除掉!

 

全局钩子

局部钩子只能校验一个字段,那么2个字段校验呢?比如密码和确认密码必须一致,这个时候,需要使用全局钩子

如何定义全局钩子呢?查看源代码

is_valid()-->self.errors-->self.full_clean()-->self._clean_form()-->self.clean()

clean源代码如下:

    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
View Code

谷歌翻译如下:

在Field.clean()之后进行任何额外的表单范围清理,呼吁每个领域。此方法引发的任何ValidationError都将与特定领域无关;它将有一个特例与名为'__all__'的字段关联。

大概意思就是,它在clean_xx执行之后,才会执行。一旦引发了ValidationError,与特定领域无关。错误信息都在'__all__'里面

这个clean是全局钩子,属于第3层校验规则。源代码没有任何逻辑,所以这个方法,需要我们来重写。注意:名字必须是clean,结尾部分必须是return self.cleaned_data

 

两次密码不一致

修改UserForm类,增加2个属性,并定义全局钩子clean

完整代码如下:

from django.shortcuts import render, HttpResponse
from django import forms  # 必须导入模块
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError


# Create your views here.

class UserForm(forms.Form):  # 必须继承Form
    # 定义变量,专门给text类型的输入框添加class
    wid = widgets.TextInput(attrs={"class": "form-control"})
    # 定义字典,错误信息显示中文
    # 限制数据为字符串,最小长度4,最大长度12
    error_hints = {"required": "该字段不能为空", "invalid": "格式错误!"}
    name = forms.CharField(min_length=4, max_length=12, label="姓名", widget=wid, error_messages=error_hints)
    # 密码字段
    pwd = forms.CharField(label="密码", widget=widgets.PasswordInput(attrs={"class": "form-control"}))

    r_pwd = forms.CharField(label="确认密码", widget=widgets.PasswordInput(attrs={"class": "form-control"}))
    # 限制为数字
    age = forms.IntegerField(label="年龄", widget=wid, error_messages=error_hints)
    # 限制为邮箱格式
    email = forms.EmailField(label="邮箱", widget=widgets.EmailInput(attrs={"class": "form-control"}),
                             error_messages=error_hints)
    # 限制长度为11位
    tel = forms.CharField(max_length=11, label="手机号码", widget=wid, error_messages=error_hints)

    def clean_name(self):  # 校验name值
        val = self.cleaned_data.get("name")  # 获取输入的用户名

        if not val.isdigit():  # 判断数字类型
            return val
        else:
            raise ValidationError("用户名不能是纯数字")

    def clean_tel(self):
        val = self.cleaned_data.get("tel")
        if len(val) == 11:  # 判断长度
            return val
        else:
            raise ValidationError("手机号码必须11位")

    def clean_age(self):
        val = self.cleaned_data.get("age")
        if int(val) > 18:  # input输入的值为字符串,必须转换为int
            return val
        else:
            raise ValidationError("年龄必须满18岁以上!")

    def clean(self):  # 全局钩子
        pwd = self.cleaned_data.get("pwd")
        r_pwd = self.cleaned_data.get("r_pwd")
        if pwd and r_pwd and pwd != r_pwd:  # 判断2次密码不为空,并且2次密码不相等
            raise ValidationError("两次密码不一致")
        else:
            return self.cleaned_data  # 这句是固定写法,不能变动


def index(request):
    return render(request, "index.html")


def adduser(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)  # 所有干净的字段以及对应的值
            # ErrorDict : {"校验错误的字段":["错误信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 获取email错误信息,返回一个错误列表,可以切片
            # print(form.errors.get("email"))
            # # 获取第一个错误信息
            # print(form.errors.get("email")[0])
        return render(request, "adduser.html", {"form": form})

    else:  # 默认是get请求(地址栏输入访问时)
        form = UserForm()  # 没有表单数据的form
        return render(request, "adduser.html", {"form": form})
View Code

 

重新访问页面,效果如下:

测试2次密码不一致

发现密码没有错误提示,为什么呢?

因为全局钩子和局部钩子不一样,它的错误信息在__all__里面

修改adduser视图函数,完整代码如下:

from django.shortcuts import render, HttpResponse
from django import forms  # 必须导入模块
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError


# Create your views here.

class UserForm(forms.Form):  # 必须继承Form
    # 定义变量,专门给text类型的输入框添加class
    wid = widgets.TextInput(attrs={"class": "form-control"})
    # 定义字典,错误信息显示中文
    # 限制数据为字符串,最小长度4,最大长度12
    error_hints = {"required": "该字段不能为空", "invalid": "格式错误!"}
    name = forms.CharField(min_length=4, max_length=12, label="姓名", widget=wid, error_messages=error_hints)
    # 密码字段
    pwd = forms.CharField(label="密码", widget=widgets.PasswordInput(attrs={"class": "form-control"}),
                          error_messages=error_hints)

    r_pwd = forms.CharField(label="确认密码", widget=widgets.PasswordInput(attrs={"class": "form-control"}),
                            error_messages=error_hints)
    # 限制为数字
    age = forms.IntegerField(label="年龄", widget=wid, error_messages=error_hints)
    # 限制为邮箱格式
    email = forms.EmailField(label="邮箱", widget=widgets.EmailInput(attrs={"class": "form-control"}),
                             error_messages=error_hints)
    # 限制长度为11位
    tel = forms.CharField(max_length=11, label="手机号码", widget=wid, error_messages=error_hints)

    def clean_name(self):  # 校验name值
        val = self.cleaned_data.get("name")  # 获取输入的用户名

        if not val.isdigit():  # 判断数字类型
            return val
        else:
            raise ValidationError("用户名不能是纯数字")

    def clean_tel(self):
        val = self.cleaned_data.get("tel")
        if len(val) == 11:  # 判断长度
            return val
        else:
            raise ValidationError("手机号码必须11位")

    def clean_age(self):
        val = self.cleaned_data.get("age")
        if int(val) > 18:  # input输入的值为字符串,必须转换为int
            return val
        else:
            raise ValidationError("年龄必须满18岁以上!")

    def clean(self):  # 全局钩子
        pwd = self.cleaned_data.get("pwd")
        r_pwd = self.cleaned_data.get("r_pwd")
        if pwd and r_pwd and pwd != r_pwd:  # 判断2次密码不为空,并且2次密码不相等
            raise ValidationError("两次密码不一致")
        else:
            return self.cleaned_data  # 这句是固定写法,不能变动


def index(request):
    return render(request, "index.html")


def adduser(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)  # 所有干净的字段以及对应的值
            # ErrorDict : {"校验错误的字段":["错误信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 获取email错误信息,返回一个错误列表,可以切片
            # print(form.errors.get("email"))
            # # 获取第一个错误信息
            # print(form.errors.get("email")[0])
            g_error = form.errors.get("__all__")  # 接收全局钩子错误信息
            if g_error:  # 判断有错误信息的情况下
                g_error = g_error[0]  # 取第一个错误信息

            # 将form和g_error变量传给adduser.html
            return render(request, "adduser.html", {"form": form, "g_error": g_error})

    else:  # 默认是get请求(地址栏输入访问时)
        form = UserForm()  # 没有表单数据的form
        return render(request, "adduser.html", {"form": form})
View Code

修改adduser.html,完整代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <style>
        .error {
            color: red;
        }
    </style>
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
        <h3>添加用户</h3><br/>
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }} <span class="error pull-right">{{ field.errors.0 }}</span>
                         {% if field.label == "确认密码" %}
                         <span class="error pull-right">{{ g_error|default_if_none:"" }}</span>
                         {% endif %}
                    </div>
                {% endfor %}
                <br/>
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

解释:

我们想在第二次密码输入框下面,展示全局钩子的错误信息。那么需要用到if判断了

g_error|default_if_none:"" 表示当g_error为none时,页面显示为空("")

 

验证一下,输入2个不一致的密码

 

效果如下:

Pycharm控制台输出:

<QueryDict: {'name': [''], 'r_pwd': ['111'], 'email': [''], 'age': [''], 'pwd': ['11'], 'csrfmiddlewaretoken': ['slo8iY8aB1Z1x6coSPfaqxdrLQW5NSCxG3z1sDSRYQxjAfgYWnX757GxtBNByTh0'], 'tel': ['']}>
###fail###
<ul class="errorlist"><li>__all__<ul class="errorlist nonfield"><li>两次密码不一致</li></ul></li><li>name<ul class="errorlist"><li>该字段不能为空</li></ul></li><li>age<ul class="errorlist"><li>该字段不能为空</li></ul></li><li>tel<ul class="errorlist"><li>该字段不能为空</li></ul></li><li>email<ul class="errorlist"><li>该字段不能为空</li></ul></li></ul>
View Code

 

如果只输入了1个密码呢?

页面提示确认密码不能为空

两次密码必须输入时,才会进入全局钩子

 这是为什么?此时的UserForm有3层校验规则。执行顺序如下:

forms组件自带的校验规则-->局部钩子-->全局钩子

那么当有一个密码没有输入时,直接被第一层校验规则拦截了,它是不会进入到第三层校验规则的!

设置全局钩子,必然会执行。如果上层报错,那么不会进入全局钩子!

 

思考问题:forms的校验规则和models.py的模型类,有没有关系?

答案是没有关系!forms可以独立运行,forms组件没有必要,必须和model表的字段一一对应。

根据业务需求,在需要校验的字段上,进行校验!

 

分离forms代码

在views.py同级目录创建文件form.py,将forms相关代码剪贴过去,完整内容如下:

from django import forms  # 必须导入模块
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError

class UserForm(forms.Form):  # 必须继承Form
    # 定义变量,专门给text类型的输入框添加class
    wid = widgets.TextInput(attrs={"class": "form-control"})
    # 定义字典,错误信息显示中文
    # 限制数据为字符串,最小长度4,最大长度12
    error_hints = {"required": "该字段不能为空", "invalid": "格式错误!"}
    name = forms.CharField(min_length=4, max_length=12, label="姓名", widget=wid, error_messages=error_hints)
    # 密码字段
    pwd = forms.CharField(label="密码", widget=widgets.PasswordInput(attrs={"class": "form-control"}),
                          error_messages=error_hints)

    r_pwd = forms.CharField(label="确认密码", widget=widgets.PasswordInput(attrs={"class": "form-control"}),
                            error_messages=error_hints)
    # 限制为数字
    age = forms.IntegerField(label="年龄", widget=wid, error_messages=error_hints)
    # 限制为邮箱格式
    email = forms.EmailField(label="邮箱", widget=widgets.EmailInput(attrs={"class": "form-control"}),
                             error_messages=error_hints)
    # 限制长度为11位
    tel = forms.CharField(max_length=11, label="手机号码", widget=wid, error_messages=error_hints)

    def clean_name(self):  # 校验name值
        val = self.cleaned_data.get("name")  # 获取输入的用户名

        if not val.isdigit():  # 判断数字类型
            return val
        else:
            raise ValidationError("用户名不能是纯数字")

    def clean_tel(self):
        val = self.cleaned_data.get("tel")
        if len(val) == 11:  # 判断长度
            return val
        else:
            raise ValidationError("手机号码必须11位")

    def clean_age(self):
        val = self.cleaned_data.get("age")
        if int(val) > 18:  # input输入的值为字符串,必须转换为int
            return val
        else:
            raise ValidationError("年龄必须满18岁以上!")

    def clean(self):  # 全局钩子
        pwd = self.cleaned_data.get("pwd")
        r_pwd = self.cleaned_data.get("r_pwd")
        if pwd and r_pwd and pwd != r_pwd:  # 判断2次密码不为空,并且2次密码不相等
            raise ValidationError("两次密码不一致")
        else:
            return self.cleaned_data  # 这句是固定写法,不能变动
View Code

修改views.py,导入UserForm类,完整代码如下:

from django.shortcuts import render, HttpResponse
from app01.form import UserForm  # 导入UserForm类

# Create your views here.
def index(request):
    return render(request, "index.html")

def adduser(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)  # 所有干净的字段以及对应的值
            # ErrorDict : {"校验错误的字段":["错误信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 获取email错误信息,返回一个错误列表,可以切片
            # print(form.errors.get("email"))
            # # 获取第一个错误信息
            # print(form.errors.get("email")[0])
            g_error = form.errors.get("__all__")  # 接收全局钩子错误信息
            if g_error:  # 判断有错误信息的情况下
                g_error = g_error[0]  # 取第一个错误信息

            # 将form和g_error变量传给adduser.html
            return render(request, "adduser.html", {"form": form, "g_error": g_error})

    else:  # 默认是get请求(地址栏输入访问时)
        form = UserForm()  # 没有表单数据的form
        return render(request, "adduser.html", {"form": form})
View Code

再次访问页面,测试密码不一致

效果如下:

 

form组件补充知识

Django内置字段

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内容后缀
 
 
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,                      文件夹路径
    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类型
    ...
View Code

你可以在里面选择属性的类型以及约束。

 

Django内置插件

TextInput(Input)
NumberInput(TextInput)
EmailInput(TextInput)
URLInput(TextInput)
PasswordInput(TextInput)
HiddenInput(TextInput)
Textarea(Widget)
DateInput(DateTimeBaseInput)
DateTimeInput(DateTimeBaseInput)
TimeInput(DateTimeBaseInput)
CheckboxInput
Select
NullBooleanSelect
SelectMultiple
RadioSelect
CheckboxSelectMultiple
FileInput
ClearableFileInput
MultipleHiddenInput
SplitDateTimeWidget
SplitHiddenDateTimeWidget
SelectDateWidget
View Code

在witgits中选择使用

 

常用插件选择

# 单radio,值为字符串
# user = fields.CharField(
#     initial=2,
#     widget=widgets.RadioSelect(choices=((1,'上海'),(2,'北京'),))
# )
 
# 单radio,值为字符串
# user = fields.ChoiceField(
#     choices=((1, '上海'), (2, '北京'),),
#     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
# )
View Code

 

周末作业:

在图书管理系统里,增加一个注册页面

要求:

1.基于forms组件做校验
  1.1 用户名不能小于4位,不能是纯数字,用户名不能重复
  1.2 密码不能小于6位,不能是纯数字
  1.3 两次密码必须一致

2.表单由forms组件渲染

3.显示错误信息

 

进阶功能
使用ajax+forms组件完成注册功能

ajax接收error信息,修改dom,来显示错误信息!

 

答案

使用form表单实现

作业提到的3点要求,在将全局钩子的时候,已经演示出来了。

那么只要合格之后,在视图函数中插入一条记录到用户表中,就可以实现功能了!

下面介绍在上面演示的项目基础上,实现这些功能

修改models.py,增加一个用户表

class User(models.Model):
    name = models.CharField(max_length=32)
    pwd = models.CharField(max_length=32)
    last_time = models.DateTimeField()
View Code

修改settings.py,注册app。最后一行添加应用名

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01',
]
View Code

使用下面2个命令生成表

python manage.py makemigrations
python manage.py migrate

手动增加一条记录

 

修改form.py,代码如下:

from django import forms  # 必须导入模块
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from app01.models import User  # 导入user表

class UserForm(forms.Form):  # 必须继承Form
    # 定义变量,专门给text类型的输入框添加class
    wid = widgets.TextInput(attrs={"class": "form-control"})
    # 定义字典,错误信息显示中文
    # 限制数据为字符串,最小长度4,最大长度12
    error_hints = {"required": "该字段不能为空", "invalid": "格式错误!"}
    name = forms.CharField(max_length=12, label="姓名", widget=wid, error_messages=error_hints)
    # 密码字段
    pwd = forms.CharField(label="密码", widget=widgets.PasswordInput(attrs={"class": "form-control"}),
                          error_messages=error_hints)

    r_pwd = forms.CharField(label="确认密码", widget=widgets.PasswordInput(attrs={"class": "form-control"}),
                            error_messages=error_hints)

    def clean_name(self):  # 校验name值
        val = self.cleaned_data.get("name")  # 获取输入的用户名
        if len(val) >= 4:  # 判断用户名长度
            if val.isdigit() is False:  # 判断用户名不是纯数字
                if not User.objects.filter(name=val).exists():  # 判断用户名是否存在
                    return val  # 返回正确的值
                else:
                    raise ValidationError("用户名已存在")
            else:
                raise ValidationError("用户名不能为纯数字")
        else:
            raise ValidationError("用户名长度不能小于4位")

    def clean_pwd(self):  # 校验pwd值
        val = self.cleaned_data.get("pwd")  # 获取输入的密码
        if len(val) >= 6:  # 判断密码长度
            if val.isdigit() is False:  # 判断密码不是纯数字
                return val  # 返回正确的值
            else:
                raise ValidationError("密码不能为纯数字")
        else:
            raise ValidationError("密码长度不能小于6位")


    def clean(self):  # 全局钩子
        pwd = self.cleaned_data.get("pwd")
        r_pwd = self.cleaned_data.get("r_pwd")
        if pwd and r_pwd and pwd != r_pwd:  # 判断2次密码不为空,并且2次密码不相等
            raise ValidationError("两次密码不一致")
        else:
            return self.cleaned_data  # 这句是固定写法,不能变动
View Code

修改views.py,代码如下:

from django.shortcuts import render, HttpResponse,redirect
from app01.form import UserForm  # 导入UserForm类
from app01.models import User
import datetime

# Create your views here.
def index(request):
    return render(request, "index.html")

def adduser(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 验证数据
            print("###success###")
            print(form.cleaned_data)  # 所有干净的字段以及对应的值
            # ErrorDict : {"校验错误的字段":["错误信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印

            name = request.POST.get("name")
            pwd = request.POST.get("pwd")
            last_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            ret = User.objects.create(name=name,pwd=pwd,last_time=last_time)
            if ret:
                # return HttpResponse("添加成功")
                return redirect('/index/')

        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 获取email错误信息,返回一个错误列表,可以切片
            # print(form.errors.get("email"))
            # # 获取第一个错误信息
            # print(form.errors.get("email")[0])
            g_error = form.errors.get("__all__")  # 接收全局钩子错误信息
            if g_error:  # 判断有错误信息的情况下
                g_error = g_error[0]  # 取第一个错误信息

            # 将form和g_error变量传给adduser.html
            return render(request, "adduser.html", {"form": form, "g_error": g_error})

    else:  # 默认是get请求(地址栏输入访问时)
        form = UserForm()  # 没有表单数据的form
    return render(request, "adduser.html",{"form": form})
View Code

修改adduser.html,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <style>
        .error {
            color: red;
        }
    </style>
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
        <h3>添加用户</h3><br/>
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }} <span class="error pull-right">{{ field.errors.0 }}</span>
                         {% if field.label == "确认密码" %}
                         <span class="error pull-right">{{ g_error|default_if_none:"" }}</span>
                         {% endif %}
                    </div>
                {% endfor %}
                <br/>
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

访问页面添加用户界面

 查看用户表记录,发现多了一条

 

ajax+forms组件实现

在上面的代码上,增加ajax功能

修改urls.py,增加路径add_ajajx

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
    path('adduser/', views.adduser),
    path('add_ajax/', views.add_ajax),
]
View Code

修改views.py,增加视图函数add_ajajx

from django.shortcuts import render, HttpResponse,redirect
from app01.form import UserForm  # 导入UserForm类
from app01.models import User
import datetime
import json

# Create your views here.
def index(request):
    return render(request, "index.html")

def adduser(request):
    if request.method == "POST":
        # 将post数据传给UserForm
        form = UserForm(request.POST)
    else:  # 默认是get请求(地址栏输入访问时)
        form = UserForm()  # 没有表单数据的form

    return render(request, "adduser.html",{"form": form})


def add_ajax(request):
    if request.method == "POST": # 判断POST请求
        print(request.POST)
        form = UserForm(request.POST)  #
        result = {"state": False,"name":"","pwd":"","r_pwd":""}
        if form.is_valid():
            name = request.POST.get("name")
            pwd = request.POST.get("pwd")
            last_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            ret = User.objects.create(name=name, pwd=pwd, last_time=last_time)
            if ret:
                result["state"] = True
            return HttpResponse(json.dumps(result,ensure_ascii=False))
        else:
            print(form.errors)
            if form.errors:  # 判断有错误信息的情况下
                if form.errors.get("name"):
                    result["name"] = form.errors.get("name")[0]
                if form.errors.get("pwd"):
                    result["pwd"] = form.errors.get("pwd")[0]
                if form.errors.get("r_pwd"):
                    result["r_pwd"] = form.errors.get("r_pwd")[0]

                g_error = form.errors.get("__all__")  # 接收全局钩子错误信息
                if g_error:  # 判断有错误信息的情况下
                    g_error = g_error[0]  # 取第一个错误信息
                    result["r_pwd"] = g_error

                return HttpResponse(json.dumps(result,ensure_ascii=False))
View Code

修改adduser.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <style>
        .error {
            color: red;
        }

        .col-center-block {

            position: absolute;
            top: 50%;
            left: 18%;
            -webkit-transform: translateY(-50%);
            -moz-transform: translateY(-50%);
            -ms-transform: translateY(-50%);
            -o-transform: translateY(-50%);
            transform: translateY(-50%);
        }


    </style>
</head>
<body>
{% csrf_token %}
<div class="container col-center-block">
    <div class="row ">
        <div class="col-md-6 col-md-offset-2">
            <h3>添加用户</h3><br/>
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }} <span class="error pull-right">{{ field.errors.0 }}</span>
                        {% if field.label == "确认密码" %}
                            <span class="error pull-right">{{ g_error|default_if_none:"" }}</span>
                        {% endif %}
                    </div>
                {% endfor %}
                <br/>
                <input type="button" class="btn btn-success btn-sm" id="sub" value="注册">
            </form>
        </div>
    </div>
</div>
<script src="/static/js/jquery.min.js"></script>
{#sweetalert插件#}
<script src="http://mishengqiang.com/sweetalert/js/sweetalert-dev.js"></script>
<link rel="stylesheet" href="http://mishengqiang.com/sweetalert/css/sweetalert.css">
<script>
    $(function () {
        $("#id_name").blur(function () {
            var csrf = $("[name=csrfmiddlewaretoken]").val();  //csrf
            var name = $("#id_name").val();  //用户名
            if (name.length != 0) {
                $.ajax({
                    url: "/zhuce_ajax/",
                    type: "post",
                    data: {
                        'name': name,
                        csrfmiddlewaretoken: csrf,
                    },
                    success: function (data) {
                        var data = JSON.parse(data);  //反序列化数据
                        console.log(data);
                        if (data.name) { //判断用户是否有错误信息
                            $("#id_name").next().text(data.name)  //修改span标签的文本
                        } else {
                            $("#id_name").next().text("")  //验证通过后,清空文件
                        }
                    }

                });
            }

        });
        $("#id_pwd").blur(function () {
            var csrf = $("[name=csrfmiddlewaretoken]").val();  //csrf
            var pwd = $("#id_pwd").val();  //密码
            if (pwd.length != 0) {
                $.ajax({
                    url: "/zhuce_ajax/",
                    type: "post",
                    data: {
                        'name': name,
                        'pwd': pwd,
                        csrfmiddlewaretoken: csrf,
                    },
                    success: function (data) {
                        var data = JSON.parse(data);  //反序列化数据
                        console.log(data);
                        if (data.pwd) { //判断密码是否有错误信息
                            $("#id_pwd").next().text(data.pwd)  //修改span标签的文本
                        } else {
                            $("#id_pwd").next().text("")  //验证通过后,清空文件
                        }
                    }

                });
            }

        });
        $("#id_r_pwd").blur(function () {
            var csrf = $("[name=csrfmiddlewaretoken]").val();  //csrf
            var pwd = $("#id_pwd").val();  //密码
            var r_pwd = $("#id_r_pwd").val();  //确认密码
            if (r_pwd.length != 0) {
                $.ajax({
                    url: "/zhuce_ajax/",
                    type: "post",
                    data: {
                        'name': name,
                        'pwd': pwd,
                        'r_pwd': r_pwd,
                        csrfmiddlewaretoken: csrf,
                    },
                    success: function (data) {
                        var data = JSON.parse(data);  //反序列化数据
                        console.log(data);
                        if (data.r_pwd) { //判断确认密码是否有错误信息
                            $("#id_r_pwd").next().text(data.r_pwd)  //修改span标签的文本
                        } else {
                            $("#id_r_pwd").next().text("")  //验证通过后,清空文件
                        }
                    }

                });
            }

        });

        $("#sub").click(function () {
            var csrf = $("[name=csrfmiddlewaretoken]").val();  //csrf
            var name = $("#id_name").val();  //用户名
            var pwd = $("#id_pwd").val();  //密码
            var r_pwd = $("#id_r_pwd").val();  //确认密码
            $.ajax({
                url: "/zhuce_ajax/",  //请求的url
                type: "post", //默认get
                data: {
                    name: name,
                    pwd: pwd,
                    r_pwd: r_pwd,
                    csrfmiddlewaretoken: csrf
                },
                success: function (data) {  //data接收响应体,必须要有
                    var data = JSON.parse(data);  //反序列化数据
                    {#console.log(data.state);#}
                    {#console.log(data);  //打印响应体#}
                    if (data.state) {
                        console.log("注册成功");
                        swal({
                            title: '注册成功',
                            type: 'success',  //展示成功的图片
                            timer: 500,  //延时500毫秒
                            showConfirmButton: false  //关闭确认框
                        }, function () {
                            window.location.href = "/index/";  //跳转首页
                        });
                    }
                    else {
                        console.log("注册失败");
                        if (data.name) { //判断用户是否有错误信息
                            $("#id_name").next().text(data.name)  //修改span标签的文本
                        } else {
                            $("#id_name").next().text("")  //验证通过后,清空文件
                        }
                        if (data.pwd) {
                            $("#id_pwd").next().text(data.pwd)
                        } else {
                            $("#id_pwd").next().text("")
                        }
                        if (data.r_pwd) {
                            $("#id_r_pwd").next().text(data.r_pwd)
                        } else {
                            $("#id_r_pwd").next().text("")
                        }

                    }
                }
            })


        })

    })
</script>

</body>
</html>
View Code

 

访问页面:

测试效果如下:

 

 查看用户表,发现多了一条记录

 

完整代码,请参考github

https://github.com/py3study/bms_multi

posted @ 2018-07-06 20:17  肖祥  阅读(1129)  评论(4编辑  收藏  举报