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

时间:2021-08-30 02:26:48

一、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),
]

修改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")

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

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

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

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

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

from app01.views import DemoForm

效果如下:

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

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

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

测试一个字典数据

执行下面2个命令

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

效果如下:输出True

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

解释:

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

测试1:age不符合

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

3个必须同时成立才行python 全栈开发,Day78(Django组件-forms组件)

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

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

测试2:少一个字段

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

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

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

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

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

网页校验

修改urls.py,增加路径addbook

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

修改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")

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>

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

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

提交之后,效果如下:

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

Pycharm控制台输出:success

空表单直接提交

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

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")

再次提交数据

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

Pycharm控制台输出:

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

虽然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)

输入一个错误的表单

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

Pycharm控制台输出:

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

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")

Pycharm控制台输出:

###fail###
[06/Jul/2018 22:33:41] "POST /addbook/ HTTP/1.1" 200 2
{'tel': '', '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.

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})

修改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>

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

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

访问页面,效果如下:

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

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

它使用了P标签来包裹

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

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>

刷新网页,效果如下:

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

渲染方式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>

刷新网页,效果如下:

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

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="手机号码")

刷新网页,效果如下:

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

美化input输入框

需要使用bootstrap

修改urls.py,修改路径

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

修改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})

将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>

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

效果如下:

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

这里面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})

解释:

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>

刷新网页,效果如下:

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

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

保存输入信息功能

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

https://account.cnblogs.com/

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

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

那么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})

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

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

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

虽然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>

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

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

点击提交,效果如下:

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

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

查看Pycharm控制台输出:

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

发现它走了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})

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

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

数据怎么就回来了呢?

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

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

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

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

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

提交一个正确的数据

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

提示添加成功

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

显示错误信息

约定俗成,使用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>

解释:

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

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

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

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

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

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

修改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>

刷新页面,效果如下:

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

错误信息显示中文

显示中文需要在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)

解释:

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

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

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

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

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

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

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

修改UserForm类,修改这一行

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

重新提示数据

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

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

执行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})

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

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

手机号码必须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})

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

重新访问页面,测试一下

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

年龄必须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岁以上!")

重新访问页面,测试一下

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

注意:

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)

它会对表单的每一个数据,使用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]

注意:看最后一行,它会将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

谷歌翻译如下:

在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})

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

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

测试2次密码不一致

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

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

因为全局钩子和局部钩子不一样,它的错误信息在__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})

修改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>

解释:

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

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

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

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

效果如下:

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

Pycharm控制台输出:

<QueryDict: {'name': [''], 'r_pwd': [''], 'email': [''], 'age': [''], 'pwd': [''], '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>

如果只输入了1个密码呢?
python 全栈开发,Day78(Django组件-forms组件)

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

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

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

这是为什么?此时的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 # 这句是固定写法,不能变动

修改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})

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

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

效果如下:

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

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类型
...

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

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

在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
# )

周末作业:

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

要求:

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()

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

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

使用下面2个命令生成表

python manage.py makemigrations
python manage.py migrate

手动增加一条记录

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

修改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 # 这句是固定写法,不能变动

修改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})

修改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>

访问页面添加用户界面

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

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

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

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),
]

修改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))

修改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>

访问页面:

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

测试效果如下:

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

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

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

完整代码,请参考github

https://github.com/py3study/bms_multi

