django表单校验组件forms

前戏

# 需求:写一个注册功能
获取用户名和密码,利用form表单提交数据
在后端判断用户名和密码是否符合一定的条件:
    用户名中不能含有'金瓶子'
    密码不能少于三位	
如何不符合条件需要你将提示信息展示到前端页面

前端显示

<form action="" method="post">
    <p>username:
        <input type="text" name="username">
        <span style="color: red">{{ back_dic.username }}</span>
    </p>
    <p>password:
        <input type="text" name="password">
        <span style="color: red">{{ back_dic.password }}</span>
    </p>
    <input type="submit" class="btn btn-info">
</form>

后端逻辑

def ab_form(request):
    response_dict = {'username': '', 'password': ''}
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        if '金瓶子' in username:
            response_dict['username'] = '不符合社会主义核心价值观'
        if len(password) < 3:
            response_dict['password'] = '不能太短 不好!'
   
    return render(request,'ab_form.html',locals())

"""
总结:
不论是get请求还是post请求,前端页面在渲染的时候都会收到response_dict中的数据。
get请求时,两个key对应的value每数据,所以前端页面渲染时没有任何数据显示
post请求时,两个key对应的value有数据,所以前端位置就显示了提示信息。
"""

总结:

  • 上面注册验证功能我们手写获取用户数据的html代码、后端手动通过逻辑判断做表单校验、对不符合条件的数据手动将提示信息展示在前端页面
  • 这种操作是繁琐、低效且容易出错的。
  • django自带的表单校验组件forms会帮我们完成上述三个需求。

数据一定要在后端做校验,前端校验是可有可无的(弱不禁风的,或者利用爬虫程序绕过前端页面直接朝后端提交数据)

forms组件基本使用

创建Form

from django import forms


class MyForm(forms.Form):
    username = forms.CharField(min_length=5, max_length=12, label='用户名')
    password = forms.CharField(min_length=6, max_length=12, label='密码')
    re_password = forms.CharField(min_length=6, max_length=12, label='确认密码')
    email = forms.EmailField(label='邮箱')
    
"""
总结:
# username字符串类型最小3位最大8位
# password字符串类型最小3位最大8位
# email字段必须符合邮箱格式  xxx@xx.com
# label属性用来指定字段展示信息

"""

校验数据

# 测试环境
- 1.测试环境的准备 可以自己拷贝代码准备
- 2.其实在pycharm里面已经帮你准备一个测试环境(python console)

使用第二种测试方式

from app01 import views

form_obj = views.MyForm({'username': 'the3times', 'password': '123456'})
form_obj.is_valid()
False
form_obj.cleaned_data
{'username': 'the3times', 'password': '123456'}
form_obj.errors
{'re_password': ['This field is required.'], 'email': ['This field is required.']}
form_obj.has_error('email')
True


"""
# 总结1:
给MyForm传值实例化对象,传值方式:将带校验的字段和数据组织成字典的形式
is_valid()		该方法只有在所有的数据全部合法的情况下才会返回True
cleaned_data	查看所有校验通过的数据
errors			查看所有不符合校验规则以及不符合的原因,{key: ['']}
has_error()		判断某一个字段是否不合法,不合法返回True


# 总结2:
校验数据只校验类中出现的字段,多传不影响,多传的字段直接忽略
校验数据 默认情况下 类里面所有的字段都必须传值,即少传肯定不合法

"""

渲染标签

forms组件只会自动渲染标签(input select radio checkbox),不能帮你渲染提交按钮。

后端视图函数

def index(request):
    form_obj = MyForm()		                        # 1 先产生一个空对象
    return render(request,'index.html',locals())	# 2 直接将该空对象传递给html页面

渲染标签的三种方式

<p>第一种渲染方式:代码书写极少,封装程度太高 不便于后续的扩展 一般情况下只在本地测试使用</p>
    {{ form_obj.as_p }}
    {{ form_obj.as_ul }}
    {{ form_obj.as_table }}
    
    
<p>第二种渲染方式:可扩展性很强 但是需要书写的代码太多  一般情况下不用</p>
    <p>{{ form_obj.username.label }}:{{ form_obj.username }}</p>
    <p>{{ form_obj.password.label }}:{{ form_obj.password }}</p>
    <p>{{ form_obj.email.label }}:{{ form_obj.email }}</p>
    
    
<p>第三种渲染方式(推荐使用):代码书写简单 并且扩展性也高</p>
    {% for form in form_obj %}
    	<p>{{ form.label }}:{{ form }}</p>
    {% endfor %}
    
    
"""
label属性默认展示的是类中定义的字段首字母大写的形式
也可以自己修改 直接给字段对象加label属性即可
username = forms.CharField(min_length=3, max_length=8, label='用户名')
"""

展示提示信息

禁止浏览器自动校验的设置

<form action="" method="post" novalidate>	# 增加参数 novalidate

前端页面预留显示提示信息的span标签

