Django 系列博客(十六)

Django 系列博客(十六)

前言

本篇博客介绍 Django 的 forms 组件。

基本属性介绍

创建 forms 类时,主要涉及到字段和插件,字段用于对用户请求数据的验证,插件用于自动生成 html。

Form 类内置字段介绍

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类型

内直插件

TextInput(Input)
NumberInput(TextInput)
EmailInput(TextInput)
URLInput(TextInput)
PasswordInput(TextInput)
HiddenInput(TextInput)
Textarea(Widget)
DateInput(DateTimeBaseInput)
DateTimeInput(DateTimeBaseInput)
TimeInput(DateTimeBaseInput)
CheckboxInput
Select
NullBooleanSelect
SelectMultiple
RadioSelect
CheckboxSelectMultiple
FileInput
ClearableFileInput
MultipleHiddenInput
SplitDateTimeWidget
SplitHiddenDateTimeWidget
SelectDateWidget

校验字段功能

在没有使用 forms 组件时,如果需要对数据进行校验,比如注册时,对用户名或者密码有位数限制等,我们需要首先获取数据然后手动进行判断,那么有了 forms 组件,这些事统统可以不用手动进行判断了,只需要把限制条件作为参数传进去那么就会自动进行判断。

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="" method="post" novalidate>
    <p>用户名: <input type="text" name="name"></p>
    <p>密码: <input type="password" name="pwd"></p>
    <p>邮箱: <input type="email" name="email"></p>
    <input type="submit" value="提交">
</form>

</body>
</html>

后端代码

from django import forms
# 继承Form这个类
class MyForm(forms.Form):
    # 限制name这个字段最长为8,最短为3
    name=forms.CharField(min_length=3,max_length=8,label='用户名',required=True,error_messages={'min_length':'至少为3','max_length':'最长为8,您太长了'})
    pwd=forms.CharField(min_length=3,max_length=8,label='密码',error_messages={'min_length':'至少为3','max_length':'最长为8,您太长了'})
    email=forms.EmailField(label='邮箱',error_messages={'invalid':'邮箱格式不合法','required':'这个字段必填'})


def register(request):
    if request.method=='GET':
        # 生成一个空的form对象
        myform=MyForm()
        return render(request,'register.html',locals())
    else:
        # 生成对象,传参,传字典,要校验数据的字典
        # myform=MyForm(request.POST)
        # 自己写要校验的字典,数据多了,多的是不校验的,但是cleaned_data中也没有多出来的数据
        # dic={'name':'lqz','pwd':'123','email':'22@qq.com','xx':'xxx'}
        # dic={'name':'lqz','pwd':'123','email':'22'}
        dic={'name':'lqzfgsdfgsdf','pwd':'1','email':'5555'}
        myform = MyForm(dic)
        # 所有字典都校验通过,它就是True的
        if myform.is_valid():
            # 取出校验通过的数据
            clean_data=myform.cleaned_data
            print(clean_data)
            # models.UserInfo.objects.create(**clean_data)
        else:
            # 所有的错误信息
            # 只要是校验通过的值,都在cleaned_data中放着
            print(myform.cleaned_data)
            print(myform.errors.as_data())
            # 字典类型
            print(type(myform.errors))
            print(myform.errors.get('name'))
            from  django.forms.utils import ErrorDict
    return HttpResponse('ok')

渲染标签功能

  1. 渲染方式一
<h1>forms的模板渲染之一(推荐)</h1>
<form action="" method="post" novalidate>
    <p>用户名: {{ myform.name }}</p>
    <p>密码: {{ myform.pwd }}</p>
    <p>邮箱: {{ myform.email }}</p>
    <input type="submit" value="提交">
</form>
  1. 渲染方式二
<h1>forms的模板渲染之二(推荐)</h1>
<form action="" method="post" novalidate>
    {% for item in myform %}
        <p>{{ item.label }}:{{ item }}</p>
    {% endfor %}

    <input type="submit" value="提交">
