forms组件
一 校验字段功能
用户注册示例:
models.py
from django.db import models
# Create your models here.
class UserForm(forms.Form): # 必须继承forms.Form
name = forms.CharField(min_length=3, max_length=8)
pwd = forms.CharField(min_length=3, max_length=8)
re_pwd = forms.CharField(min_length=3, max_length=8)
email = forms.EmailField()
phone = forms.CharField(min_length=11, max_length=11)
执行数据库迁移命令
python manage.py makemigrations
python manage.py migrate
urls.py
from django.contrib import admin
from django.urls import path
from app01 import views
urlpatterns = [
path('admin/', admin.site.urls),
path('register/', views.register)
]
register.html
<body>
<form action="" method="post">
<p>用户名:
<input type="text" name="name">
</p>
<p>密码:
<input type="password" name="pwd">
</p>
<p>确认密码:
<input type="password" name="re_pwd">
</p>
<p>邮箱:
<input type="email" name="email">
</p>
<p>手机号:
<input type="text" name="phone">
</p>
<p>
<input type="submit" value="提交">
</p>
</form>
</body>
views.py
from django.shortcuts import render, HttpResponse
# Create your views here.
from django import forms # 引入forms组件
class UserForm(forms.Form): # 必须继承forms.Form
name = forms.CharField(min_length=3, max_length=8)
pwd = forms.CharField(min_length=3, max_length=8)
re_pwd = forms.CharField(min_length=3, max_length=8)
email = forms.EmailField()
phone = forms.CharField(min_length=11, max_length=11)
def register(request):
if request.method == 'POST':
print(request.POST)
# <QueryDict: {'name': ['jasper'], 'pwd': ['123'], 're_pwd': ['1'],
# 'email': ['593718731@qq.com'], 'phone': ['152000']}>
# 1. 生成一个UserForm对象 将前端发送的form表单传递的QueryDict交给forms组件校验
form_obj = UserForm(request.POST) # form表单的input标签中的name属性必须和forms组件中的字段名一致 不一致则不会校验
print(form_obj.is_valid()) # False
# 2. 校验是否有效
if form_obj.is_valid(): # is_valid只有当所有校验都通过才会返回True 否则返回False
print(form_obj.cleaned_data)
else:
# cleaned_date存放的是所有校验通过的数据
print(form_obj.cleaned_data) # {'name': 'jasper', 'pwd': '123', 'email': '593718731@qq.com'}
# errors存放的是所有校验不通过的数据
print(form_obj.errors)
# <ul class="errorlist">
# <li>re_pwd<ul class="errorlist"><li>Ensure this value has at least 3 characters (it has 1).</li></ul></li>
# <li>phone<ul class="errorlist">
# <li>Ensure this value has at least 11 characters (it has 6).</li></ul></li></ul>
# ErrorDict
print(type(form_obj.errors)) # <class 'django.forms.utils.ErrorDict'>
print(form_obj.errors.get('re_pwd'))
# <ul class="errorlist"><li>Ensure this value has at least 3 characters (it has 1).</li></ul>
# 用列表来存放错误信息
print(type(form_obj.errors.get('re_pwd'))) # <class 'django.forms.utils.ErrorList'>
# 获取到具体的报错信息
print(form_obj.errors.get('re_pwd')[0]) # Ensure this value has at least 3 characters (it has 1).
return HttpResponse('收到')
return render(request, 'register.html')
注意:
- 引入forms组件的类,定义时必须继承forms.Form,forms类定义的属性默认不能为空。
- 生成的forms对象,只要传入属性有的键值对就可以校验。
但我们需要校验的是form表单提交的数据和forms组件定义好的属性,所以将request.POST传入。def register(request): if request.method == 'POST': form_obj = UserForm({'name': 'jasper', 'pwd': '123', 're_pwd': '321'}) if form_obj.is_valid(): print(form_obj.cleaned_data) else: print(form_obj.cleaned_data) # {'name': 'jasper', 'pwd': '123', 're_pwd': '321'} print(form_obj.errors) # <ul class="errorlist"><li>email<ul class="errorlist"><li>This field is required.</li></ul></li> # <li>phone<ul class="errorlist"><li>This field is required.</li></ul></li></ul>
forms组件的字段名应该和form表单中的name属性值相同,相同才会匹配上进行校验,forms表单中没有的就不校验。 - is_valid()方法返回布尔值,当所有的校验都通过才返回True,否则返回False,与forms组件无关的键值不影响结果。
- cleaned_data:通过校验的数据存放在cleaned_data中,字典数据类型。
- errors:没有通过校验的数据存放的地方,可以看作是一个字典,校验不通过的字段名为键,错误信息为键值,键值是列表数据类型,可以通过get方法拿到字段的错误信息列表在取索引0。
二 渲染标签功能
2.1 方式一
views.py
from django.shortcuts import render, HttpResponse
# Create your views here.
from django import forms # 引入forms组件
class UserForm(forms.Form): # 必须继承forms.Form
name = forms.CharField(min_length=3, max_length=8, label='用户名') # label是自定义的标签名
pwd = forms.CharField(min_length=3, max_length=8, label='密码')
re_pwd = forms.CharField(min_length=3, max_length=8, label='确认密码')
email = forms.EmailField()
phone = forms.CharField(min_length=11, max_length=11)
def register(request):
obj = UserForm()
return render(request, 'register.html', locals())
register.html
<body>
<h3>渲染的方式一</h3>
<form action="">
<p>
{{ obj.name.label }}
{{ obj.name }}
</p>
<p>
{{ obj.pwd.label }}
{{ obj.pwd }}
</p>
<p>
{{ obj.re_pwd.label }}
{{ obj.re_pwd }}
</p>
<p>
{{ obj.email.label }}
{{ obj.email }}
</p>
<p>
{{ obj.phone.label }}
{{ obj.phone }}
</p>
<input type="submit" value="提交">
</form>
</body>
渲染效果
2.2 方式二
与方式一的原理一致,只是使用了for循环
<h3>渲染的方式二</h3>
<form action="">
{% for foo in obj %}
<p>
<label for="">{{ foo.label }}</label>
{{ foo }}
</p>
{% endfor %}
<input type="submit" value="提交">
</form>
2.3 方式三
调用form对象的组件:as_p和as_ul,即可完成渲染。缺点是结构固定,扩展性查。
<h3>渲染的方式三</h3>
<form action="">
{{ obj.as_p }}
</form>
三 显示错误提示
register.html
<form action="" method="post" novalidate> <!--取消前端校验-->
{% for foo in form_obj %}
<p>
<label for="">{{ foo.label }}</label>
{{ foo }} <span>{{ foo.errors.0 }}</span>
</p>
{% endfor %}
<input type="submit" value="提交">
</form>
views.py
def register(request):
form_obj = UserForm() # 创建一个空的forms对象 每一次执行此函数都是空的对象
if request.method == 'POST': # 当前端发送post请求时
form_obj = UserForm(request.POST) # 就将前端form表单传过来的QueryDict进行校验
print(form_obj.is_valid())
if form_obj.is_valid():
print(form_obj.cleaned_data)
# 如果是get请求 前端接受到的form_obj是一个空的 errors也是空的 所以span标签不会展示
# 如果是post请求 前端返回的是被重新赋值后的对象 如果有错误信息 前端就会拿到并展示
return render(request, 'register.html', locals())
效果:
四 widgets
是用来引入bootstrap样式的。
register.html
<div class="container">
<h1 class="text-center">注册功能</h1>
<div class="row">
<div class="col-md-4 col-md-offset-4">
<form action="" method="post" novalidate>
{% for foo in form_obj %}
<p>
<label for="">{{ foo.label }}</label>
{{ foo }}<span style="color: red">{{ foo.errors.0 }}</span>
</p>
{% endfor %}
<input type="submit" value="提交" class="btn btn-success">
</form>
</div>
</div>
</div>
views.py
from django import forms # 引入forms组件
from django.forms import widgets
class UserForm(forms.Form): # 必须继承forms.Form
# forms.CharField和forms.EmailField会渲染为input标签
name = forms.CharField(min_length=3, max_length=8, label='用户名',
error_messages={'required': '用户名不能为空'},
widget=widgets.TextInput(attrs={'class': 'form-control'})
)
pwd = forms.CharField(min_length=3, max_length=8, label='密码',
widget=widgets.PasswordInput(attrs={'class': 'form-control'}) # 密文文本输入框
)
re_pwd = forms.CharField(min_length=3, max_length=8, label='确认密码',
widget=widgets.PasswordInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(label='邮箱', error_messages={'required': '邮箱地址不能为空', 'invalid': '格式错误'},
widget=widgets.EmailInput(attrs={'class': 'form-control'})
)
phone = forms.CharField(min_length=11, max_length=11, error_messages={'required': '手机号不能为空'},
widget=widgets.TextInput(attrs={'class': 'form-control'}) )
效果
注意:
- 在字段构造中设置error_messages参数修改字段错误信息提示:其中required表示字段不能为空的提示,invalid表示字段格式错误的提示信息。
- 在字段构造函数中配置input类型:设置为文本域、密码域、单选框、复选框等等类型。
五 钩子函数
5.1 局部钩子
检验单个数据
# forms组件类中
def clean_name(self):
value = self.cleaned_data.get('name')
from app01 import models
user_obj = models.UserInfo.objects.filter(name=value).first()
if user_obj:
return self.add_error('name', '用户名已存在')
return value
5.2 全局钩子
可以检验多条数据
from django.shortcuts import render
# Create your views here.
from django import forms
from django.forms import widgets
class UserForm(forms.Form):
name = forms.CharField(min_length=3, max_length=8, label='用户名',
error_messages={'required': '用户名不能为空'},
widget=widgets.TextInput(attrs={'class': 'form-control'})
)
pwd = forms.CharField(min_length=3, max_length=8, label='密码',
widget=widgets.PasswordInput(attrs={'class': 'form-control'}) # 密文文本输入框
)
re_pwd = forms.CharField(min_length=3, max_length=8, label='确认密码',
widget=widgets.PasswordInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(label='邮箱', error_messages={'required': '邮箱地址不能为空', 'invalid': '格式错误'},
widget=widgets.EmailInput(attrs={'class': 'form-control'})
)
phone = forms.CharField(min_length=11, max_length=11, error_messages={'required': '手机号不能为空'},
widget=widgets.TextInput(attrs={'class': 'form-control'})
)
def clean(self):
name = self.cleaned_data.get('name')
pwd = self.cleaned_data.get('pwd')
re_pwd = self.cleaned_data.get('re_pwd')
if not pwd == re_pwd:
self.add_error('re_pwd', '两次输入密码不一致')
return
from app01 import models
obj = models.UserInfo.objects.filter(name=name).first()
if obj:
self.add_error('name', '用户名已存在')
return
return self.cleaned_data
def register(request):
form_obj = UserForm()
if request.method == 'POST':
form_obj = UserForm(request.POST)
if form_obj.is_valid():
from app01 import models
form_obj.cleaned_data.pop('re_pwd')
models.UserInfo.objects.create(**form_obj.cleaned_data)
return render(request, 'register.html', locals())
六 forms组件源码分析
def register(request):
form_obj = UserForm()
if request.method == 'POST':
form_obj = UserForm(request.POST)
if form_obj.is_valid():
from app01 import models
form_obj.cleaned_data.pop('re_pwd')
models.UserInfo.objects.create(**form_obj.cleaned_data)
return render(request, 'register.html', locals())
上边写的代码可以得出,如果forms组件校验成功,那么form_obj.is_valid() 是返回True的,所以先查看is_valid()。
def is_valid(self):
"""Return True if the form has no errors, or False otherwise."""
return self.is_bound and not self.errors
self.is_bound 必须是True而且 self.errors 必须是False
self.is_bound = data is not None or files is not None # True
form_obj = UserForm(request.POST) # 实例化对象
class UserForm(forms.Form): #继承了Form类
pass
class Form(BaseForm, metaclass=DeclarativeFieldsMetaclass): # 又继承了BaseForm 元类是产生类的类
pass
class BaseForm(RenderableFormMixin):
def __init__(
self,
data=None, # data=request.POST Query_set类型
files=None,
pass
data肯定不是空,那么self.is_bound 是True。
@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._errors默认为None 会走self.full_clean()
self._errors = None # Stores the errors after clean() has been called.
self._errors默认为None 会走self.full_clean()
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.
# 判断form表单是否允许为空 并且没有数据 默认不为空 empty_permitted=False,
if self.empty_permitted and not self.has_changed():
return
self._clean_fields() # 字段校验和局部钩子函数校验
self._clean_form()
self._post_clean()
self._clean_fields()
def _clean_fields(self):
for name, bf in self._bound_items():
field = bf.field # form表单传过来的name属性值
value = bf.initial if field.disabled else bf.data # 判断标签是否被禁用 真实数据值
try:
if isinstance(field, FileField): # 判断是不是文件字段 我们没有传文件 所以不走这
value = field.clean(value, bf.initial)
else:
value = field.clean(value) # 真正的校验
self.cleaned_data[name] = value # 没有报错就说明验证成功 将属性名和数据值放入cleaned_data中
# 局部钩子函数校验
# 判断forms组件中是否与clean_属性名这个属性
if hasattr(self, "clean_%s" % name):
# 有的话就调用这个局部钩子函数
# 并且接受一个返回值
value = getattr(self, "clean_%s" % name)()
# 钩子函数校验成功的话需要返回一个正确的数据值 需要存放到cleaned_data中
# 校验不成功的话 必须return一个None
self.cleaned_data[name] = value
# 如果钩子函数函数出错的话 就将错误信息添加到errors中
except ValidationError as e:
self.add_error(name, e)
def add_error(self, field, error):
...
if field in self.cleaned_data:
del self.cleaned_data[field]
# 1.for name, bf in self._bound_items():
def _bound_items(self):
"""Yield (name, bf) pairs, where bf is a BoundField object."""
for name in self.fields:
yield name, self[name] # 字典的键和值
self.fields = copy.deepcopy(self.base_fields)
# base_fileds就是form表单提交的数据,是字典类型
# {'name': <django.forms.fields.CharField object at 0x000001AE84ADD7C0> ...}
_clean_form()
def _clean_form(self):
try:
cleaned_data = self.clean() # 调用全局钩子函数
except ValidationError as e:
self.add_error(None, e)
else: # 如果没有报错 并且cleaned_data不为空就重新赋值
if cleaned_data is not None:
self.cleaned_data = cleaned_data
==Form类中的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__'.
"""
# 验证的所有错误将于__all__字段关联
# 直接返回了cleaned_data
return self.cleaned_data
这就是为什么使用全局钩子的时候需要返回一个完整的cleaned_data, 否则将是一个不完整的数据
七 补充
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类型
常用选择插件
# 单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
# )
八 modelform组件
from django.shortcuts import render
# Create your views here.
from django.forms import ModelForm # 导入ModelForm组件
from app01 import models
from django.forms import widgets
class UserModelForm(ModelForm):
class Meta:
model = models.UserInfo
fields = "__all__" # 校验所有字段
# fields = ['name', 'pwd', 're_pwd'] # 校验列表中有的字段
# exclude = ['email'] # 列表中有的不校验
labels = {'name': '用户名'} # 字段的别名 前端渲染展示
widgets = {
# 不同类型的字段要用不同的属性输出,不然表单的格式验证失效
# 给不同字段添加class属性,改变样式
'name': widgets.TextInput({'class': 'form-control'}),
'pwd': widgets.PasswordInput({'class': 'form-control'}),
're_pwd': widgets.PasswordInput({'class': 'form-control'}),
'email': widgets.EmailInput({'class': 'form-control'}),
'phone': widgets.TextInput({'class': 'form-control'})
}
error_messages = {
'email': {'invalid': '输入正确的格式'},
}
def clean_name(self): # 自定义钩子函数
value = self.cleaned_data.get('name')
obj = models.UserInfo.objects.filter(name=value).first()
if obj:
return self.add_error('name', '用户名已存在')
return value
# 操作于forms组件大差不差
def register(request):
form_obj = UserModelForm()
if request.method == 'POST':
form_obj = UserModelForm(request.POST)
print(form_obj.base_fields)
if form_obj.is_valid():
from app01 import models
# form_obj.cleaned_data.pop('re_pwd')
models.UserInfo.objects.create(**form_obj.cleaned_data)
return render(request, 'register.html', locals())