<form action="" method="post" novalidate>
    {% for form in form_obj %}
        {{ form.label }}:
        <p>
            {{ form }}
            <span style="color: red">{{ form.errors.0 }}</span>
        </p>
    {% endfor %}

    <input type="submit" value="提交">
</form>

后端逻辑判断

def register(request):

    form_obj = MyForm()

    if request.method == 'POST':
        form_obj = MyForm(request.POST)
        if form_obj.is_valid():
            return HttpResponse('ok')

    return render(request, 'register.html', locals())

'''
总结:
不论是get请求还是post请求,前端页面都可以接收名字为 form_obj的对象;
区别在于get请求时该对象时空对象,没有值和错误信息,所以此时前端页面什么只有标签没有任何数据和提示信息。
post请求后,前端页面收到的时有数据的form_obj,以及非法字段的提示信息。
这样有两个好处:前端post提交失败时保留数据;并显示提示信息。

'''

自定制错误信息

forms默认的错误信息是英文的,可以通过参数error_messages自定制错误信息。

class MyForm(forms.Form):
    username = forms.CharField(min_length=5,
                               max_length=12,
                               label='用户名',
                               error_messages={
                                   'min_length': '用户名不能少于5位',
                                   'max_length': '用户名不能超多12位',
                                   'required': '用户名不能为空',
                               }
                               )
    password = forms.CharField(min_length=6, max_length=12, label='密码')
    re_password = forms.CharField(min_length=6, max_length=12, label='确认密码')
    email = forms.EmailField(label='邮箱')
    
    
"""
error_messages参数需要构造成字典的数据结构,key是校验条件,value是校验失败时的提示信息

"""

钩子函数(hook)

上述校验是forms的第一道校验,如果希望自定一些特殊的校验规则,可以使用钩子函数。

钩子函数在forms组件中类似于第二道关卡,能够让我们自定义校验规则。

校验流程通过第一道关卡后就会来到钩子函数,我们可以在钩子函数里面进一步定制校验规则。

钩子函数分两种:局部钩子、全局钩子

# 局部钩子:给单个字段添加校验规则
# 全局钩子:给多个字段添加校验规则

局部钩子使用:用户名不能包含'666'

class MyForm(forms.Form):
    username = forms.CharField(min_length=5,
                               max_length=12,
                               label='用户名',
                               error_messages={
                                   'min_length':'用户名不能少于5位',
                                   'max_length':'用户名不能超多12位',
                                   'required':'用户名不能为空',
                               })

    def clean_username(self):
        username = self.cleaned_data.get('username')
        if '666' in username:
            self.add_error('username', '不要666的没完没了')
        return username
    
"""
总结:
定义方法:clean_字段()
该方法中在cleaned_data中获取该字段的数据,局部钩子中只能拿到当前字段的数据
校验数据失败时,通过add_error方法给字段添加错误信息,最后一定要返回该字段
局部钩子取出的字段数据一定要返回出去

"""

全局钩子的使用:两次输入密码一致

class MyForm(forms.Form):
    
    password = forms.CharField(min_length=6, max_length=12, label='密码')
    re_password = forms.CharField(min_length=6, max_length=12, label='确认密码')
   
    def clean(self):
        password = self.cleaned_data.get('password')
        re_password = self.cleaned_data.get('re_password')
        if password != re_password:
            self.add_error('re_password', '两次密码输入不一致')

        return self.cleaned_data

    
"""
总结:
全局钩子内可以在cleaned_data内获得任意字段的数据,
全局钩子取出的cleaned_data一定要返回出去
"""

其他参数及知识点

常用参数

min_length			最少几位字符
max_length			最多几位字符
label				字段名
required  			控制字段是否必填(默认required=True)
error_messages  	        自定义报错信息,字典的结构
initial  			初始值,input框里面的初始值value

widget参数

增加校验字段的样式属性,修改input标签的type类型,通过参数widget

class MyForm(forms.Form):
    username = forms.CharField(min_length=5,
                               max_length=12,
                               widget=forms.widgets.TextInput(attrs={'class': 'form-control'}))

"""
总结:
widget=forms.widgets.TextInput()默认是TextInput(及input[type='text'])
widget=forms.widgets.PasswordInput()密码格式
widget=forms.widgets.EmailInput()邮箱格式

attrs提供字段的属性,可以是内置的也可以是自定义的;如有多个class:空格隔开即可。
"""

补充:每个字段都通过widget参数增加设置属性很繁琐,可以通过类的构造方法循环设置每个字段的属性。

class MyForm(forms.Form):
    username = forms.CharField(min_length=5,
                               max_length=12,
                               widget=forms.widgets.TextInput())
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 批量增加属性
        for field in self.fields:
            self.fields[field].widget.attrs.update({'class': 'form-control'})

validators参数

第一道关卡里面还支持正则校验, 通过参数validators

from django import forms
from django.core.validators import RegexValidator

class MyForm(forms.Form):
 
    phone = forms.CharField(label='手机号',
                            validators=[
                                RegexValidator(r'^[0-9]+$', '请输入数字'),
                                RegexValidator(r'^159[0-9]+$', '数字必须以159开头')
                            ],)
   
