Django学习笔记一十七——Django的FORM表单
在之前的web开发中,我们大量的使用了form表单来提交数据,比方我们要做一个登录的页面,大概是下面几个流程:
- 先要做一个包含form标签的页面————》HTML代码
- 然后把这个form提交到后端,后端对其进行相关操作————》数据有效性校验
- 然后页面把这个操作的结论显示出来。
关于数据的校验,大致有两种方法:
- 通过前端的JS代码做校验。
- 后端后端的视图做校验。
但是前段的JS校验是可有可无的,比方我们写一个爬虫,在爬取信息的时候是绕过了前端的JS代码直接伪装一个请求的。所以后端的校验是必须的,前端的校验可以用来降低了一部分服务器的压力,把明显的违规的数据直接提示出来,就不用服务器去判定了,所以最好还是加上。
先看一看我们最初级的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表单的组件,可以直接声明一个类
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是,主要涉及到的就是字段和插件,字段用于对用户请求数据的验证,而插件则用于自动生成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里几乎所有的字段
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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类型
在对声明的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组件里如何实现数据的校验的功能的
这里特别要注意两个方法
- is_valid()表明已经通过验证
- cleaned_data用来获取已经经过校验的数据(字典)
初级校验
我们结合一个输入框来研究一下这个校验是怎么用的。比方我们规定了这个输入框长度为大于6小于10,然后在模板里直接使用这个form组件
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!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,做出来下面的效果
单个标签的校验
下面就是模板的代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!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
视图
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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})
这里一定要注意保存数据时参数的传递方法
models.User.objects.create(**form_obj.cleaned_data)
动态数据的校验
上面的校验都是静态的校验,都是用来校对数据是否满足单条输入框的要求,但是还有一些其他的要求,比方用户名是否已经存在,密码和确认密码是否一致,这些数据的判定就要另外的方法了。
以后如果有机会的话我们再捋一下form的源码,这里主要用到了一个叫做钩子(HOOK)的思路。
模板可以不用变,把修改过的视图放出来看一下
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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,后面的名称可以我们自己来定。如果有机会了后面在详细讲一下。