</form>
  1. 渲染方式三
<h1>forms的模板渲染之三</h1>
<form action="" method="post" novalidate>
{#    <table>#}
{#            {{ myform.as_table }}#}
{#    </table>#}

    {{ myform.as_p }}
    <input type="submit" value="提交">
</form>

渲染错误信息功能

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.css">
    <script src="/static/jquery-3.3.1.js"></script>
    <title>注册</title>
</head>
<body>
<div class="row">
    <div class="col-md-6 col-md-offset-3">
        <form action="" method="post" novalidate>
            {% for foo in myform %}
                <p>{{ foo.label }}:{{ foo }} <span class="pull-right">{{ foo.errors.0 }}</span></p>
            {% endfor %}
            <input type="submit" value="提交"><span>{{error_all.0 }}</span>


        </form>
        <hr>

        <form action="" method="post" novalidate>
            <p>用户名:{{ myform.name }} <span>{{ myform.errors.name }}</span></p>
            <p>用户名:{{ myform.pwd }} <span>{{ myform.errors.pwd }}</span></p>
            <p>用户名:{{ myform.email }} <span>{{ myform.errors.email }}</span></p>
            <input type="submit" value="提交">
        </form>


    </div>
</div>

</body>
</html>

后端代码

from app01.MyForm import RegForm


def register(request):
    if request.method == 'GET':
        myform = RegForm()
    else:
        myform = RegForm(request.POST)
        if myform.is_valid():
            # 存数据库,保存这个人
            print(myform.cleaned_data)
        else:
            # 校验不通过,这里面也可能有值
            # 总错误信息{'name':['太长了',],'pwd':['太短了']}
            # print(myform.cleaned_data)
            # 总错误
            # myform.glo_err=myform.errors.get('__all__')
            error_all = myform.errors.get('__all__')
            # print()
            # 每一个的错误信息
            # for item in myform:
            #     # ['太长了',]
            #     # print(item.errors[0])
            #     # print(type(item.errors))
            #     from django.forms.utils import ErrorList
            #     # print(item.errors.as_data())
            #     print(item.errors)
            #     # print(type(item.errors))
    return render(request, 'register.html', locals())

组件的参数设置

class RegForm(forms.Form):
    name = forms.CharField(max_length=8, min_length=3, label='用户名',
                           error_messages={'max_length': '太长了',
                                           'min_length': '太短了',
                                           'required': '该项不能为空'
                                           },
                           widget=widgets.TextInput(attrs={'class': 'form-control'})
                           )
    pwd = forms.CharField(max_length=8, min_length=3, label='密码',
                          error_messages={'max_length': '太长了',
                                          'min_length': '太短了',
                                          'required': '该项不能为空'
                                          },
                          widget=widgets.PasswordInput(attrs={'class': 'form-control'})
                          )
    re_pwd = forms.CharField(max_length=8, min_length=3, label='确认密码',
                          error_messages={'max_length': '太长了',
                                          'min_length': '太短了',
                                          'required': '该项不能为空'
                                          },
                          widget=widgets.PasswordInput(attrs={'class': 'form-control'})
                          )
    email = forms.EmailField(label='邮箱', error_messages={'invalid': '格式不正确',
                                                         'required': '该项不能为空'
                                                         },
                             widget=widgets.EmailInput(attrs={'class': 'form-control'})
                             )

局部钩子

继承自 Form类的校验类,可以对类中的每个字段进行校验,校验通过后的数据会放在 clean_data中,没通过的放在errors中,可以通过字段名取值,那么校验通过的字段如果还需要进行其他的判断,比如是否有敏感词,这些单单依靠字段的校验是完成不了的。所以 Django 提供了局部钩子函数,当每个字段进行校验完成后会自动调用该钩子函数。看源码就很清楚了。

首先,所有的数据校验都是调用了 Form 对象的 is_valid()方法进行的。所以进入is_valid()方法。

self.is_bound为 True,所以进入self.errors方法,为什么没加括号呢,因为被property装饰了,变成了属性。

self._errorsNone,进入full_clean()方法。

在这个方法里,首先定义了cleaned_data为一个空字典,然后就是下面的三个方法:

  1. _clean_fields():见名思义,这个方法是用来校验字段的,就是继承自 Form 中类里面定义的字段。

光看源码不看例子很难理解,

class MyForm(forms.Form):
    name = forms.CharField(min_length=3, max_length=8, label='用户名', required=True, error_messages={'min_length': '用户名长度至少为三位', 'max_length': '用户名长度最长为八位', })
    pwd = forms.CharField(min_length=3, max_length=8, label='密码', required=True, error_messages={'min_length': '用户名长度至少为三位', 'max_length': '用户名长度最长为八位', })
    email = forms.EmailField(label='邮箱', error_messages={'invalid': '邮箱格式不合法', 'required': '该字段必填'})

self.fields中放的是一个个键值对,比如在上面的MyForm里面的字段name和后面的CharField类的一个对象的对应关系,那么在self.fields里面就有三个对应关系,也就是有三个键值对。

往下走,字典的items()方法,把MyForm里面的字段值赋给了name,把字段指向的类对象赋给了field,之后进行判断,if field.disable:,这个判断是判断类对象的某个属性值,可以知道在定义字段时,并没有定义这个值,那么这个值是否有默认值,进入forms.CharField中查看,

可以看出该类中没有,那么去父类中查看也就是Field

找到了,默认为 False, 所以说if field.disables:为 False, 就走到了下一步:

value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))这句看不懂,接着往下走:

首先,咱们定义的都不是FileField字段,所以走value=field.clean(value)

因为是字段的方法,所以找forms.CharField,在Field类中找到:

这里面有点绕,我先把顺序写出来:

首先要知道value到底是什么:

在上面说过调用了field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))方法,是字段的方法,