"""
总结:
validators的值是一个列表,烈面可以更多个正则表校验条件
RegexValidator第一个参数是正则表校验条件,第二个是校验失败是的提示信息

"""

其他类型渲染

# radio单选
gender = forms.ChoiceField(
    choices=((1, "男"), (2, "女"), (3, "保密")),
    label="性别",
    initial=1,
    widget=forms.widgets.RadioSelect()
)

# checkbox多选
hobby = forms.MultipleChoiceField(
    choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
    label="爱好",
    initial=[1, 3],
    widget=forms.widgets.CheckboxSelectMultiple()
)


# 下拉单选:即使添加attrs={'multiple': 'multiple'}任然为单选
hobby2 = forms.ChoiceField(
    choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
    label="爱好",
    initial=3,
    widget=forms.widgets.Select()
)

# 下拉多选
hobby3 = forms.MultipleChoiceField(
    choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
    label="爱好2",
    initial=[1, 3],
    widget=forms.widgets.SelectMultiple()
)

# 选择checkbox是否选择, initial空表示False, 只要有值就是True默认选中(任何值都可以)
keep = forms.ChoiceField(
    choices=(('False', 0), ('True', 1)),
    label="是否记住密码",
    initial='',
    widget=forms.widgets.CheckboxInput()
)

choices参数注意

如果choicee展示的数据是动态从数据库中取出的,需要额外的设置。

class BookAddForm(forms.Form):
    name = forms.CharField(label='图书名称',
                           error_messages={'required': '图书名称不能为空'},
                           widget=forms.widgets.TextInput())
    
    price = forms.DecimalField(label='价格',
                               error_messages={'required': '图书价格不能为空'},
                               widget=forms.widgets.TextInput())
    
    publish_date = forms.DateField(label='出版日期',
                                   error_messages={'required': '出版日期不能为空'},
                                   widget=forms.widgets.DateInput())
    
    publish_id = forms.ChoiceField(label='出版社',
                                   error_messages={'required':'出版社不能为空'},
                                   widget=forms.widgets.Select())
    
    author = forms.MultipleChoiceField(label='作者',
                                       error_messages={'required':'作者不能为空'},
                                       widget=forms.widgets.SelectMultiple())

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # choices字段的数据动态来自数据库
        self.fields['publish_id'].choices = models.Publish.objects.values_list('pk', 'name')
        self.fields['author'].choices = models.Author.objects.values_list('pk', 'name')

        # 批量增加属性
        for field in self.fields:
            self.fields[field].widget.attrs.update({'class': 'form-control'})

钩子函数源码

原本想自己记录总结这部分内容,无意间发现一篇介绍非常详细的博客,点击查看我就不写了

关于源码的阅读,需要注意两点:知道自己想看什么只看自己能看懂的

总结和补充

  • 自己定义的局部钩子和全局钩怎么被调用的
# 从is_valid()函数出发进入self.errors再到self.full_clean()
在full_clean()方法内,会依次调用三个方法
self._clean_fields()
self._clean_form()
self._post_clean()

第1个方法内会通过反射调用我们自己定义的局部钩子函数;
第2个方法内会调用self.clean()全局钩子函数。
  • 为什么局部钩子要返回校验字段的数据,全局钩子要返回self.cleaned_data
"""
self._clean_fields()内通过反射动态判断form_obj对象是否定义了局部钩子函数,如果定义了就立即加括号执行并且通过一个value变量接收执行的返回值。该value变量会再赋值为该字段对应的数据值。
这就是为啥要要校验之后还需要再将数据值返回的原因,如果该局部钩子没有返回值则默认返回None,这样就将该字段的数据值丢失了。


全局钩子函数也是一致的,self._clean_form()方法内,直接调用self.clean()。并且clean()其实是一个接口,这个接口内部仅仅返回了self.cleaned_data,外部需要这个返回值赋值给self.cleaned_data.
所以我写全局钩子是也要返回这个self.cleaned_data, 否则通过form_obj.cleaned_data就拿不到数据了。

"""
  • 钩子函数是第二道关卡,只有经过了第一道管卡才会来到钩子函数。那么在钩子函数内校验失败时,cleaned_data里面就不应该有这个字段及其数据。解决这个问题的关键在于add_error()方法。
"""
我们在钩子函数内做校验时,校验失败是通过add_error(field, error_info)方法添加错误字段和错误提示信息的。
add_error()内部添加错误信息后会再判断该字段是否在clened_data这个字典内,如果存在则会将其从该字典中删除。
"""
  • 添加校验错误方式,主动抛出异常
"""
看源码之后发现,forms源码也使用了异常捕获或添加错误信息。
所以其实我们也可以主动抛出异常来将错误信息添加到errors中,但这种方式操作起来不简洁,不如add_error()用的方便。
"""
posted @ 2020-06-04 19:18  the3times  阅读(438)  评论(0编辑  收藏  举报