form组件
环境
from django import forms # 组件
from django.core.exceptions import ValidationError # 校验错误
from django.forms import widgets # 部件,定义样式
定义表单
class UserForm(forms.Form):
username = forms.CharField(min_length=5, label="用户名", widget=forms.widgets.TextInput,
error_messages={"required": "用户名不能为空", "min_length": "用户名不能少于5个字符"})
password = forms.CharField(min_length=5, label="密码", widget=forms.widgets.PasswordInput,
error_messages={"required": "密码不能为空", "min_length": "密码不能少于5个字符"})
r_passowrd = forms.CharField(min_length=5, label="确认密码", widget=forms.widgets.PasswordInput,
error_messages={"required": "确认密码不能为空", "min_length": "确认密码不能少于5个字符"})
email = forms.EmailField(min_length=5, label="邮箱", widget=forms.EmailInput,
error_messages={"required": "邮箱不能为空", "min_length": "邮箱不能少于5个字符"})
# 定义全局样式
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
# 定义验证钩子, 此为自定义校验,第一次验证已经通过,这是第二次验证
# 局部钩子
# 判断用户名是否已经存在
def clean_username(self):
val = self.cleaned_data.get('username')
user = UserInfo.objects.get(username=val)
if user:
raise ValidationError('用户名已经存在')
else:
return val # 必须返回val
#全局钩子
def clean(self):
pwd = self.cleaned_data.get('password')
r_pwd = self.cleaned_data.get('r_password')
if pwd and r_pwd and pwd != r_pwd: # 同时有值并且不相等则抛异常
raise ValidationError('两次密码不一致')
else:
return self.cleaned_data
is_valid()源码解析
form组件核心方法
-
is_valid
- self.is_bound--> True, self.errors 为 True 则校验通过
def is_valid(self): """Return True if the form has no errors, or False otherwise.""" return self.is_bound and not self.errors
-
self.errors
@property def errors(self): """Return an ErrorDict for the data provided for the form.""" if self._errors is None: self.full_clean() return self._errors
-
self.full_clean()
校验接收的表单数据
- 定义
self._errors
为字典(校验错误的字典) - 定义
self.cleaned_data
为字典(校验通过的字典) - 执行
self._clean_fields()
进行校验form字段
def full_clean(self): """ Clean all of self.data and populate self._errors and self.cleaned_data. """ self._errors = ErrorDict() if not self.is_bound: # Stop further processing. return self.cleaned_data = {} # If the form is permitted to be empty, and none of the form data has # changed from the initial data, short circuit any validation. if self.empty_permitted and not self.has_changed(): return self._clean_fields() # 基础规则 与 局部钩子 self._clean_form() # 全局钩子 不同字段间的比较 self._post_clean()
- 定义
-
self._clean_fields()
表单格式基础验证 与 局部钩子(自定义字段验证)
class UserForm(forms.Form): username = forms.CharField(min_length=6) user_form = UserForm({"username": "admin"})
-
self.fields
:form表单字典:{name:field}: {"username": username}
-
name
: form表单字段名: "username" -
field
:form表单字段对象: username 即forms.CharField()类实例化的username对象(规则对象) -
value
: 表单实例化对象后,传入的需要校验的字段的值: "admin"
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)
-
循环表单字典{name:field}
表单校验:
-
校验通过
-
value = field.clean(value)
: 校验value。self.cleaned_data[name] = value
, 存入cleaned_data
:{"username": "admin"} -
通过反射检查是否有自定义校验
'clean_%s' % name
,即 clean_字段名 方法:clean_username
,通过则self.cleaned_data[name] = value
前提为通过了第一步的校验
def clean_username(self): val = self.cleaned_data.get('username') user = UserInfo.objects.get(username=val) if user: raise ValidationError('用户名已经存在') else: return val # 必须返回val
-
-
校验不通过
-
self.add_error(name, e)
,如没有通过第一步的校验,则直接将 {"username": 错误信息} 存入self._errors
,不执行第二步的反射方法校验self._errors 字典(django.forms.utils.ErrorDict)
错误信息: 列表 (django.forms.utils.ErrorList)
表单对象.errors.get("username")[0], 取得错误描述
-
如第一步校验通过,第二步反射方法校验不通过,依然会将没有通过校验的值存入
cleaned_data
,最后add_error()
中将没有通过第二步校验的字段名、错误信息 键值对存入self._errors
,并将键值从cleaned_data
中删除
-
-
-
-
self._clean_form()
全局钩子, 不同字段间校验
add_error(None,e)
增加键为"__all__"的异常, 如: {"__all__":[" 两次的密码不一致", ], }指定form字段异常,需重写clean()方法
def _clean_form(self):
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
self.clean()
需重写
self.add_error(表单字段名,异常), 指定form表单字段异常
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
# 重写父类clean方法
def clean(self):
pwd = self.cleaned_data.get('password')
r_pwd = self.cleaned_data.get('r_password')
if pwd and r_pwd and pwd != r_pwd: # 同时有值并且不相等则抛异常
self.add_error("r_password", "两次密码不一致")
raise ValidationError('两次密码不一致')
else:
return self.cleaned_data
- 为r_password增加异常
注册登录实例
settings.py
# 用户认证组件扩展字段添加配置,
AUTH_USER_MODEL = "app01.UserInfo"
models.py
# 扩展用户组件后,原auth_user表移除,变为uaerinfo表
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class UserInfo(AbstractUser):
tel = models.CharField(max_length=32)
urls.py
from django.contrib import admin
from django.urls import path
from apps.app01 import views
urlpatterns = [
path('admin/', admin.site.urls),
path('login/', views.login),
path('get_valid_code', views.get_valid_code, name='get_valid_code'),
path('reg/', views.reg)
]
views.py
import random
import re
import io
from PIL import Image, ImageDraw, ImageFont
from django.shortcuts import render
from django.http import HttpResponse, JsonResponse
from django.contrib import auth
from django import forms
from django.core.exceptions import ValidationError
from django.forms import widgets
from apps.app01.models import UserInfo
# Create your views here.
# form 表单验证
class UserForm(forms.Form):
username = forms.CharField(min_length=5, label="用户名", widget=forms.widgets.TextInput,
error_messages={"required": "用户名不能为空", "min_length": "用户名不能少于5个字符"})
password = forms.CharField(min_length=5, label="密码", widget=forms.widgets.PasswordInput,
error_messages={"required": "密码不能为空", "min_length": "密码不能少于5个字符"})
r_password = forms.CharField(min_length=5, label="确认密码", widget=forms.widgets.PasswordInput,
error_messages={"required": "确认密码不能为空", "min_length": "确认密码不能少于5个字符"})
email = forms.EmailField(min_length=5, label="邮箱", widget=forms.EmailInput,
error_messages={"required": "邮箱不能为空", "min_length": "邮箱不能少于5个字符", "invalid": "邮箱格式错误"})
# 自定义全局样式
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
# 定义验证钩子, 此为自定义校验,第一次验证已经通过,这是第二次验证
# 局部钩子
# 判断用户名是否已经存在
def clean_username(self):
val = self.cleaned_data.get('username')
try:
UserInfo.objects.get(username=val)
except Exception as e:
return val
else:
raise ValidationError('用户名已经存在')
# 密码不能有纯数字
def clean_password(self):
val = self.cleaned_data.get('password')
if val.isdigit():
raise ValidationError('密码不能是纯数字')
else:
return val
# 邮箱必须是163邮箱
def clean_email(self):
val = self.cleaned_data.get('email')
ret = re.match(r'w+@163.com$', val)
if not ret:
raise ValidationError('必须为163邮箱')
else:
return val
# 定义全局钩子
def clean(self):
pwd = self.cleaned_data.get('password')
r_pwd = self.cleaned_data.get('r_password')
if pwd and r_pwd and pwd != r_pwd: # 同时有值并且不相等则抛异常
self.add_error("r_password", "两次密码不一致")
raise ValidationError('两次密码不一致')
else:
return self.cleaned_data
# 登录
def login(request):
if request.method == "POST":
res_code = {"user": None, "state": None}
# 取得session中网页请求中的验证码
username = request.POST.get('username')
password = request.POST.get('password')
valid_code = request.POST.get('valid_code')
session_valid_code = request.session.get('keep_str')
if valid_code.upper() == session_valid_code.upper():
user_obj = auth.authenticate(username=username, password=password)
if user_obj:
res_code['user'] = username
else:
res_code["state"] = "用户名或者密码错误"
else:
res_code["state"] = "验证码错误"
return JsonResponse(res_code)
else:
return render(request, 'login.html')
def get_random_color():
return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)
def get_random_chr():
rand_digi = str(random.randint(0, 9))
rand_upper_alpha = chr(random.randint(65, 90))
rand_lower_alpha = chr(random.randint(97, 122))
rand_chr = random.choice([rand_digi, rand_upper_alpha, rand_lower_alpha])
return rand_chr
# 验证码图片生成,生成session
def get_valid_code(request):
img = Image.new('RGB', (160, 35), get_random_color())
draw = ImageDraw.Draw(img)
font = ImageFont.truetype('static/fonts/msMonaco.ttf', 32)
# 验证码图片设置文字
keep_str = ""
for i in range(4):
rand_chr = get_random_chr()
draw.text((i * 30 + 20, 0), rand_chr, get_random_color(), font=font)
keep_str += rand_chr
# 图片加噪点
width = 160
height = 35
# 画线
for i in range(10):
x1 = random.randint(0, width)
x2 = random.randint(0, width)
y1 = random.randint(0, height)
y2 = random.randint(0, height)
draw.line((x1, y1, x2, y2), fill=get_random_color())
# 画点和弧
for i in range(10):
draw.point([random.randint(0, width), random.randint(0, height)], fill=get_random_color())
x = random.randint(0, width)
y = random.randint(0, height)
draw.arc((x, y, x + 4, y + 4), 0, 90, fill=get_random_color())
# 图片验证码保存在session中,login调用比对
request.session['keep_str'] = keep_str
# 内存读写
f = io.BytesIO()
img.save(f, "png")
data = f.getvalue()
return HttpResponse(data)
# 注册
def reg(request):
if request.method == 'POST':
print(request.POST)
res = {"user": None, "err_msg": None}
form = UserForm(request.POST)
if form.is_valid():
res["usr"] = request.POST.get("username")
else:
res["err_msg"] = form.errors
return JsonResponse(res)
else:
user_form = UserForm()
return render(request, 'reg.html', {"user_form": user_form})
login.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<script src="{% static 'js/jquery.js' %}"></script>
<body>
<h1>Login Page</h1>
<div class="container">
<div class="row">
<div class="col-md-4">
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<label for="username">用户名</label>
<input type="text" class="form-control" id="username" name="username">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<div class="form-group">
<label for="valid_code">验证码</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="valid_code" name="valid_code">
</div>
<div class="col-md-6">
<img src="{% url 'get_valid_code' %}" width="160" height="35" alt="" id="img">
</div>
</div>
</div>
<div class="form-group">
<input type="button" class="btn btn-primary pull-right" id="valid_btn" value="登陆">
<span class="err_msg"></span>
</div>
</form>
</div>
</div>
</div>
<script>
$("#valid_btn").click(function () {
const username = $("#username").val();
const password = $("#password").val();
const valid_code = $("#valid_code").val();
$.ajax({
url: "",
type: "post",
data: {
csrfmiddlewaretoken: $('[name="csrfmiddlewaretoken"]').val(),
username: username,
password: password,
valid_code: valid_code,
},
success: function (response) {
console.log(response.user);
if (response.user) {
console.log('ok');
location.href = 'http://www.baidu.com'
} else {
$(".err_msg").html(response.state).css("color", "red")
}
}
})
});
// 验证码点击刷新
$("#img").click(function () {
this.src += "?"
})
</script>
</body>
</html>
reg.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<script src="{% static 'js/jquery.js' %}"></script>
<body>
<h1>register Page</h1>
<div class="container">
<div class="row">
<div class="col-md-4">
<form action="">
{% csrf_token %}
{% for field in user_form %}
<div class="form-group">
<label for="{{ field.label }}">{{ field.label }}</label>
{{ field }} <span class="errs pull-right"></span>
</div>
{% endfor %}
<div class="form-group">
<input type="button" class="btn btn-primary pull-left" id="reg_btn" value="注册">
<span class="err_msg"></span>
</div>
</form>
</div>
</div>
</div>
<script>
$("#reg_btn").click(function () {
$.ajax({
url: "",
type: "post",
data: {
username: $("#id_username").val(),
password: $("#id_password").val(),
r_password: $("#id_r_password").val(),
email: $("#id_email").val(),
csrfmiddlewaretoken: $('[name="csrfmiddlewaretoken"]').val(),
},
success: function (response) {
// 点击先清除错误信息
$(".errs").html("");
// 移除class属性为form-group的标签的 has-error属性,此处为input标签的父标签div标签
$(".form-group").removeClass("has-error");
// 循环标签, 为显示错误的span添加错误信息
$.each(response.err_msg, function (i, j) {
console.log(i, j);
$("#id_" + i).next().html(j[0]).css("color", "red").parent().addClass("has-error")
// 链式操作
// i 字段名, j 错误信息
// "#id_"+i 对应字段名为i的input标签
// next() input标签的下一个标签: span标签
// html(j[0], span标签文本赋值, j[0], js数组的第一个值
// css("color", "red") 添加css样式
// parent() 父标签,span的父标签 即<div calss="form-group">标签
// addClass("has-error") 添加div标签class属性
})
}
})
})
</script>
</body>
</html>