Django学习笔记一十七——Django的FORM表单

在之前的web开发中,我们大量的使用了form表单来提交数据,比方我们要做一个登录的页面,大概是下面几个流程:

  1. 先要做一个包含form标签的页面————》HTML代码
  2. 然后把这个form提交到后端,后端对其进行相关操作————》数据有效性校验
  3. 然后页面把这个操作的结论显示出来。

关于数据的校验,大致有两种方法:

  1. 通过前端的JS代码做校验。
  2. 后端后端的视图做校验。

但是前段的JS校验是可有可无的,比方我们写一个爬虫,在爬取信息的时候是绕过了前端的JS代码直接伪装一个请求的。所以后端的校验是必须的,前端的校验可以用来降低了一部分服务器的压力,把明显的违规的数据直接提示出来,就不用服务器去判定了,所以最好还是加上。

先看一看我们最初级的FORM表单的使用方法

原始FORM表单使用

 我们先回顾一下最原始的FORM表单是怎么使用的,和前面的案例一样,这里把主要的视图和模板罗列出来,其他的就直接套就行了。

视图

def reg1(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        pwd = request.POST.get('pwd')

        if username == 'jack' and pwd =='123':
            return HttpResponse('OK')
        else:
            return HttpResponse('登录失败')

    else:
        return render(request,'reg_original.html')

模板

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>注册页面初始版</title>
</head>

<body>
    <form action="/reg1/" method="POST">
        {%csrf_token%}
        <div>
            用户名:<input type="text" name="username">
        </div>
        <div>
            密码:<input type="password" name="pwd">
        </div>
        <div>
            <input type="submit" value="注册">
        </div> 
    </form>
</body>
</html>

这样就是一个最简单的注册form标签的使用事例。

Django提供的FORM组件

 Django还我们提供了一个使用FORM表单的组件,可以直接声明一个类

from django import forms

class RegForm(forms.Form):
    name = forms.CharField(max_length=16,label='用户名')
    pwd = forms.CharField(min_length=6,label='密码')

然后做一个模板

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form action="/reg/" method="POST" novalidate>
        {%csrf_token%}
        {{form_obj.as_p}}
        <input type="submit">
    </form>
</body>
</html>

把这个类在视图中实例化,然后直接用render替换里面的变量

def reg(request):
    form_obj = RegForm()
    return render(request,'reg.html',{"form_obj":form_obj})

这样就可以直接使用了。

form组件的常用字段

 在创建FORM是,主要涉及到的就是字段和插件,字段用于对用户请求数据的验证,而插件则用于自动生成HTML;和ORM一样,FORM组件也定义了一些字段,上面的例子中我们就使用了一个Charfield字段,除此意外我么还可以看一看常用的一些其他的字段。

initial

初始值,指input框里的初始值,用法:

class RegForm(forms.Form):
    name = forms.CharField(max_length=16,label='用户名' ,initial='张三')

错误信息error_messages

重写错误的信息,在校验的时候可以推出指定的错误信息

error_messages={
    "min_length":'密码不能少于6位'
}

当校验不满足要求的时候,就会pull出我们定义好的错误信息。

password类的input

 我们在前面定义的CharField是默认text类的,但是在输入密码的时候需要的可不是明文,就要用下面的方式指定成密文

class RegForm(forms.Form):
    name = forms.CharField(max_length=16,label='用户名' ,initial='张三')
    pwd = forms.CharField(min_length=6,label='密码',widget = forms.widgets.PasswordInput)

widgets

 

email类型的数据

相当于input标签里的email类型

class RegForm(forms.Form):
    emaile = forms.EmailField()

 

radioSelect

单选的radio

class RegForm(forms.Form):
    name = forms.CharField(max_length=16,label='用户名' ,initial='张三')
    pwd = forms.CharField(min_length=6,label='密码',widget = forms.widgets.PasswordInput)
    gender = forms.ChoiceField(
        label='性别',
        choices=((1,''),(2,''),(3,'保密')),
        initial = 2,#默认选中
        widget = forms.widgets.RadioSelect
    )

出来的效果就是这种单选radio的效果

 我们在choice里通过initial指定了默认的选择项,那么在每次页面刷新出来以后就是这个默认的选项被选中。

Select下拉选框

hobby = forms.ChoiceField(
    label= '爱好',
    choices=((1,'篮球'),(2,'乒乓球'),(3,'羽毛球')),
    initial = 2,
    widget = forms.widgets.Select
)

这里就写出来这一个字段,其他的就不重复列举了

出来的效果

 

 

多选Select

多选的方法和单选的差不多,就是改了一下最后的widgets里的属性,就还用上面那个选项来举例

hobby = forms.ChoiceField(
    label= '爱好',
    choices=((1,'篮球'),(2,'乒乓球'),(3,'羽毛球')),
    initial = 2,
    widget = forms.widgets.SelectMultiple
)

注意最后的不是Select而是SelectMultiple了

单选checkbox

keep_pwd = forms.ChoiceField(
    label = '记住密码',
    initial = 'checked',
    widget = widgets.CheckboxInput
)

 

多选的checkbox

hobby2 = forms.ChoiceField(
    label = 'checkbox多选',
    choices=((1,'篮球'),(2,'乒乓球'),(3,'羽毛球')),
    initial = [1,3],
    widget = widgets.CheckboxSelectMultiple
)

和前面的 多选的Select差不多,就是渲染出来的效果不太一样:

 

加参数的widget属性

注意看一下,我们前面的字段在要求在套用模板的时候使用p标签默认包裹的({{form_obj.as_p}})但是在生成HTML代码后多出来了一个li标签(选项前面的小黑点),那么我们就可以在声明类的时候加上一个属性,给生成 的标签附一个类的值

hobby2 = forms.ChoiceField(
    label = 'checkbox多选',
    choices=((1,'篮球'),(2,'乒乓球'),(3,'羽毛球')),
    initial = [1,3],
    widget = widgets.CheckboxSelectMultiple(attrs={'class':'c1'})    #给生成的标签添加一个类属性
)

然后在模板里一开始的地方对c1这个类价格css效果

<style>
    .c1 {list-style-type: none;}
</style>

这样在渲染出来的标签就没有前面的小黑点了

 

 这里主要讲的是如何给标签添加一些属性。

下面这个表里列举了Django的FORM里几乎所有的字段

Field
    required=True,               是否允许为空
    widget=None,                 HTML插件
    label=None,                  用于生成Label标签或显示内容
    initial=None,                初始值
    help_text='',                帮助信息(在标签旁边显示)
    error_messages=None,         错误信息 {'required': '不能为空', 'invalid': '格式错误'}
    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类型
Django内置FORM字段

 

生成HTML标签的几种方式

在对声明的FORM类实例化以后,我们要把对象送给模板生成HTML代码,下面是几种生成代码的方式(比方我们实例化的对象名为form_obj)

1.手动写

<form action="">
    <div>
        {{form_obj.username}}
    </div>
</form>

username就对应的是生成一个input,内容对应的就是username

手动写的方式最简单粗暴,也好按照自己的需求布局,也可以套用类似前面用的Bootstrap之类的模板,但是就是太麻烦

 2.自动生成

就像前面用的方法一样

{{form_obj.as_p}}

生成的每一个标签都用p标签包裹

数据校验功能

既然Django为我们提供了FORM组件,那么下面来试一下在FORM组件里如何实现数据的校验的功能的

这里特别要注意两个方法

  1. is_valid()表明已经通过验证
  2. cleaned_data用来获取已经经过校验的数据(字典)

初级校验

我们结合一个输入框来研究一下这个校验是怎么用的。比方我们规定了这个输入框长度为大于6小于10,然后在模板里直接使用这个form组件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>校验效果</title>
</head>
<body>
    <form action="/check/" method="POST" novalidate>
        {%csrf_token%}
    {{check_obj.input_check}}
    <span>{{check_obj.errors}}</span>
    <input type="submit" name="" id="">
    </form>
</body>
</html>
模板
from django import forms
from django.forms import widgets

class Check_test(forms.Form):
    input_check = forms.CharField(
        max_length=10,
        min_length=6,
        widget = widgets.TextInput(),
        error_messages={
            'max_length':'长度不能超过10',
            'min_length':'长度不能小于6'
        }
    )

def check(request):
    check_obj = Check_test()
    if request.method == 'POST':
        check_obj =Check_test(request.POST)
        if check_obj.is_valid():
            print(check_obj.cleaned_data)

    return render(request,'check.html',{'check_obj':check_obj})

如果输入的内容长度不满足要求,就会这样:

 

 

注意下上面我们说的两个关键方法的使用,如果校验不通过,是不用显性的写返回的值的,Django直接把我们在类里定义的错误信息直接发送过去了。

还有,就是这种直接在下面显示出来对应的错误信息的效果是我们在form标签里通过novalidate属性声明的,如果不加这个关键字就是以弹框的效果显示出来

并且这里的提示信息和我们指定的值是没关系的。

如果我们输入的内容满足校验,看看打印的值是什么样的

 直接获得一个字典,key就是我们定义的类里的字段,值就是我们输入的值。

正则校验

除了上面说的那种简单的校验方式,我们还可以使用正则表达式的方法对数据进行校验,比方我们把刚才的输入框定义为手机号吗的输入框,要求电话好必须是180开头的手机号,那么就可以用这种正则的方法来校验

模板和视图跟前面的一样,这里主要看一看类是怎么定义的

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



class Check_test(forms.Form):
    input_check = forms.CharField(
        label = '手机号码',
        widget = widgets.TextInput(),
        validators=[RegexValidator(r'^1[\d]{10}$','请输入11位数字'),
                    RegexValidator(r'^180[\d]{8}$','请用180开头的手机号')])

这样就可以直接使用了,当我们输入的值不满足要求时

 

 就会直接pull出我们定义的错误信息。

 

标签的动态加载

 假设我们有个下拉框,数据的内容是从数据库里获取的(城市)

class City_input(forms.Form):
    city = forms.ChoiceField(
        choices = models.City.objects.all().values_list('id','city'),
        widget = widgets.Select
    )

如果我们的数据库里有两个城市,那么就是这样的效果

 

 

这时候,我们在数据库里新加一个数据(深圳),刷新页面后数据是依旧不会变的,必须重启服务以后才能刷新数据,这不是我们需要的,但是我们需要在页面刷新以后有新的数据进来,那么就要对构造函数重写一遍

class City_input(forms.Form):
    city = forms.ChoiceField(
        choices = models.City.objects.all().values_list('id','city'),
        widget = widgets.Select
    )

    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        self.fields['city'].widget.choices = models.City.objects.all().values_list('id','city')

这样,我们指定了这个字段,在每次调用的时候重新去数据库里get到所有的数据。

联合数据库的校验

 下面我们就试一下,如何联合数据库做一个校验(这就比较简单了),要求要做一个注册的页面,要求记录用户名,手机号,设置密码(密码还有有个确认密码),页面效果使用Bootstrap,做出来下面的效果

单个标签的校验

 

 下面就是模板的代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .c1 {
            list-style-type: none;
        }
    </style>

    <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <form action="/reg/" method="POST" novalidate>
                {%csrf_token%}

                <div class="form-group {% if form_obj.name.errors.0 %}has-error{% endif %}">
                    {{ form_obj.name.label }}
                    {{ form_obj.name }}
                    <span class="help-block">{{ form_obj.name.errors.0 }}</span>
                </div>

                <div class="form-group {% if form_obj.pwd.errors.0 %}has-error{% endif %}">
                    {{form_obj.pwd.label}}
                    {{form_obj.pwd}}
                    <span class="help-block">{{ form_obj.pwd.errors.0 }}</span>
                </div>
                
                <div class="form-group {% if form_obj.pwd.errors.0 %}has-error{% endif %}">
                    {{form_obj.pwd_confirm.label}}
                    {{form_obj.pwd_confirm}}
                    <span class="help-block">{{ form_obj.pwd_confirm.errors.0 }}</span>
                </div>

                <div class="form-group {% if form_obj.pwd.errors.0 %}has-error{% endif %}">
                    {{form_obj.phone.label}}
                    {{form_obj.phone}}
                    <span class="help-block">{{ form_obj.phone.errors.0 }}</span>
                </div>

                <div class="form-group {% if form_obj.pwd.errors.0 %}has-error{% endif %}">
                    {{form_obj.email.label}}
                    {{form_obj.email}}
                    <span class="help-block">{{ form_obj.email.errors.0 }}</span>
                </div>    

                <div class="form-group">
                    <input type="submit" class="btn btn-default">
                </div>
                </div>
            </form>
        </div>
    </div>


</form>
</body>>
模板

数据库的类

class User(models.Model):
    name = models.CharField(max_length=16,unique=True,null=False,default='11')
    pwd = models.CharField(max_length=32,default='11')
    phone = models.CharField(max_length=11,unique=True,default='11')
    email = models.CharField(max_length=32,unique=True,default='11')



    def __str__(self):
        return self.name

视图

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


class RegForm(forms.Form):
    name = forms.CharField(max_length=16,
        label='用户名' ,
        widget = widgets.TextInput(attrs={'class':'form-control'}),
        error_messages={
            "max_length":'用户名不能大于16位'
        })

    # 密码
    pwd = forms.CharField(
        min_length=6,
        label='密码',
        widget = widgets.PasswordInput(attrs={'class':'form-control'}),
        error_messages={
            "min_length":'密码不能少于6位'
        })

    #确认密码
    pwd_confirm = forms.CharField(
        min_length=6,
        label='确认密码',
        widget = widgets.PasswordInput(attrs={'class':'form-control'}),
        error_messages={
            "min_length":'密码不能少于6位'
        })

    #电话
    phone = forms.CharField(
        label='手机号码',
        widget = widgets.TextInput(attrs={'class':'form-control'}),
        validators = [RegexValidator(r'^1[\d]{10}','请输入11位手机号')]
    )

    #email
    email = forms.EmailField(label='email',
        widget = widgets.EmailInput(attrs={'class':'form-control'}))

from . import models

def reg(request):
    form_obj = RegForm()
    if request.method == 'POST':
        form_obj = RegForm(request.POST)
        if form_obj.is_valid():
            print(123)
            print(form_obj.cleaned_data)
            del form_obj.cleaned_data['pwd_confirm']
            models.User.objects.create(**form_obj.cleaned_data)
            return HttpResponse(12345)

    return render(request,'reg.html',{'form_obj':form_obj})
View Code

这里一定要注意保存数据时参数的传递方法

models.User.objects.create(**form_obj.cleaned_data)

动态数据的校验

上面的校验都是静态的校验,都是用来校对数据是否满足单条输入框的要求,但是还有一些其他的要求,比方用户名是否已经存在,密码和确认密码是否一致,这些数据的判定就要另外的方法了。

以后如果有机会的话我们再捋一下form的源码,这里主要用到了一个叫做钩子(HOOK)的思路。

模板可以不用变,把修改过的视图放出来看一下

from django import forms
from django.forms import widgets
from django.core.validators import RegexValidator
from django.core.validators import ValidationError
class Check_test(forms.Form):
    input_check = forms.CharField(
        label = '手机号码',
        widget = widgets.TextInput(),
        validators=[RegexValidator(r'^1[\d]{10}$','请输入11位数字'),
                    RegexValidator(r'^180[\d]{8}$','请用180开头的手机号')],
    )

def check(request):
    check_obj = Check_test()
    if request.method == 'POST':
        check_obj =Check_test(request.POST)
        if check_obj.is_valid():
            print(check_obj.cleaned_data)

    return render(request,'check.html',{'check_obj':check_obj})


class RegForm(forms.Form):
    name = forms.CharField(max_length=16,
        label='用户名' ,
        widget = widgets.TextInput(attrs={'class':'form-control'}),
        error_messages={
            "max_length":'用户名不能大于16位'
        })

    # 密码
    pwd = forms.CharField(
        min_length=6,
        label='密码',
        widget = widgets.PasswordInput(attrs={'class':'form-control'}),
        error_messages={
            "min_length":'密码不能少于6位'
        })

    #确认密码
    pwd_confirm = forms.CharField(
        min_length=6,
        label='确认密码',
        widget = widgets.PasswordInput(attrs={'class':'form-control'}),
        error_messages={
            "min_length":'密码不能少于6位'
        })

    #电话
    phone = forms.CharField(
        label='手机号码',
        widget = widgets.TextInput(attrs={'class':'form-control'}),
        validators = [RegexValidator(r'^1[\d]{10}','请输入11位手机号')]
    )

    #email
    email = forms.EmailField(label='email',
        widget = widgets.EmailInput(attrs={'class':'form-control'}))


    #自定义校验单项数据
    def clean_name(self):
        name = self.cleaned_data.get('name')
        if models.User.objects.filter(name=name):
            raise ValidationError('用户名已存在')
        return self.name

    
    def clean(self):
        pwd = self.cleaned_data.get('pwd')
        pwd_confirm = self.cleaned_data.get('pwd_confirm')

        if pwd != pwd_confirm:
            self.add_error('pwd_confirm','两次密码不一致!')  # 指定错误信息和显示位置(给哪个标签显示错误)

        return self.cleaned_data



def reg(request):
    form_obj = RegForm()
    if request.method == 'POST':
        form_obj = RegForm(request.POST)
        if form_obj.is_valid():
            print(123)
            print(form_obj.cleaned_data)
            del form_obj.cleaned_data['pwd_confirm']
            models.User.objects.create(**form_obj.cleaned_data)
            return HttpResponse(12345)
        print(form_obj.errors)

    return render(request,'reg.html',{'form_obj':form_obj})
视图

主要看里面的两个函数:clean_name和重构的clean()函数。

 特别是clean_name()的用法,

if hasattr(self, 'clean_%s' % name):
    value = getattr(self, 'clean_%s' % name)()
    self.cleaned_data[name] = value

上面是Django里form的一段源代码,用了反射的方法,但是这是一个hook的用法,指定了方法名开始的字符串clean,后面的名称可以我们自己来定。如果有机会了后面在详细讲一下。

posted @ 2020-05-12 00:47  银色的音色  阅读(572)  评论(0编辑  收藏  举报