用户名登陆
需求: 用户认证相关的功能, 两个方面 - 用户名和密码登录、短信登录.. 登陆成功后,在 文件/数据库/缓存 中保存用户信息的session..
# 用户名登陆(初版)
大致思路如下:
先在页面上展示, 输入用户名和密码, 提交数据..
再根据数据去数据库中进行校验.. 若成功,用户信息写入session; 若失败, 在页面上展示错误信息.
# 路由配置
"""总路由"""
from django.urls import path
from web.views import account
urlpatterns = [
path('login/', account.login, name='login'),
path('sms/login/', account.sms_login, name='sms_login'),
]
2
3
4
5
6
7
8
# 登陆页面
# 注意的点!!
模版层页面的编写,需要注意的点:
1> 注意!! 路由层那,路由最前面无需加/ ; 在模版层,路由前面需加 /
href="/sms/login/" -- 自动拼接路径是 127.0.0.1/sms/login
href="sms/login/" -- 自动拼接路径是 127.0.0.1/login/sms/login
2> <a href="{% url 'sms_login' %}" style="float: right">短信登陆</a> 使用name自动生成路由
3> {% load static %}
<link rel="stylesheet" href="{% static 'plugins/bootstrap-3.4.1/css/bootstrap.css' %}">
动态的引入静态文件.
一般会在app下创建4个目录,css、js、images、plugins
4> {% csrf_token %} <!-- 前后端分离的话,是用不到它的 -->
5> 左边是输入框,右边是`发送短信`的按钮,可以用Bootstrap的珊格系统实现.
6> 要有个单选框,让登陆用户选择是管理员还是客户.
2
3
4
5
6
7
8
9
10
11
12
# 用户名登陆页面代码
http://127.0.0.1:8000/login/
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户名密码登陆</title>
<link rel="stylesheet" href="{% static 'plugins/bootstrap-3.4.1/css/bootstrap.css' %}">
<style>
.box {
width: 480px;
margin-left: auto; /* 盒子水平居中 */
margin-right: auto; /* 盒子水平居中 */
border: 1px solid #f0f0f0;
margin-top: 100px;
padding: 20px;
box-shadow: 5px 10px 10px rgb(0 0 0 / 5%); /* 阴影 */
color: gray;
}
a {
text-decoration: none;
color: #5cb85c;
opacity: 0.6;
}
a:hover {
text-decoration: none;
color: #5cb85c;
opacity: 0.9;
}
</style>
</head>
<body>
<div class="box">
<h3 style="text-align: center;">用户名登陆</h3>
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<div class="row">
<div class="col-md-4">
<select class="form-control" name="role">
<option value="1">管理员</option>
<option value="2">客户</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" name="username" placeholder="请输入用户名" class="form-control">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" placeholder="请输入密码" class="form-control">
</div>
<input type="submit" value="登陆" class="btn btn-success">
<span style="color: brown" id="error">{{ error }}</span>
<a href="{% url 'sms_login' %}" style="float: right">短信登陆</a>
</form>
</div>
<script>
document.getElementsByClassName("box")[0].onclick = function () {
document.getElementById("error").innerHTML = ""
}
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 短信登陆页面代码
http://127.0.0.1:8000/sms/login/
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>短信登陆</title>
<link rel="stylesheet" href="{% static 'plugins/bootstrap-3.4.1/css/bootstrap.css' %}">
<style>
.box {
width: 480px;
margin-left: auto; /* 盒子水平居中 */
margin-right: auto; /* 盒子水平居中 */
border: 1px solid #f0f0f0;
margin-top: 100px;
padding: 20px;
box-shadow: 5px 10px 10px rgb(0 0 0 / 5%); /* 阴影 */
color: gray;
}
a {
text-decoration: none;
color: #5cb85c;
opacity: 0.6;
}
a:hover {
text-decoration: none;
color: #5cb85c;
opacity: 0.9;
}
</style>
</head>
<body>
<div class="box">
<h3 style="text-align: center;">短信登陆</h3>
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<div class="row">
<div class="col-md-4">
<select class="form-control" name="role">
<option value="1">管理员</option>
<option value="2">客户</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label>手机号</label>
<input type="text" name="mobile" placeholder="请输入手机号" class="form-control">
</div>
<div class="form-group">
<label>短信验证码</label>
<div class="row">
<div class="col-md-9">
<input type="text" name="code" placeholder="请输入短信验证码" class="form-control">
</div>
<div class="col-lg-3">
<input type="button" value="发送短信" class="btn btn-default" style="float: right">
</div>
</div>
</div>
<input type="submit" value="登陆" class="btn btn-success">
<a href="{% url 'login' %}" style="float: right">用户名登陆</a>
</form>
</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 视图函数
# 注意的点!!
`http://127.0.0.1:8000/login/` -- login函数
-- GET请求返回的是登陆页面
-- POST请求,提交数据到当前页面的地址.
1> login函数获取表单信息 username、password、role
并利用Form组件 & ModelForm组件 对获取的数据进行校验!! -- 初版中没有,在第二版中加
2> 将password转换成加密后的密码.(通过加盐的md5进行的加密) 数据库中存储的密码不可能是明文的!
3> 根据role找到数据库相应的表,根据username、password看对象是否存在.
注: 若该客户或管理员已经被删除了,是不允许登陆的!! 所以查询条件还要加上active=1
- 若不存在,返回登陆页面,在登陆按钮后面显示 "用户名或密码错误",点击盒子任一地方,错误信息消失.
- 若存在,将用户信息存储到session中.并进入项目后台.
注: session是存储在redis缓存中的!
★[思考]
1> 思考一个问题,返回登陆页面,显示错误信息,,会进行刷新页面的操作!!用户在input框里填写的内容会被清除
若input框很多,用户体验会很差!!
2> 若用户在表单里填写的用户名和密码为空,后端接受到了,肯定是不合法的,还去数据库中进行验证?压根就不用了.
So,在查询数据库之前,需要对前端传递过来的数据进行 <数据格式或是否为空的验证>!!
像这样吗?
if not username:
return render(request, "login.html", {"error": "用户名或密码错误!"})
No,字段少还好办,字段一多就难顶啦!!
解决方案:使用Form组件 & ModelForm组件!!
★[小细节]
这里有个小细节,订单系统使用配置文件的形式来设置菜单和权限 + 在表单里选择角色时,传给后端的是数字"1"、"2"
eg: 在settings文件里这样配置.
MENU = {
"ADMIN":[],
"CUSTOMER":[],
}
PERMISSION = {
"ADMIN":{},
"CUSTOMER":{},
}
在视图函数里,建立对应关系,mapping = {"1": "ADMIN", "2": "CUSTOMER"},更加的直观!!
request.session['user_info'] = {'role': mapping[role], 'name': user_obj.username, 'id': user_obj.pk}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 初版代码
"""
web.views.account 账户相关的,用户名密码登陆、短信登陆、注销
"""
from django.shortcuts import render, redirect
from web import models
from utils.encrypt import md5
def login(request):
if request.method == "GET":
return render(request, "login.html")
username = request.POST.get("username")
password = request.POST.get("password")
password = md5(password)
role = request.POST.get("role")
mapping = {"1": "ADMIN", "2": "CUSTOMER"}
if role not in mapping:
return render(request, "login.html", {"error": "选择的角色不存在!"})
if role == "1":
user_obj = models.Administrator.objects.filter(active=1, username=username, password=password).first()
else:
user_obj = models.Customer.objects.filter(active=1, username=username, password=password).first()
if not user_obj:
return render(request, "login.html", {"error": "用户名或密码错误!"})
request.session['user_info'] = {'role': mapping[role], 'name': user_obj.username, 'id': user_obj.pk}
return redirect("/home/")
def sms_login(request):
return render(request, "sms_login.html")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 简单测试
"""
写两个离线脚本 init_admin.py 和 init_customer.py 通过md5加密创建 管理员和客户,进行测试!!
管理员: dc - admin123!
客户: wupeiqi - wupeiqi123
egon - egon123
"""
# -- init_customer.py
import os
import sys
import django
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(base_dir)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'order.settings')
django.setup()
if __name__ == '__main__':
from web import models
from utils.encrypt import md5
level_object = models.Level.objects.create(title="VIP", percent=90) # 创建级别
models.Customer.objects.create(
username='wupeiqi',
password=md5("wupeiqi123"),
mobile='17387677890',
level=level_object,
creator_id=1
)
models.Customer.objects.create(
username='egon',
password=md5("egon123"),
mobile='18954787888',
level_id=1,
creator_id=1
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 用户名登陆(form组件)
使用Form组件 & ModelForm组件!!
解决了两个问题: 登陆失败返回"用户名或密码错误",页面会刷新,输入框里的值消失; 在查询数据库之前对接受到的数据进行验证!!
form组件的作用:
"""
username = forms.CharField(
initial="egon", # -- 设置默认值
required=True, # -- 数据校验,该表单字段不能为空!必填项.默认required=True,此行代码可不填.
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": "请输入用户名"})
)
"""
"""
def login(request):
if request.method == "GET":
form = LoginForm(initial={"username": "alex"}) # -- 可设置默认值.两处都设置了,会以此处的为准.
return render(request, "login.html", {"form": form})
form = LoginForm(data=request.POST) # -- 传入了request.POST请求体中的数据
if not form.is_valid():
return render(request, "login.html", {"form": form})
"""
1.通过几行代码,可以在页面上生成html标签内容.
2.当点击submit按钮提交数据,若登陆失败,返回的是当前登陆页面,页面会刷新,但表单的内容不会消失!
3.显示页面时,表单输入框的内容可以有默认值. 应用场景:在对表单信息进行编辑的页面,编辑页面显示默认值!!
4.可以在查询数据库之前对用户提交的表单数据的格式进行校验.
★ 不仅可以简单的校验字段值是否为空,还有更高阶的用法: 正则表达式、钩子函数(eg:校验需要打开某个文件)等.
钩子函数 def clean_字段名(self):pass 若函数体里raise异常,该字段校验不通过;若有返回值,校验通过.
综上,两个方面:
1> 生成html标签+携带数据;
2> 数据校验.
form表单提交,这两方面都会用到;ajax提交不会刷新页面,通常只会用第二方面.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 视图函数
校验失败,显示错误信息; 校验成功, 取出数据, 连接数据库再进行其它操作!!
"""
账户相关的,用户名密码登陆、短信登陆、注销
"""
from django.shortcuts import render, redirect
from web import models
from utils.encrypt import md5
from django import forms
class LoginForm(forms.Form):
role = forms.ChoiceField(
required=True, # 必填项,默认required值就为True,这行代码可不填.
choices=(("1", "管理员"), ("2", "客户"),),
# 这里的变量名role就是该select标签的name属性值
widget=forms.Select(attrs={"class": "form-control"})
)
username = forms.CharField(
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": "请输入用户名"})
)
password = forms.CharField(
widget=forms.PasswordInput(
attrs={"class": "form-control", "placeholder": "请输入密码", },
render_value=True, # 表单里,密码是默认不保留的,加了该配置后,密码也可以保留
)
)
def login(request):
if request.method == "GET":
form = LoginForm()
return render(request, "login.html", {"form": form})
# 验证接收到的数据
form = LoginForm(data=request.POST)
if not form.is_valid(): # 开始验证
return render(request, "login.html", {"form": form})
"""
username = request.POST.get("username")
password = request.POST.get("password")
password = md5(password)
role = request.POST.get("role")
"""
# print(form.cleaned_data) # {'role': '1', 'username': 'dc', 'password': 'admin123!'}
username = form.cleaned_data.get("username")
password = form.cleaned_data.get("password")
password = md5(password)
role = form.cleaned_data.get("role")
mapping = {"1": "ADMIN", "2": "CUSTOMER"}
if role not in mapping:
return render(request, "login.html", {"error": "选择的角色不存在!", 'form': form})
if role == "1":
user_obj = models.Administrator.objects.filter(active=1, username=username, password=password).first()
else:
user_obj = models.Customer.objects.filter(active=1, username=username, password=password).first()
if not user_obj:
return render(request, "login.html", {"error": "用户名或密码错误!", 'form': form})
request.session['user_info'] = {'role': mapping[role], 'name': user_obj.username, 'id': user_obj.pk}
return redirect("/home/")
def sms_login(request):
return render(request, "sms_login.html")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 页面部分代码
<body>
<div class="box">
<h3 style="text-align: center;">用户名登陆</h3>
<!-- 记得加上 novalidate -->
<form action="" method="post" novalidate>
{% csrf_token %}
<div class="form-group">
<div class="row">
<div class="col-md-4">
<!--
<select class="form-control" name="role">
<option value="1">管理员</option>
<option value="2">客户</option>
</select>
-->
{{ form.role }}
</div>
</div>
</div>
<div class="form-group">
<label>用户名</label>
<!--
<input type="text" name="username" placeholder="请输入用户名" class="form-control">
-->
{{ form.username }}
</div>
<div class="form-group">
<label>密码</label>
<!--
<input type="password" name="password" placeholder="请输入密码" class="form-control">
-->
{{ form.password }}
</div>
<input type="submit" value="登陆" class="btn btn-success">
<span style="color: brown" id="error">{{ error }}</span>
<a href="{% url 'sms_login' %}" style="float: right">短信登陆</a>
</form>
</div>
<script>
document.getElementsByClassName("box")[0].onclick = function () {
document.getElementById("error").innerHTML = ""
}
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 用户名登陆(优化)
# form对象循环
字段一多的话, 会省事很多!!
Django支持在模版里对 form = LoginForm()
form对象进行循环! 循环出来的是, 一个个的字段!
-- 默认required=True在源码中的体现!
role = forms.ChoiceField() 点ChoiceField查看源码,该类继承了Field类,Field类的__init__里required=True!!
-- role = forms.ChoiceField(label="角色")
ChoiceField类加括号,相当于类实例化,ChoiceField类继承了Field类,Field类的__init__里有lable
传入label后,self.label = label
这意味着role对象的内部有一个label成员.
2
3
4
5
6
7
# 错误信息位置
每个字段验证不通过, 出现的错误信息应该在当前字段的下方!
运用”子绝父相“显示错误信息 -- 可以简单理解为浮动起来啦,不占据标准流中的位置.有没有错误提示都不会影响页面布局!
{% for field in form %}
<div class="form-group" style="position: relative; margin-bottom: 21px">
{% if field.name == "role" %}
<div class="row">
<div class="col-md-4">
{{ field }}
</div>
</div>
{% else %}
<label>{{ field.label }}</label>
{{ field }}
{% endif %}
<span style="color: #ff6b6b; position: absolute">{{ field.errors.0 }}</span>
</div>
{% endfor %}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 表单校验的流程
Form组件目的: 生成标签 + 校验 生成标签
1> 循环 or 单独某个字段
2> label页面显示文本信息
3> 自定义错误信息位置, position下面探讨下表单校验的流程!!
# 大致流程
<依次> 执行下面三步!
第一步:
for field in fields:
try:
字段内部的校验, required=True、validators=[]、min_length=6、max_length=10
校验成功self.cleaned_data['字段名'] = 表单里对应的值
校验失败,抛出异常,且self.cleaned_data中没有该字段,下一个字段的钩子里就取不到
字段的钩子方法..
校验成功self.cleaned_data['字段名'] = 钩子方法的返回值
(上面的两个小部分在执行过程中都可能抛出异常, 若字段内部的校验抛出异常,字段的钩子方法就不会执行 )
except ValidationError as e:
放到了errors里, 键是出现错误的字段,值是错误信息 校验失败self.errors['字段名'] = 错误信息
第二步: 执行clean()方法
校验失败,self.errors["__all__"] = 错误信息
校验成功, 注意clean方法的返回值, 若返回值不是None,self.cleaned_data = 返回值
第三步: 执行_post_clean()方法★ 校验成功, self.cleaned_data中有所有校验成功的键值对; 校验失败,self.errors里有所有校验失败的信息.
Ps: 此处的self是自定义的表单校验类LoginForm的实例化对象form..
在模版里取错误,
form.errors.username.0
;
若用到了循环,field.errors.0
(这里的field是指username字段)
若是在第二步执行clean方法抛出的错误form.non_field_errors.0
form = LoginForm(data=request.POST) self.errors 是<class 'django.forms.utils.ErrorDict'> 类型的数据 假如,用户名为空,密码填入的不全是数字且密码小于6位.打印self.errors结果为. """ <ul class="errorlist"> <li>username <ul class="errorlist"> <li>用户名不能为空!</li> </ul> </li> <li>password <ul class="errorlist"> <li>密码请输入数字</li> <li>密码至少是6位!</li> </ul> </li> </ul> """ 但self.errors的本质就是字典!! self.errors["password"][0] 之所以取第一个, 因为password这个字段可能在字段内部校验过程中有多个不符合的!! 探究下,为何是这样的?! from django.forms.utils import ErrorDict 查看ErrorDict类的源码! class ErrorDict(dict):... 下面用简单的代码来解释下原理.没啥深奥的,就是面向对象的知识!! """ class ErrorDict(dict): def __str__(self): return self.as_ul() def as_ul(self): for k, v in self.items(): item = "<ul>{}</ul>".format(k) return item obj = ErrorDict() obj["k1"] = [1, 2, 3] obj["k1"] = [4, 5] print(obj) # <ul>k1</ul> """ - 稍微实践了下: if not form.is_valid(): # <ul class="errorlist"><li>mobile<ul class="errorlist"><li>手机格式错误</li></ul></li></ul> print(form.errors) # {'mobile': [ValidationError(['手机格式错误'])]} print(form.errors.as_data()) # {"mobile": [{"message": "\u624b\u673a\u683c\u5f0f\u9519\u8bef", "code": "invalid"}]} print(form.errors.as_json())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49细说下,form.non_field_errors.0 执行clean()方法的过程中报错,在源码里 self.add_error(None, e) --> self.errors["__all__"] = e (此处的self就是form) self里有non_field_errors方法!! self.non_field_errors() <=等同于=> self.errors["__all__"] 源代码如下: def non_field_errors(self): # -- command+b跳转可以发现 NON_FIELD_ERRORS = '__all__' return self.errors.get(NON_FIELD_ERRORS, self.error_class(error_class='nonfield')) 在模版语法里是不支持,form.errors.__all__ 这种写法的,会报错 So,在Django的模版里,这样写 form.non_field_errors.0 也就是在视图里写的form.non_field_errors()[0] (在模版语法里,form会找到non_field_errors,看non_field_errors是否可执行,若可执行,它会自动帮你执行!!) """该模版语法在源码里大致是这么个实现逻辑,以下是伪代码 my_str = "form.non_field_errors.xxx" data_list = my_str.split(".") obj = data_list[0] for name in data_list[1:]: data = getattr(obj, name, None) # -- 判断form有没有non_field_errors if data: if callable(data): # -- 判断non_field_errors是不是一个可执行的函数 obj = obj.data() else: obj = obj.data # -- 第一次循环后 obj = form.non_field_errors 然后进入第二次的循环,看form.non_field_errors有没有xxx """
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24[扩展] 如果想要让某个错误信息,展示在特定的字段旁,就可以使用: form.add_error("password", "用户名或密码错误")
1
2
from django import forms
from django.core.validators import RegexValidator
class LoginForm(forms.Form):
role = forms.ChoiceField()
username = forms.CharField()
password = forms.CharField(
label="密码",
min_length=6,
validators=[RegexValidator(r'^[0-9]+$', '密码请输入数字.')],
widget=forms.PasswordInput(
attrs={"class": "form-control", "placeholder": "请输入密码", },
render_value=True
),
error_messages={"min_length": "密码至少是6位!", "required": "密码不能为空!"},
)
def clean_username(self):
# -- 只要能执行到钩子函数这,就证明字段内部自身的校验是通过的,cleaned_data字典里肯定有该字段的信息
user = self.cleaned_data['username']
if len(user) < 3:
from django.core.exceptions import ValidationError
raise ValidationError("用户名格式错误")
return user
def clean_password(self):
return md5(self.cleaned_data['password'])
def clean(self): # -- 很少使用
"""
可以在自定义的表单类里定义一个名为clean()方法,再做一个整体的校验!!
1.有返回值,则将返回值赋值给self.cleaned_data!!
2.无返回值,返回默认的self.cleaned_data!!
3.抛出ValidationError异常可以捕获.
但要注意,此处是self.add_error(None, e) 把错误e给到None啦!! 而不是,self.add_error(name, e)
"""
# -- 特别注意一点,该方法里,小心使用 user = self.cleaned_data['username']
# 看源码自己可以分析出来!!
# 若username在第一步里校验不通过,还是会执行第二步的,但self.cleaned_data可能没有username!!
# 所以我们一般会先 if self.cleaned_data.get('username') 进行判断!
# -- 当然,该方法也不常用.因为按照我们的想法,我们想在所有字段都检验通过了,然后再执行clean方法.
# 但,事实是,无论第一步里的字段校验成功与否都会执行clean方法.
pass
def _post_clean(self): # -- 几乎不用
pass
def login(request):
# -- 一般我们会这样做:校验失败,显示错误信息; 校验成功, 取出数据, 连接数据库再进行其它操作!!
form = LoginForm(data=request.POST)
if not form.is_valid():
# print(form.errors["__all__"]) # -- 可以拿到在第二步里抛出的异常
return render(request, "login.html", {"form": form})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 源码分析
校验从
form.is_valid()
开始!!
form是LoginForm类的实例化对象
LoginForm类里没有is_valid方法,去它继承的Form类中找,Form类中也没有,最后在Form类继承的BaseForm类中找到!!
form = LoginForm(data=request.POST)
可以认为BaseForm里的self就是上面一行代码的form!!
class BaseForm:
def __init__(self, data=None, files=None, ...):
self.is_bound = data is not None or files is not None
self.data = MultiValueDict() if data is None else data
self.files = MultiValueDict() if files is None else files
self._errors = None
@property
def errors(self):
if self._errors is None:
self.full_clean() # -- 验证规则在full_clean方法里!!
return self._errors # -- # ★ 看懂了吗?相当于self.errors = self._errors
def is_valid(self):
# -- 因为在视图函数里执行了form = LoginForm(data=request.POST) 所以 self.is_bound 值为True.
# 再看self.errors, errors是一个被@property修饰的方法
# 此行代码表示当有数据+没有错误,返回True,表明验证通过!!
return self.is_bound and not self.errors
def full_clean(self):
self._errors = ErrorDict() # ErrorDict()!!本质就是字典.
if not self.is_bound:
return
# -- self.cleaned_data是校验成功后的字典,它默认是空的!
self.cleaned_data = {}
if self.empty_permitted and not self.has_changed():
return
self._clean_fields() # -- 依次对每个字段进行<内部的校验+钩子方法的校验>
self._clean_form() # -- 可以自定义一个名为clean的钩子函数,实现字段与字段结合的校验!!
self._post_clean() # -- 也是一个内部的钩子,让你进行校验.
"""
self._clean_form()、self._post_clean()都是给我们预留的可校验的地方..
看源码,前者可以抛出的ValidationError异常并捕获, 后者不能.
"""
def _clean_fields(self):
"""
role = forms.ChoiceField()
username = forms.CharField()
password = forms.CharField()
"""
# -- 循环得到的name是字段名 role、username、password
# 循环得到的field是 forms.ChoiceField()、...、...
for name, field in self.fields.items():
if field.disabled:
value = self.get_initial_for_field(field, name)
else:
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, FileField):
initial = self.get_initial_for_field(field, name)
value = field.clean(value, initial)
else:
# -- 1.每个字段内部的校验
# 每个字段中有个clean方法来进行校验,传递的参数value就是用户在表单里填入的数据
""" forms.ChoiceField() 点进ChoiceField查看源码,找到对应的clean方法.
def clean(self, value):
value = self.to_python(value) # 其实就是value=vlaue
self.validate(value) # 字段内部具体校验过程,有时间再探究!!
self.run_validators(value)
return value
def to_python(self, value):
return value
"""
# -- 若字段内部校验,不通过,会抛出异常,该次循环此行代码后面的不会执行啦 会被try-except捕获异常
# 所以self.cleaned_data可能不存在该name值
value = field.clean(value)
# -- 将校验通过的字段数据放到cleaned_data字典中
# eg: cleaned_data = {"username":"dc","password":"admin123!"}
self.cleaned_data[name] = value
# -- 2.会去我们在视图里自定义的LoginForm表单类里查看
# 看有没有clean_role、clean_username、clean_password方法!
if hasattr(self, 'clean_%s' % name):
# -- 如果有,直接加括号执行!!该方法里,可以调用self.cleaned_data来获取检验通过的键值对!
# 此处的self是form对象!! form = LoginForm(data=request.POST)
value = getattr(self, 'clean_%s' % name)()
"""
若self.clean_password方法执行了,并有返回值,假如return "123"
源码里,进行了覆盖操作.举个例子:
cleaned_data = {"username":"dc","password":"admin123!"} -- 每个字段的校验后的结果
会变为
cleaned_data = {"username":"123","password":"123"}
-- 这样编写钩子函数,(定义了钩子方法,又将之前校验通过的值返回了)跟没写效果一样,相当于没写:
def clean_password(self):
return self.cleaned_data["password"]
-- 钩子函数的正确打开方式
def clean_password(self):
password = self.cleaned_data["password"]
# 进行自定义的校验.如果校验通过,返回原来的值.若校验失败,抛出ValidationError类型的异常!!
from django.core.exceptions import ValidationError
if len(password) < 3:
raise ValidationError("密码格式错误!")
return password
"""
self.cleaned_data[name] = value
# -- 执行field.clean(value)方法时,检验不通过,会raise ValidationError的异常.在此处会捕获到!!
except ValidationError as e:
self.add_error(name, e) # -- 查看add_error的源码,不难看到,它会将错误放到self.errors里!!
def _clean_form(self):
"""
可以在自定义的表单类里定义一个名为clean()方法,再做一个整体的校验!!
1.有返回值,则将返回值赋值给self.cleaned_data!!
2.返回值为None, 返回默认的self.cleaned_data/啥也没做.
3.抛出ValidationError异常可以捕获.
但要注意,此处是self.add_error(None, e) 把错误e给到None啦!! 而不是,self.add_error(name, e)
把错误e给到None,这个说法也不准确,点进add_error的源码,里面有这么一行代码:
def add_error(self, field, error): # -- 传入的field为None,error是错误信息e
# -- 若field为None,取的是全局变量NON_FIELD_ERRORS
# 而 NON_FIELD_ERRORS = '__all__'
error = {field or NON_FIELD_ERRORS: error.error_list}
So,可通过self.error['__all__']拿到_clean_form方法里抛出的错误信息
"""
try:
cleaned_data = self.clean()
except ValidationError as e:
self.add_error(None, e)
else:
if cleaned_data is not None:
self.cleaned_data = cleaned_data
def clean(self):
return self.cleaned_data
def _post_clean(self):
"""
An internal hook for performing additional cleaning after form cleaning
is complete. Used for model validation in model forms.
"""
pass
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
扩展: (有空再探究)
1> self.validate(value) 字段内部具体的校验过程
2> class Form(BaseForm, metaclass=DeclarativeFieldsMetaclass): Form组件涉及到的元类怎么写的!!
1 Field !!!!公共的字段属性!!!
2 required=True, 是否允许为空
3 widget=None, HTML插件
4 label=None, 用于生成Label标签或显示内容
5 initial=None, 初始值
6 help_text='', 帮助信息(在标签旁边显示)
7 error_messages=None, 错误信息 {'required': '不能为空', 'invalid': '格式错误'}
8 validators=[], 自定义验证规则
9 localize=False, 是否支持本地化
10 disabled=False, 是否可以编辑
11 label_suffix=None Label内容后缀
12
13
14 CharField(Field)
15 max_length=None, 最大长度
16 min_length=None, 最小长度
17 strip=True 是否移除用户输入空白
18
19 IntegerField(Field)sss
20 max_value=None, 最大值
21 min_value=None, 最小值
22
23 FloatField(IntegerField)
24 ...
25
26 DecimalField(IntegerField)
27 max_value=None, 最大值
28 min_value=None, 最小值
29 max_digits=None, 总长度
30 decimal_places=None, 小数位长度
31
32 BaseTemporalField(Field)
33 input_formats=None 时间格式化
34
35 DateField(BaseTemporalField) 格式:2015-09-01
36 TimeField(BaseTemporalField) 格式:11:12
37 DateTimeField(BaseTemporalField)格式:2015-09-01 11:12
38
39 DurationField(Field) 时间间隔:%d %H:%M:%S.%f
40 ...
41
42 RegexField(CharField)
43 regex, 自定制正则表达式
44 max_length=None, 最大长度
45 min_length=None, 最小长度
46 error_message=None, 忽略,错误信息使用 error_messages={'invalid': '...'}
47
48 EmailField(CharField)
49 ...
50
51 FileField(Field)
52 allow_empty_file=False 是否允许空文件
53
54 ImageField(FileField)
55 ...
56 注:需要PIL模块,pip3 install Pillow
57 以上两个字典使用时,需要注意两点:
58 - form表单中 enctype="multipart/form-data"
59 - view函数中 obj = MyForm(request.POST, request.FILES)
60
61 URLField(Field)
62 ...
63
64
65 BooleanField(Field)
66 ...
67
68 NullBooleanField(BooleanField)
69 ...
70
71 ChoiceField(Field) #简单的性别选项 适用于不用查询数据库数据
72 ...
73 choices=(), 选项,如:choices = ((0,'上海'),(1,'北京'),)
74 required=True, 是否必填
75 widget=None, 插件,默认select插件
76 label=None, Label内容
77 initial=None, 初始值
78 help_text='', 帮助提示
79
80
81 ModelChoiceField(ChoiceField) #单项选择
82 ... django.forms.models.ModelChoiceField
83 queryset, # 查询数据库中的数据
84 empty_label="---------", # 默认空显示内容
85 to_field_name=None, # HTML中value的值对应的字段
86 limit_choices_to=None # ModelForm中对queryset二次筛选
87
88 ModelMultipleChoiceField(ModelChoiceField) #多项选择
89 ... django.forms.models.ModelMultipleChoiceField
90
91
92
93 TypedChoiceField(ChoiceField)
94 coerce = lambda val: val 对选中的值进行一次转换
95 empty_value= '' 空值的默认值
96
97 MultipleChoiceField(ChoiceField)
98 ...
99
100 TypedMultipleChoiceField(MultipleChoiceField)
101 coerce = lambda val: val 对选中的每一个值进行一次转换
102 empty_value= '' 空值的默认值
103
104 ComboField(Field)
105 fields=() 使用多个验证,如下:即验证最大长度20,又验证邮箱格式
106 fields.ComboField(fields=[fields.CharField(max_length=20), fields.EmailField(),])
107
108 MultiValueField(Field)
109 PS: 抽象类,子类中可以实现聚合多个字典去匹配一个值,要配合MultiWidget使用
110
111 SplitDateTimeField(MultiValueField)
112 input_date_formats=None, 格式列表:['%Y--%m--%d', '%m%d/%Y', '%m/%d/%y']
113 input_time_formats=None 格式列表:['%H:%M:%S', '%H:%M:%S.%f', '%H:%M']
114
115 FilePathField(ChoiceField) 文件选项,目录下文件显示在页面中
116 path, 文件夹路径
117 match=None, 正则匹配
118 recursive=False, 递归下面的文件夹
119 allow_files=True, 允许文件
120 allow_folders=False, 允许文件夹
121 required=True,
122 widget=None,
123 label=None,
124 initial=None,
125 help_text=''
126
127 GenericIPAddressField
128 protocol='both', both,ipv4,ipv6支持的IP格式
129 unpack_ipv4=False 解析ipv4地址,如果是::ffff:192.0.2.1时候,可解析为192.0.2.1, PS:protocol必须为both才能启用
130
131 SlugField(CharField) 数字,字母,下划线,减号(连字符)
132 ...
133
134 UUIDField(CharField) uuid类型
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134