CharField中没找到widget,去Field中寻找,

可以看到widget=TextInput,所以肯定是调用了Textinputvalue_from_datadict方法,

没有,去Input

还是没有,去Widget

找到,返回了data.get(name),很显然这是字典的取值方法,但是data是什么呢?

data是个QueryDict对象,找到name是什么,就可以知道value是什么了。

可以知道name是通过self.add_prefix(name)获取的,进入:

是个三元表达式,关键在于self.prefix的值是多少。这是个对象属性,很显然去类中找:

所以这个三元表达式的值就是MyForm中定义的字段名,所以通过data.get(name)取得值就是上传的值,比如musibii

那么执行field.clean(value)相当于把musibii传进去执行,

field.clean方法中:

  1. to_python('musibii'):因为CharField中定义了to_python方法,所以进入自己的方法;

musibii不是None,空字符串、空字典、空元祖,所以进入forece_text('musibii')方法:

别看这么多,其实就进行了一步判断:if issubclass(type('musibii'), six.text_type):因为这个判断为True

所以直接把musibii返回。然后判断if self.strip:因为该参数默认为True,所以value=value.strip(),默认是通过空格进行分割的,走完后后进入self.valitate('musibii')

直接退出进入run_validators('musibii')

在这个方法里面,主要是进行了字段的一些限制校验,比如长度等,musibii没有错误,返回musibii,然后把该数据加到self.cleaned_data中,表示这个数据是成功通过校验的。

这句代码是反射用法,判断self是否具有clean_name属性,(name 变量的值为 name),因为没有,所以第一个字段的校验结束,进行第二个字段的校验。

如果我们在MyForm类中定义了clean_name或者clean_pwd或者clean_email方法,如果在_clean_fields方法中通过校验就会执行自定义的校验方法,这就是局部钩子。

