title: Django之form和ModelForm组件 #标题tags: #标签
date: 2022-01-15
categories: python # 分类
之前在HTML页面中利用form表单向后端提交数据时,都会写一些获取用户输入的标签并且用form标签把它们包起来。
与此同时我们在好多场景下都需要对用户的输入做校验,比如校验用户是否输入,输入的长度和格式等正不正确。如果用户输入的内容有错误就需要在页面上相应的位置显示对应的错误信息.。
Django form组件就实现了上面所述的功能。form组件的主要功能如下:
- 生成页面可用的HTML标签
- 对用户提交的数据进行校验
- 保留上次输入内容
本次项目代码地址:Gitee码云。
视频资料:Python全栈 900集就业班(完结)B站最全
注意:仓库中浏览代码时,可能会提示如下:
不要慌,直接刷新页面即可。
form组件demo
在开始之前,先来一版之前写的注册功能是如何写的,以便来做个对比。
low版注册功能
urls.py文件
from django.urls import path
from app01 import views
urlpatterns = [
path('registy/', views.Registy.as_view(), name='registy'),
]
views.py文件
from django.shortcuts import render, redirect, HttpResponse
from django.views import View
from django.http import JsonResponse
# Create your views here.
class Registy(View):
def get(self, request):
return render(request, 'registy.html')
def post(self, request):
uname = request.POST.get('uname')
pwd = request.POST.get('pwd')
print(uname, pwd)
'''
需要自行写一些判断逻辑,以便确认用户提交的数据是合法的,才可以登录成功
这里就省略这些判断逻辑了,只是打印下
'''
return HttpResponse('<h1>注册成功</h1>')
registy.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册页面</title>
</head>
<body>
<h1>注册页面</h1>
<form action="" method="post">
{% csrf_token %}
<div>
用户名:<input type="text" name="uname">
</div>
<div>
密 码:<input type="password" name="pwd">
</div>
<div>
<input type="submit" value="注册">
</div>
</form>
</body>
</html>
使用form组件
现在来使用form组件,重写上面的功能。
urls.py文件
from django.urls import path
from app01 import views
urlpatterns = [
path('form_registy/', views.Registy_Form.as_view(), name='registy_form'),
]
views.py文件
from django.shortcuts import render, redirect, HttpResponse
from django.views import View
from django.http import JsonResponse
from django import forms
# Create your views here.
# 定义一个form类,必须继承forms.Form
class LoginForm(forms.Form):
# 想有哪些字段,写就好了
uname = forms.CharField(label='用户名 ')
pwd = forms.CharField(label='密 码 ')
class Registy_Form(View):
# 实例化form对象
form_obj = LoginForm()
def get(self, request):
# 将实例化的form对象传入html页面中进行渲染
return render(request, 'registy_form.html', {'form_obj': self.form_obj})
def post(self, request):
uname = request.POST.get('uname')
pwd = request.POST.get('pwd')
print(uname, pwd)
return HttpResponse('<h1>注册成功</h1>')
registy_form.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册页面</title>
</head>
<body>
<h1>registy_form注册页面</h1>
<h1>方式一:推荐使用,想生成哪个字段就调用哪个</h1>
<form action="" method="post">
{% csrf_token %}
<div>
<label for="">{{ form_obj.uname.label }}</label>
{{ form_obj.uname }}
</div>
<div>
<label for="">{{ form_obj.pwd.label }}</label>
{{ form_obj.pwd }}
</div>
<div>
<input type="submit" value="注册">
</div>
</form>
<h1>方式二:不推荐使用,调用不太灵活</h1>
<form action="" method="post">
{{ form_obj.as_p }}
<input type="submit" value="注册">
</form>
<h1>方式三</h1>
<form action="" method="post">
{{ form_obj.as_table }}
<input type="submit" value="注册">
</form>
</body>
</html>
Form常用字段与插件
创建Form类时,主要涉及到 【字段】 和 【插件】,字段用于对用户请求数据的验证,插件用于自动生成HTML。
这里只贴视图函数中的部分代码,完整代码请移步项目地址中的form_field_plug/
这个path。
字段
initial
初始值,input框里面的初始值。
class Field_plugs(forms.Form):
username = forms.CharField(
min_length=6,
label="用户名",
initial="吕建钊"
)
error_messages
重写错误信息。
class Field_plugs(forms.Form):
username = forms.CharField(
min_length=2,
label="用户名",
initial="吕建钊",
error_messages={
"required": "不能为空",
"invalid": "格式错误",
"min_length": "用户名最短两位"
}
)
password
密码输入框。
class Field_plugs(forms.Form):
pwd = forms.CharField(
min_length=6,
label="密码",
widget=forms.widgets.PasswordInput(
attrs={'class': 'form-control'}, # attrs可以给标签添加html页面中引入的样式
render_value=True,
# 密码字段和其他字段不一样,默认在前端输入数据错误的时候,
# 点击提交之后,默认是不保存的原来数据的,
# 但是可以通过这个render_value=True让这个字段在前端保留用户输入的数据
)
)
radioSelect
单radio值为字符串。
class Field_plugs(forms.Form):
gender = forms.fields.ChoiceField(
choices=((1, "男"), (2, "女"), (3, "保密")),
label="性别",
initial=3, # 默认为保密
widget=forms.widgets.RadioSelect()
)
单选select
注意,单选框用的是ChoiceField,并且里面的插件是Select,不然验证的时候会报Select a valid choice
的错误。
class Field_plugs(forms.Form):
hobby = forms.fields.ChoiceField(
choices=((1, "篮球"), (2, "足球"), (3, "乒乓球"),),
label="爱好",
initial=2,
widget=forms.widgets.Select()
)
多选select
多选框的时候用MultipleChoiceField,并且里面的插件用的是SelectMultiple,不然验证的时候会报错。
multi_hobby = forms.fields.MultipleChoiceField(
choices=((1, "篮球"), (2, "足球"), (3, "乒乓球"),),
label="多选爱好",
initial=[1, 3],
widget=forms.widgets.SelectMultiple()
)
单选checkbox
一般用于在登录时下面的是否记住密码等场景。
keep = forms.fields.ChoiceField(
label="是否记住密码",
initial="checked",
widget=forms.widgets.CheckboxInput()
)
单选checkbox示例:
keep_demo = forms.fields.ChoiceField(
choices=(
# 选中:True
# 未选中:false
('True', 1),
('False', 0),
),
label="是否7天内自动登录",
initial="1",
widget=forms.widgets.CheckboxInput()
)
多选checkbox
checkbox_mutil = forms.fields.MultipleChoiceField(
choices=(
(1, "篮球"),
(2, "足球"),
(3, "双色球"),
),
label="爱好",
initial=[1, 3],
widget=forms.widgets.CheckboxSelectMultiple()
)
date输入框
form默认没有提供日期输入框,需要借助attrs来将input文本输入框改为日期输入框。
date_input=forms.DateField(
label="选择日期",
widget=forms.widgets.TextInput(
attrs={
'type':'date'
}
)
)
choice字段从数据库中取值
在使用选择标签时,需要注意choices的选项可以配置从数据库中获取,但是由于是静态字段,获取的值无法实时更新,需要重写构造方法从而实现choice实时更新。
方式一:
from django.forms import Form
from django.forms import widgets
from django.forms import fields
class MyForm(Form):
user = fields.ChoiceField(
# choices=((1, '上海'), (2, '北京'),),
initial=2,
widget=widgets.Select
)
def __init__(self, *args, **kwargs):
# 注意重写init方法的时候,*args和**kwargs一定要给写上,不然会出问题,并且验证总是不能通过,还不显示报错信息
super(MyForm,self).__init__(*args, **kwargs)
# self.fields['user'].choices = ((1, '上海'), (2, '北京'),)
# 或
self.fields['user'].choices = models.Classes.objects.all().values_list('id','caption')
方式二:
from django import forms
from django.forms import fields
from django.forms import models as form_model
class FInfo(forms.Form):
authors = forms.ModelMultipleChoiceField(queryset=models.NNewType.objects.all()) # 多选
# 或者下面这种方式,通过forms里面的models中提供的方法也是一样的。
authors = form_model.ModelMultipleChoiceField(queryset=models.NNewType.objects.all()) # 多选
# authors = form_model.ModelChoiceField(queryset=models.NNewType.objects.all()) # 单选
# 或者,forms.ModelChoiceField(queryset=models.Publisth.objects.all(),widget=forms.widgets.Select()) 单选
#
authors = forms.ModelMultipleChoiceField(
queryset=models.Author.objects.all(),
widget=forms.widgets.Select(attrs={'class': 'form-control'}
))
# 如果用这种方式,别忘了model表中,NNEWType的__str__方法要写上,不然选择框里面是一个个的object对象
Form组件的验证及错误提示功能
视图函数内容
# 下面是此示例中的视图函数内容
# Form组件的验证及错误提示功能
class LoginForm(forms.Form):
username = forms.CharField(
required=True, # 是否必填
min_length=3, # 最短为3位
max_length=10, # 最长为10位
label="用户名",
initial="吕建钊",
help_text='这是输入用户名的地方!',
# error_messages来定义不符合上面定义的属性时的提示信息
error_messages={
"required": "不能为空",
"invalid": "格式错误",
"min_length": "用户名最短三位",
"max_length": "用户名最长为10位",
},
widget=forms.widgets.TextInput,
)
pwd = forms.CharField(
min_length=6,
max_length=12,
label="密码",
error_messages={
"required": "不能为空",
"invalid": "格式错误",
"min_length": "密码最短6位",
"max_length": "密码最长为12位",
},
widget=forms.widgets.PasswordInput(
render_value=True,
# 密码字段和其他字段不一样,默认在前端输入数据错误的时候,
# 点击提交之后,默认是不保存的原来数据的,
# 但是可以通过这个render_value=True让这个字段在前端保留用户输入的数据
)
)
class Login_Form(View):
# 实例化form对象
form_obj = LoginForm()
def get(self, request):
# 将实例化的form对象传入html页面中进行渲染
return render(request, 'login_form.html', {'form_obj': self.form_obj})
def post(self, request):
'''
实例化 LoginForm 对象的时候,将post请求中包含的数据作为参数传入进去(post请求中的数据就是一个字典)
LoginForm自动会将post传入的数据和自己定义的字段对应起来,并判断是否合法,
如果不合法,则会将定义的 error_messages 赋值为 form_obj.errors
'''
form_obj = LoginForm(request.POST)
print(
form_obj.fields) # 输出:{'username': <django.forms.fields.CharField object at 0x000001D86B0B4160>, 'pwd': <django.forms.fields.CharField object at 0x000001D86B0B41C0>}
print(form_obj.is_valid()) # True
# 如果post提交的数据,满足form_obj中定义的字段属性,比如最长几位,最短几位的,则值为True,反之为false
if form_obj.is_valid():
print(form_obj.cleaned_data) # 输出:{'username': '吕建f', 'pwd': '11ffff'}
# cleaned_data用于获取post提交的所有数据
return HttpResponse('<h1>登录成功</h1>')
else:
# 如果提交的数据不合法,再走下面的逻辑,并且将初始化的form对象传给到前端,这样前端就能显示错误提示了
print('-' * 60)
print(
form_obj.errors) # 输出:<ul class="errorlist"><li>pwd<ul class="errorlist"><li>密码最短6位</li></ul></li></ul>
return render(request, 'login_form.html', {'form_obj': form_obj})
前端login_form.html文件内容
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form字段校验</title>
<link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">
<style>
h4 {
color: green;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-5 col-md-offset-3">
novalidate
<form action="" method="post" novalidate>
{% csrf_token %}
<div>
<h4>form组件验证</h4>
<label for="">{{ form_obj.username.label }}</label>
{{ form_obj.username }}
{# {{ form_obj.username.help_text }} 输入框的提示信息#}
<!--用于展示提示的错误信息
form_obj.username.errors:输出的是一个列表,包含了所有的错误信息,
可以通过 .0 这种索引取值的方式,不管有多少错误消息,只渲染一条到前端
-->
<span style="color: red">{{ form_obj.username.errors.0 }}</span>
</div>
<div>
<label for="">{{ form_obj.pwd.label }}</label>
{{ form_obj.pwd }}
<span style="color: red">{{ form_obj.pwd.errors.0 }}</span>
</div>
<hr>
<div>
<input type="submit" value="提交">
</div>
</form>
</div>
</div>
</div>
</body>
</html>
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类型
字段校验
RegexValidator验证器
class LoginForm(forms.Form):
......
Mobile = forms.fields.CharField(
label="手机号",
# 下面的RegexValidator方法,第一位传入正则表达式,以便去匹配输入的内容
# 第二段传输的是如果不满足正则匹配的条件后的错误提示信息
# 像下面的例子,就是如果不是以数字开头,前端就可以报错:“请输入数字”,如果不是159开头,就可以报错“数字必须以159开头”
validators=[RegexValidator(r'^[0-9]+$', '请输入数字'), RegexValidator(r'^159[0-9]+$', '数字必须以159开头')],
)
自定义验证函数
import re
from django.forms import Form
from django.forms import widgets
from django.forms import fields
from django.core.exceptions import ValidationError
# 自定义验证规则
def mobile_validate(value):
# 定义一个正则
mobile_re = re.compile(r'^(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$')
if not mobile_re.match(value):
raise ValidationError('手机号码格式错误') # 自定义验证规则的时候,如果不符合你的规则,需要自己发起错误
class PublishForm(Form):
title = fields.CharField(max_length=20,
min_length=5,
error_messages={'required': '标题不能为空',
'min_length': '标题最少为5个字符',
'max_length': '标题最多为20个字符'},
widget=widgets.TextInput(attrs={'class': "form-control",
'placeholder': '标题5-20个字符'}))
# 使用自定义验证规则
phone = fields.CharField(validators=[mobile_validate, ],
error_messages={'required': '手机不能为空'},
widget=widgets.TextInput(attrs={'class': "form-control",
'placeholder': u'手机号码'}))
email = fields.EmailField(required=False,
error_messages={'required': u'邮箱不能为空', 'invalid': u'邮箱格式错误'},
widget=widgets.TextInput(attrs={'class': "form-control", 'placeholder': u'邮箱'}))
Hook钩子方法
除了上面两种方式,我们还可以在Form类中定义钩子函数,来实现自定义的验证功能。
局部钩子
在Fom类中定义 clean_字段名() 方法,就能够实现对特定字段进行校验。
# Form组件的验证及错误提示功能
class LoginForm(forms.Form):
username = forms.CharField(
required=True, # 是否必填
min_length=3, # 最短为3位
max_length=10, # 最长为10位
label="用户名",
initial="吕建钊",
help_text='这是输入用户名的地方!',
# error_messages来定义不符合上面定义的属性时的提示信息
error_messages={
"required": "不能为空",
"invalid": "格式错误",
"min_length": "用户名最短三位",
"max_length": "用户名最长为10位",
},
widget=forms.widgets.TextInput,
)
.......... # 省略部分内容
# clean_字段名称是固定格式,要对哪个字段进行校验,就写哪个字段
def clean_username(self):
value = self.cleaned_data.get('username') # 从用户提交的数据(cleaned_data)中获取值
'''
如果用户提交的username中包含666,就提示错误
如果不包含666,就将原值进行return即可
'''
if "666" in value:
raise forms.ValidationError("光喊666是不行的!")
else:
return value
效果如下:
全局钩子
在Fom类中定义clean()
方法,就能够实现对字段进行全局校验,字段全部验证完,局部钩子也全部执行完之后,执行这个全局钩子校验。
class LoginForm(forms.Form):
.........
pwd = forms.CharField(
min_length=6,
max_length=12,
label="密码",
error_messages={
"required": "不能为空",
"invalid": "格式错误",
"min_length": "密码最短6位",
"max_length": "密码最长为12位",
},
widget=forms.widgets.PasswordInput(
render_value=True,
# 密码字段和其他字段不一样,默认在前端输入数据错误的时候,
# 点击提交之后,默认是不保存的原来数据的,
# 但是可以通过这个render_value=True让这个字段在前端保留用户输入的数据
)
)
re_pwd = forms.CharField(
label="确认密码",
widget=forms.widgets.PasswordInput(),
)
# clean方法用于定义全局钩子
def clean(self):
# cleaned_data:用于获取用户提交的数据
# print(self.cleaned_data) # {'username': '吕建钊', 'pwd': '111111', 're_pwd': '33333333'}
_pwd_values = self.cleaned_data
_pwd = _pwd_values.get('pwd')
_re_pwd = _pwd_values.get('re_pwd')
if _re_pwd == _pwd:
return _pwd
else:
# 在re_pwd这个字段的错误列表中加上一个错误,并且cleaned_data里面会自动清除这个re_password的值,
# 所以打印cleaned_data的时候会看不到它
self.add_error('re_pwd', '两次密码不一致')
raise forms.ValidationError('两次密码不一致')
如果两次输入的密码不一致,则会提示如下:
ModelForm:从数据库中读取数据
这里通过重写之前的书籍管理系统,来将form模块应用了起来,项目地址:multi_books_admin_from
通过使用form模块来重写的书籍管理系统呢,还是有点麻烦,毕竟form中要有哪些字段,还是得我们自己来定义,并且这些数据是要传给后端数据库的,那能不能直接将数据表中定义的字段拿来给生成对应的输入框呢?肯定是能的,那就是要用ModelForm
这个类了,已经写好了项目并上传至Gitee:书籍管理系统(ModelForm版)
这里只贴一小部分代码,来看看是如何通过ModelForm
生成各种输入框的:
from django.shortcuts import render, redirect, reverse, HttpResponse
from app01 import models
from django.views import View
from django import forms
from django.core.exceptions import ValidationError
class BookModelForm(forms.ModelForm):
# 可以自己写字段,来重写数据模型中的字段
# title=forms.CharField(max_length=15,min_length=6)
class Meta:
# 将Book模型中定义的全部字段全部转为form中要手动生成的字段
model = models.Book
# fields = ['title', 'publish'] # 可以指定要给前端渲染Book模型中的指定字段
fields = '__all__' # Book模型中的所有字段
# exclude = ['authors', 'publish'] # 也可以通过exclude来排除指定字段
# 通过labels字段来给Book这个数据模型中的字段打上label,以便可以在前端显示为指定的名称
labels = {
'title': '书籍名称',
'publish': '出版社',
'publishDate': '出版日期',
'price': '价格',
'authors': '作者'
}
# 通过widgets字段,给数据模型中的指定字段添加属性
widgets = {
'publishDate': forms.widgets.TextInput(attrs={'type': 'date'}),
}
error_messages = {
'title': {'required': '不能为空', },
'publish': {'required': '不能为空', },
'publishDate': {'required': '', }, # 不想让某个字段显示报错信息的话,直接设置为空即可
}
# 定义局部钩子的方法,和使用form方法中是一样的
def clean_title(self):
value = self.cleaned_data.get('title')
if '666' in value:
raise ValidationError("光喊6是不行的")
else:
return value
# 定义全局钩子的方法,同样和form方法中是一样的
def clean(self):
pass
# 通过重写父类的__init__方法,给所有字段添加指定的bootstrap属性
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({'class': 'form-control'})