forms组件与源码分析、modelform组件
一、forms组件
forms组件介绍
我们之前在HTML页面中利用form表单向后端提交数据时,都会写一些获取用户输入的标签并且用form标签把它们包起来.
与此同时我们在好多场景下都需要对用户的输入做校验,比如校验用户是否输入,输入的长度和格式等正不正确... 如果用户输入的内容有错误就需要在页面上相应的位置显示对应的错误信息.
Django form组件就实现了上面所述的功能, forms组件的主要功能如下:
- 自动校验数据
- 自动生成标签
- 自动展示信息
在用到forms组件的之前首先需要定义forms组件, 然后才能使用.
接下来都以用户注册作为示例.
Form定义
要定义Form组件, 需要与模型表一一对应, 下面是模型表的定义
from django import forms
class MyForm(forms.Form):
username = forms.CharField(min_length=3, max_length=8) # username字段最少三个字符最大八个字符
age = forms.IntegerField(min_value=0, max_value=200) # 年龄最小0 最大200
email = forms.EmailField() # 必须符合邮箱格式
校验数据的功能(初识)
小需求:获取用户数据并发送给后端校验 后端返回不符合校验规则的提示信息
这里我们使用测试的方式检验校验数据的功能,因此以下代码都写在测试文件中。
form_obj = views.MyForm({'username':'jason','age':18,'email':'123'})
form_obj.is_valid() # 1.判断数据是否全部符合要求
False # 只要有一个不符合结果都是False
form_obj.cleaned_data # 2.获取符合校验条件的数据
{'username': 'jason', 'age': 18}
form_obj.errors # 3.获取不符合校验规则的数据及原因
{'email': ['Enter a valid email address.']}
1.只校验类中定义好的字段对应的数据 多传的根本不做任何操作(即多传的数据不 校验功能的判断)
2.默认情况下类中定义好的字段都是必填的
二、forms组件渲染标签
forms组件的渲染标签比较强大, 主要有三种方式
部分代码如下:
使用之前需要在视图层定义函数调用我们自己定义的form模型表
class MyForm(forms.Form):
username = forms.CharField(min_length=3, max_length=8, label='用户名', initial='假阳比真阳更可怕',
)
password = forms.CharField(min_length=3, max_length=8, label='密码',
)
confirm_pwd = forms.CharField(min_length=3, max_length=8, label='确认密码')
email = forms.EmailField(required=False)
def ab_forms_func(request):
# 1.产生一个空对象
form_obj = MyForm()
if request.method == 'POST':
form_obj = MyForm(request.POST) # request.POST可以看成是一个字典 直接传给forms类校验 字典中无论有多少键值对都没关系 之在乎类中编写的
if form_obj.is_valid(): # 校验数据是否合法
print(form_obj.cleaned_data)
else:
print(form_obj.errors)
# 2.将该对象传递给html文件
return render(request, 'formsPage.html', locals())
html文件中的代码
<body>
{#<p>forms组件渲染标签的方式1(封装程度过高 扩展性差 主要用于本地测试):</p>#}
{# {{ form_obj.as_p }}#}
{# {{ form_obj.as_ul }}#}
{# {{ form_obj.as_table }}#}
{#<p>forms组件渲染标签的方式2(封装程度过低 扩展性高 编写麻烦)</p>#}
{# {{ form_obj.username.label }}#}
{# {{ form_obj.username }}#}
{# {{ form_obj.age.label }}#}
{# {{ form_obj.age }}#}
{# {{ form_obj.email.label }}#}
{# {{ form_obj.email }}#}
{#<p>forms组件渲染标签的方式3(封装程度较高 扩展性高 编写简单 推荐使用)</p>#}
<form action="" method="post" novalidate>
{% for form in form_obj %}
<p>
{{ form.label }}
{{ form }}
<span>{{ form.errors.0 }}</span>
</p>
{% endfor %}
<input type="submit">
</form>
</body>
forms组件渲染标签的方式1
<p>forms组件渲染标签的方式1(封装程度过高 扩展性差 主要用于本地测试):</p>
{# {{ form_obj.as_p }}#}
{# {{ form_obj.as_ul }}#}
{# {{ form_obj.as_table }}#}
图片中的名称之所以是中文,因为在后端设置了label属性,然后在前端使用他充当标签名称。
通过第二张图片,我们可以发现第一种渲染方式,label标签和input是直接绑定的,我们并不能设置前端中Username的渲染方式。同时我们也可以看到所有后端设置过的字段的限制条件,都是可以在前端找到具体代码的。
forms组件渲染标签的方式2
<p>forms组件渲染标签的方式2(封装程度过低 扩展性高 编写麻烦)</p>
{# {{ form_obj.username.label }}#}
{# {{ form_obj.username }}#}
{# {{ form_obj.age.label }}#}
{# {{ form_obj.age }}#}
{# {{ form_obj.email.label }}#}
{# {{ form_obj.email }}#}
通过上面的图片我们可以发现第二种方法内如果不设置label标签就不自动渲染label标签了,就算我们添加了前端代码进行展示,也可以对他进行自定义
后端代码给前端form标签添加样式
如果我们想要在后端给前端的标签添加样式,需要在forms组件的模型表中添加,代码如下:(别忘了导入bootstrap)
class RegForm(forms.Form):
# 接下来的定义需要与模型表的字段类型一一对应
username = forms.CharField(
max_length=15, # 用户名最大长度为15
min_length=3, # 用户名的最小长度为3
label='用户名', # 渲染出在页面上的标签的名字
widget=forms.TextInput(attrs={'class': 'form-control'})
)
password = forms.CharField(
max_length=15, # 密码最大长度为15
min_length=3, # 密码的最小长度为3
label='密码', # 渲染出在页面上的标签的名字
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
)
re_password = forms.CharField(
max_length=15, # 密码最大长度为15
min_length=3, # 密码的最小长度为3
label='确认密码', # 渲染出在页面上的标签的名字
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
)
email = forms.EmailField(
label='邮箱',
widget=forms.EmailInput(attrs={'class': 'form-control'})
)
ps:如果想要批量添加样式,需要用派生方法自定义一个双下init方法,在创建对象、添加字段信息的时候统一添加
class RegForm(forms.Form):
# 接下来的定义需要与模型表的字段类型一一对应
username = forms.CharField(
max_length=15, # 用户名最大长度为15
min_length=3, # 用户名的最小长度为3
label='用户名', # 渲染出在页面上的标签的名字
)
password = forms.CharField(
max_length=15, # 密码最大长度为15
min_length=3, # 密码的最小长度为3
label='密码', # 渲染出在页面上的标签的名字
)
...
from django.forms import widgets
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].widget.attrs.update({
'class': 'form-control'
})
forms组件渲染标签的方式3
虽然第二种方法扩展性高,但是当字段个数很多的时候,工作量太大,而第三种渲染方式就能解决这个问题。
第三种方式是用for循环的方式获取字段信息,然后进行渲染。封装程度较高,同时扩展性高,编写简单。
<p>forms组件渲染标签的方式3(封装程度较高 扩展性高 编写简单 推荐使用)</p>
{# {% for form in form_obj %}#}
{# <p>#}
{# {{ form.label }}#}
{# {{ form }}#}
{# </p>#}
{# {% endfor %}#}
注意事项
forms组件之负责渲染获取用户数据的标签 也就意味着form标签与按钮都需要自己写
前端的校验是弱不禁风的 最终都需要后端来校验 所以我们在使用forms组件的时候可以直接取消前端帮我们的校验
<form action="" novalidate>
三、forms组件展示信息
当前端获取的数据值不符合我们后端设置的条件时,我们应当把判断的代码放到后端(毕竟前端的代码可以该,不靠谱),然后返回错误信息给前端,给用户提示。
实现这个需求,它的代码中的关键点就是用forms组件的模型表产生对象。当前端发送GET请求的时候定义一个变量名,调用模型表产生一个空对象,当我们接收数据的时候(POST),再次产生一个对象,这个对象名称跟GET请求下的空对象名称一致。这样设置后,前端就不需要进行更多的设置,当我们发送GET请求的时候不会有报错信息,所以不会展示form_obj.errors的信息,当请求为POST的时候如果有报错信息就会展示报错提示信息。
针对这个需求,实现代码如下:
后端不同请求返回的forms对象一定要是相同的变量名
def ab_forms_func(request):
# 1.产生一个空对象
form_obj = MyForm()
if request.method == 'POST':
form_obj = MyForm(request.POST) # request.POST可以看成是一个字典 直接传给forms类校验 字典中无论有多少键值对都没关系 只在乎类中编写的
if form_obj.is_valid(): # 校验数据是否合法
print(form_obj.cleaned_data)
else:
print(form_obj.errors)
# 2.将该对象传递给html文件
return render(request, 'formsPage.html', locals())
html部分代码
{% for form in form_obj %}
<p>
{{ form.label }}
{{ form }}
<span>{{ form.errors.0 }}</span>
</p>
{% endfor %}
同时我们也可以用正则进行筛选,然后设置错误提示信息
先导入模块
from django.core.validators import RegexValidator
phone = forms.CharField(
validators=[
RegexValidator(r'^[0-9]+$', '请输入数字'),
RegexValidator(r'^159[0-9]+$', '数字必须以159开头'),
],
)
针对错误信息的提示可以修改成各国语言
方式1:自定义内容
在模型表中给字段对象添加errors_messages参数
age = forms.IntegerField(min_value=0, max_value=200, label='年龄',
error_messages={
'min_value':'年龄不能小于0岁',
'max_value':'你他喵的200岁以上?',
'required':'年龄不能为空 你妹的'
}
)
email = forms.EmailField()
方式2:修改系统语言环境
from django.conf import global_settings django内部真正的配置文件
在这个文件内部我们可以看到每个语言对应的字符,之后我们在配置文件中修改即可更改语言环境
四、forms组件校验补充
forms组件针对字段数据的校验 提供了三种类型的校验方式(可以一起使用)
第一种类型:直接填写参数 max_length
第二种类型:使用正则表达式 validators
第三种类型:钩子函数 编写代码自定义校验规则
这里需要特别说一下三种校验方式的执行顺序,是从上到下依次执行的,其中钩子函数是先执行局部钩子再执行全局钩子。
局部钩子
所谓的局部钩子,就是调取一部分数据出来,对这部分数据设置自定义的筛选条件,筛选后,对应的函数需要把数据return会去(可以看成把某个产品的一部分取出来检查,然后检查完了要塞回去)。
全局钩子
全局钩子类似局部钩子,唯一不同的是,他是把所有的数据都拿来进行校验了,所以在设置返回值的时候,我们需要把所有的数据(也就是要把数据对象返回)都返回。
代码:
class MyForm(forms.Form):
username = forms.CharField(min_length=3, max_length=8)
password = forms.CharField(min_length=3, max_length=8)
confirm_pwd = forms.CharField(min_length=3, max_length=8)
# 钩子函数>>>:校验的最后一环 是在字段所有的校验参数之后触发
# 局部钩子:每次只校验一个字段数据 校验用户名是否已存在
def clean_username(self):
username = self.cleaned_data.get('username')
if username == 'jason':
self.add_error('username', '用户名jason已存在')
return username
# 全局钩子:一次可以校验多个字段数据 校验两次密码是否一致
def clean(self):
password = self.cleaned_data.get('password')
confirm_pwd = self.cleaned_data.get('confirm_pwd')
if not password == confirm_pwd:
self.add_error('confirm_pwd', '两次密码不一致')
return self.cleaned_data
五、forms组件参数补充
min_length 最小字符
max_length 最大字符
min_value 最小值
max_value 最大值
label 字段注释
error_messages 错误提示
validators 正则校验器
initial 默认值
required 是否必填
widget 控制标签的各项属性
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control', 'username': 'jason'})
六、forms组件源码剖析
切入口:form_obj.is_valid()
我们需要知道,只有执行了is.valid方法后才能使用errors方法和cleaned_data查看数据的校验情况。
因此我们从这里点进源码,可以发现is_valid()这个函数返回值是self.is_bound和self.errors,两者用and连接,而结果是通过判断两者的数据值所代表的布尔值得出的。
def is_valid(self):
"""Return True if the form has no errors, or False otherwise."""
return self.is_bound and not self.errors
而is_bound是BaseForm这个类中的双下init的一个属性,errors是BaseForm的一个内置方法。这BaseForm是Form的父类,而这个form就是模型表的类在创建时候继承的父类。同时我们查看源码我们发现这个Form内部没有代码只是继承了BaseForm。
接着我们深入研究errors函数,我们发现这个函数内部的if语句的判断条件时self._errors这个属性,并且在没有传参设置的情况下是None,回到errors函数这里,if条件在_errors属性为None的时候会执行full_clean方法。这里的代码可以在BaseForm下方一些的方法中找到,如果看不懂代码在干嘛也没事,我们可以看注释嘛。
"""
Clean all of self.data and populate self._errors and self.cleaned_data.
"""
如果想要仔细研究内部的代码我们会发现内部代码只是在设置三个隐藏属性或方法的值。
首先是定义了两个空的字典或对象.
self._errors = ErrorDict()
self.cleaned_data = {}
其中第一个隐藏方法_clean_fields是获取内部所有的字段名称和字段对象(for循环的name代表的就是字段名称,field就是字段对象)。接着我们看下面的异常捕获代码,可以发现下面的代码就是用反射的方式获取钩子函数的执行结果,然后把结果返回出去,如果出现异常就会使用ValidationError这个方法进行报错(因此我们可以在外面使用这个方法进行modelform组件的主动报错)
第二个隐藏方法_clean_form是直接在内部上异常捕获了,在第一块代码中发现他执行了self.clean,也就是全局钩子,通过他的异常捕获我们发现如果我们没有传钩子参数,他会给我们传一个cleaned_data回去,因为不传这个参数就获取不到钩子函数的结果了。这也解释了为什么我们要在返回值处返回勾出来的数据对象,同时当我们不返回这个对象的时候代码也会帮我们自动返回数据对象。
第三个方法则是什么都没写。
通过注释我们得知他就是清空这三个属性的作用。因此我们可以推断出在is_valid方法内返回值中的self.errors因为默认情况下值为空所以布尔值为False,因为返回值中的判断条件有个not在前面所以通常来说后面部分的布尔值是True。
接下来研究一个前面部分的is_bound方法的结果,我们可以发现他是类代码创建对象是设置的一个属性,主要是用于判断是否在data和files这里接收到了参数,如果接收了参数就会通过or条件判断布尔值,而data和files是最前面的两个位置参数,因此通常来说数据都是这两个参数接收的,调用的时候布尔值通常都是True。
因此我们可以得知,is_valid方法是通过钩子函数进行校验布尔值结果的,返回的结果虽然是布尔值,但这只是表象。
七、modelform组件
什么是modelform组件?
这是一个神奇的组件,通过名字我们可以看出来,这个组件的功能就是把model和form组合起来。先来一个简单的例子来看一下这个东西怎么用:
比如我们的数据库中有这样一张学生表,字段有姓名,年龄,爱好,邮箱,电话,住址,注册时间等等一大堆信息,现在让你写一个创建学生的页面,你的后台应该怎么写呢?
首先我们会在前端一个一个罗列出这些字段,让用户去填写,然后我们从后天一个一个接收用户的输入,创建一个新的学生对象,保存起来。
用之前学的方式创建模型表,很慢很繁琐。
我们现在有个更方便的方法:ModelForm
使用校验性组件的目的
- 我们学习校验性组件的目的 绝大部分是为了数据录入数据库之前的各项审核
- forms组件使用的时候需要对照模型类编写代码 不够方便
- forms组件的强化版本 更好用更简单更方便!!!
常用参数介绍
model = models.Book # 对应的Model中的类
fields = "__all__" # 字段,如果是__all__,就是表示列出所有的字段
exclude = None # 排除的字段
labels = None # 提示信息
help_texts = None # 帮助提示信息
widgets = None # 自定义插件
error_messages = None # 自定义错误信息
代码展示
modelform提供了一个save方法,可以帮我们保存数据。(可以代替ORM的create和update操作)
from django import forms
from app01 import models
class MyModelForm(forms.ModelForm):
class Meta:
model = models.UserInfo
fields = '__all__'
labels = {
'username':'用户名'
}
def ab_mf_func(request):
modelform_obj = MyModelForm()
if request.method == 'POST':
modelform_obj = MyModelForm(request.POST,instance=User_obj)
if modelform_obj.is_valid():
modelform_obj.save() # models.UserInfo.objects.create(...)/update(...)
else:
print(modelform_obj.errors)
return render(request, 'modelFormPage.html', locals())