class RegForm(forms.Form):
    name = forms.CharField(max_length=8, min_length=3, label='用户名',
                           error_messages={'max_length': '太长了',
                                           'min_length': '太短了',
                                           'required': '该项不能为空'
                                           },
                           widget=widgets.TextInput(attrs={'class': 'form-control'})
                           )
    pwd = forms.CharField(max_length=8, min_length=3, label='密码',
                          error_messages={'max_length': '太长了',
                                          'min_length': '太短了',
                                          'required': '该项不能为空'
                                          },
                          widget=widgets.PasswordInput(attrs={'class': 'form-control'})
                          )
    re_pwd = forms.CharField(max_length=8, min_length=3, label='确认密码',
                          error_messages={'max_length': '太长了',
                                          'min_length': '太短了',
                                          'required': '该项不能为空'
                                          },
                          widget=widgets.PasswordInput(attrs={'class': 'form-control'})
                          )
    email = forms.EmailField(label='邮箱', error_messages={'invalid': '格式不正确',
                                                         'required': '该项不能为空'
                                                         },
                             widget=widgets.EmailInput(attrs={'class': 'form-control'})
                             )
    # clean_字段名字
    def clean_name(self):
        # 从cleaned_data中取出字段的值
        name = self.cleaned_data.get('name')
        # # 校验是否以sb开头
        if name.startswith('sb'):
            raise ValidationError('不能以sb开头')
        else:
            return name
        # user = models.UserInfo.objects.filter(name=name).first()
        # if user:
        #     # 用户已经存在,校验不通过,抛一个异常
        #     raise ValidationError('该用户已经存在')
        #
        # else:
        #     # 校验通过,返回要校验的数据
        #     return name
    # def clean_pwd(self):
    #     pass

在这里,如果name字段校验通过就会执行clean_name这个方法进行进一步的校验。

全局钩子

_clean_fields运行结束后会执行_clean_form方法,

如果MyForm中定义了clean方法的话就会执行该方法:

def clean(self):
        pwd=self.cleaned_data.get('pwd')
        re_pwd=self.cleaned_data.get('re_pwd')
        if pwd==re_pwd:
            # 正确,返回self.cleaned_data
            return None
        else:
            # 校验失败,抛异常
            raise ValidationError('两次密码不一致')

比如要判断两次密码是否一致就可以定义一个全局钩子函数。

应用

模板层

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.css">
    <script src="/static/jquery-3.3.1.js"></script>
    <title>注册</title>
</head>
<body>
<div class="row">
    <div class="col-md-6 col-md-offset-3">
        <form action="" method="post" novalidate>
            {% for foo in myform %}
                <p>{{ foo.label }}:{{ foo }} <span class="pull-right">{{ foo.errors.0 }}</span></p>
            {% endfor %}
            <input type="submit" value="提交"><span>{{error_all.0 }}</span>
        </form>
    </div>
</div>

</body>
</html>

视图层

def register(request):
    if request.method == 'GET':
        myform = RegForm()
    else:
        myform = RegForm(request.POST)
        if myform.is_valid():
            # 存数据库,保存这个人
            print(myform.cleaned_data)
        else:
            # 校验不通过,这里面也可能有值
            # 总错误信息{'name':['太长了',],'pwd':['太短了']}
            # print(myform.cleaned_data)
            # 总错误
            # myform.glo_err=myform.errors.get('__all__')
            error_all = myform.errors.get('__all__')
            # print()
            # 每一个的错误信息
            # for item in myform:
            #     # ['太长了',]
            #     # print(item.errors[0])
            #     # print(type(item.errors))
            #     from django.forms.utils import ErrorList
            #     # print(item.errors.as_data())
            #     print(item.errors)
            #     # print(type(item.errors))
    return render(request, 'register.html', locals())

全局钩子产生的错误会放到myform.errors[__all__]中

posted @ 2019-01-20 15:27  rsuxwvilc  阅读(394)  评论(0编辑  收藏  举报