python 全栈开发,Day78(Django组件-forms组件)的更多相关文章

  1. Python全栈开发:django网络框架&lpar;二&rpar;

    Model 到目前为止,当我们的程序涉及到数据库相关操作时,我们一般都会这么搞: 创建数据库,设计表结构和字段 使用 MySQLdb 来连接数据库,并编写数据访问层代码 业务逻辑层去调用数据访问层执行 ...

  2. Python全栈开发:django网络框架&lpar;一&rpar;

    Python的WEB框架有Django.Tornado.Flask 等多种,Django相较与其他WEB框架其优势为:大而全,框架本身集成了ORM.模型绑定.模板引擎.缓存.Session等诸多功能. ...

  3. python全栈开发day78、79 --bss项目

    一.回顾 1. BBS项目 CMS 1. 登录 1. form组件 2. auth模块 3. 验证码 2. 注册 1. form组件 1. 生成html代码 直接for循环form_obj,就能够遍历 ...

  4. python 全栈开发,Day99&lpar;作业讲解&comma;DRF版本&comma;DRF分页&comma;DRF序列化进阶&rpar;

    昨日内容回顾 1. 为什么要做前后端分离? - 前后端交给不同的人来编写,职责划分明确. - API (IOS,安卓,PC,微信小程序...) - vue.js等框架编写前端时,会比之前写jQuery ...

  5. Python 全栈开发【第0篇】:目录

    Python 全栈开发[第0篇]:目录   第一阶段:Python 开发入门 Python 全栈开发[第一篇]:计算机原理&Linux系统入门 Python 全栈开发[第二篇]:Python基 ...

  6. Python全栈开发【面向对象】

    Python全栈开发[面向对象] 本节内容: 三大编程范式 面向对象设计与面向对象编程 类和对象 静态属性.类方法.静态方法 类组合 继承 多态 封装 三大编程范式 三大编程范式: 1.面向过程编程 ...

  7. python 全栈开发之路 day1

    python 全栈开发之路 day1   本节内容 计算机发展介绍 计算机硬件组成 计算机基本原理 计算机 计算机(computer)俗称电脑,是一种用于高速计算的电子计算机器,可以进行数值计算,又可 ...

  8. python全栈开发中级班全程笔记(第二模块、第四章)(常用模块导入)

    python全栈开发笔记第二模块 第四章 :常用模块(第二部分)     一.os 模块的 详解 1.os.getcwd()    :得到当前工作目录,即当前python解释器所在目录路径 impor ...

  9. python全栈开发之正则表达式和python的re模块

    正则表达式和python的re模块 python全栈开发,正则表达式,re模块 一 正则表达式 正则表达式(Regular Expression)是一种文本模式,包括普通字符(例如,a 到 z 之间的 ...

  10. python全栈开发目录

    python全栈开发目录 Linux系列 python基础 前端~HTML~CSS~JavaScript~JQuery~Vue web框架们~Django~Flask~Tornado 数据库们~MyS ...

随机推荐

  1. ORA-01653&colon;表空间扩展失败的问题以及增加表空间

    一.脚本修改方式: ----查询表空间使用情况---使用DBA权限登陆SELECT UPPER(F.TABLESPACE_NAME) "表空间名",D.TOT_GROOTTE_MB ...

  2. IOS UILabel 根据内容自适应高度

    iOS Label 自适应高度  适配iOS7以后的版本 更多 self.contentLabelView = [[UILabel alloc] init]; self.contentLabelVie ...

  3. mysql 的 存储结构(储存引擎)

    1 MyISAM:这种引擎是mysql最早提供的.这种引擎又可以分为静态MyISAM.动态MyISAM 和压缩MyISAM三种:    静态MyISAM:如果数据表中的各数据列的长度都是预先固定好的, ...

  4. mysql innodb 数据打捞(一)innodb 页面结构特征

    如果文件系统损坏或意外删除了数据库文件,只要磁盘空间没有被覆盖,其实数据都还在磁盘的扇区中,还是可以恢复出来的,有些通用的文件恢复工具好象也可以恢复文件 ,但这里要研究的是在通用文件 恢复工具失效的时 ...

  5. 矢量做图组件OTGisX的使用(类似Mapbase)

    一:组件添加到工具栏 要在应用程序中应用OTGisX控件,首先要把所下载的OTGisX组件添加到.Net工程中.并将其添加到工具箱托盘中.添加方式为:在工具箱上单击右键,选择“选择项”,会出现“选择工 ...

  6. Windows多线程同步系列之四-----信号量

    信号量说实话自己没怎么使用过.书上大概这样说,信号量设置一个资源访问计数.当该计数值大于0的时候,该信号量对象 为有信号状态,当该计数值等于0的时候,该信号量对象为无信号状态. 我们来查几个主要的AP ...

  7. CentOS 6&period;5下快速搭建ftp服务器&lbrack;转&rsqb;

    CentOS 6.5下快速搭建ftp服务器 1.用root 进入系统 2.使用命令 rpm -qa|grep vsftpd 查看系统是否安装了ftp,若安装了vsftp,使用这个命令会在屏幕上显示vs ...

  8. 【RDB】MariaDB 之事务、复制、集群

    目录 简介 安装启动 权限 事务 脏读.不可重复读.幻读 MVCC 复制 异步复制 半同步复制 GTID复制 集群(Galera) 配置 监控(Zabbix) 简介 环境: CentOS 7.4.17 ...

  9. Redis2&period;2&period;2源码学习——Server&amp&semi;Client链接的建立以及相关Event

    Redis中Server和User建立链接(图中的client是服务器端用于描述与客户端的链接相关的信息) Redis Server&Client链接的建立时相关Event的建立(图中的cli ...

  10. Krapo 2

    The krpano Viewer is a small and very flexible high-performance viewer for all kind of panoramic ima ...