python项目Django(Form和ModelForm组件)
一、Form介绍
我们之前在HTML页面中利用form表单向后端提交数据时,都会写一些获取用户输入的标签并且用form标签把它们包起来。
与此同时我们在好多场景下都需要对用户的输入做校验,比如校验用户是否输入,输入的长度和格式等正不正确。如果用户输入的内容有错误就需要在页面上相应的位置显示对应的错误信息.。
Django form组件就实现了上面所述的功能。
总结一下,其实form组件的主要功能如下:
-
- 生成页面可用的HTML标签
- 对用户提交的数据进行校验
- 保留上次输入内容
1.1 普通方式手写注册功能:
views.py文件: # 注册 def register(request): error_msg = "" if request.method == "POST": username = request.POST.get("name") pwd = request.POST.get("pwd") # 对注册信息做校验 if len(username) < 6: # 用户长度小于6位 error_msg = "用户名长度不能小于6位" else: # 将用户名和密码存到数据库 return HttpResponse("注册成功") return render(request, "register.html", {"error_msg": error_msg})
login.html文件: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>注册页面</title> </head> <body> <form action="/reg/" method="post"> {% csrf_token %} <p> 用户名: <input type="text" name="name"> </p> <p> 密码: <input type="password" name="pwd"> </p> <p> <input type="submit" value="注册"> <p style="color: red">{{ error_msg }}</p> </p> </form> </body> </html>
1.2 使用form组件实现注册功能:
views.py文件: 先定义好一个RegForm类: from django import forms # 按照Django form组件的要求自己写一个类 class RegForm(forms.Form): name = forms.CharField(label="用户名") #form字段的名称写的是什么,那么前端生成input标签的时候,input标签的name属性的值就是什么 pwd = forms.CharField(label="密码")
再写一个视图函数: # 使用form组件实现注册方式 def register2(request): form_obj = RegForm() if request.method == "POST": # 实例化form对象的时候,把post提交过来的数据直接传进去 form_obj = RegForm(data=request.POST) #既然传过来的input标签的name属性值和form类对应的字段名是一样的,所以接过来后,form就取出对应的form字段名相同的数据进行form校验 # 调用form_obj校验数据的方法 if form_obj.is_valid(): return HttpResponse("注册成功") return render(request, "register2.html", {"form_obj": form_obj})
login2.html文件: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>注册2</title> </head> <body> <form action="/reg2/" method="post" novalidate autocomplete="off"> #novalidate 告诉前端form表单,不要对输入的内容做校验 {% csrf_token %} #{{ form_obj.as_p }} 直接写个这个,下面的用户名和密码的标签不自己写,你看看效果 <div> <label for="{{ form_obj.name.id_for_label }}">{{ form_obj.name.label }}</label> {{ form_obj.name }} {{ form_obj.name.errors.0 }} #errors是这个字段所有的错误,我就用其中一个错误提示就可以了,再错了再提示,并且不是给你生成ul标签了,单纯的是错误文本 {{ form_obj.errors }} #这是全局的所有错误,找对应字段的错误,就要form_obj.字段名 </div> <div> <label for="{{ form_obj.pwd.id_for_label }}">{{ form_obj.pwd.label }}</label> {{ form_obj.pwd }} {{ form_obj.pwd.errors.0 }} </div> <div> <input type="submit" class="btn btn-success" value="注册"> </div> </form> </body> </html>
看网页效果发现 也验证了form的功能:
前端页面是form类的对象生成的 -->生成HTML标签功能
当用户名和密码输入为空或输错之后 页面都会提示 -->用户提交校验功能
当用户输错之后 再次输入 上次的内容还保留在input框 -->保留上次输入内容
二、Form常用字段与插件
创建Form类时,主要涉及到 【字段】 和 【插件】,字段用于对用户请求数据的验证,插件用于自动生成HTML;
2.1 initial:
初始值,input框里面的初始值: class LoginForm(forms.Form): username = forms.CharField( min_length=8, label="用户名", initial="张三" # 设置默认值 ) pwd = forms.CharField(min_length=6, label="密码")
2.2 error_messages:
重写错误信息: class LoginForm(forms.Form): username = forms.CharField( min_length=8, label="用户名", initial="张三", error_messages={ "required": "不能为空", "invalid": "格式错误", "min_length": "用户名最短8位" } ) pwd = forms.CharField(min_length=6, label="密码")
2.3 password:
password密码设置: class LoginForm(forms.Form): ... pwd = forms.CharField( min_length=6, label="密码", widget=forms.widgets.PasswordInput(attrs={'class': 'c1'}, render_value=True) #这个密码字段和其他字段不一样,默认在前端输入数据错误的时候,点击提交之后,默认是不保存的原来数据的,但是可以通过这个render_value=True让这个字段在前端保留用户输入的数据 )
2.4 radioSelect:
单radio值为字符串: class LoginForm(forms.Form): username = forms.CharField( #其他选择框或者输入框,基本都是在这个CharField的基础上通过插件来搞的 min_length=8, label="用户名", initial="张三", error_messages={ "required": "不能为空", "invalid": "格式错误", "min_length": "用户名最短8位" } ) pwd = forms.CharField(min_length=6, label="密码") gender = forms.fields.ChoiceField( choices=((1, "男"), (2, "女"), (3, "保密")), label="性别", initial=3, widget=forms.widgets.RadioSelect() )
2.5 单选Select:
class LoginForm(forms.Form): ... hobby = forms.fields.ChoiceField( #注意,单选框用的是ChoiceField,并且里面的插件是Select,不然验证的时候会报错, Select a valid choice的错误。 choices=((1, "篮球"), (2, "足球"), (3, "双色球"), ), label="爱好", initial=3, widget=forms.widgets.Select() )
2.6 多选Select:
class LoginForm(forms.Form): ... hobby = forms.fields.MultipleChoiceField( #多选框的时候用MultipleChoiceField,并且里面的插件用的是SelectMultiple,不然验证的时候会报错。 choices=((1, "篮球"), (2, "足球"), (3, "双色球"), ), label="爱好", initial=[1, 3], widget=forms.widgets.SelectMultiple() )
2.7 单选checkbox:
# 单选的checkbox方式一:
class LoginForm(forms.Form): ... keep = forms.fields.CharField( label="是否记住密码", initial="checked", widget=forms.widgets.CheckboxInput() )
# 单选的checkbox方式二:
class TestForm2(forms.Form):
keep=forms.ChoiceField(
choices=(
("True", 1),
("False", 0),
),
1abe1="是否7天内自动登录”,
initial = "1",
widget = forms.widgets.checkboxInput()
)
2.8 多选checkbox:
class LoginForm(forms.Form): ... hobby = forms.fields.MultipleChoiceField( choices=((1, "篮球"), (2, "足球"), (3, "双色球"),), label="爱好", initial=[1, 3], widget=forms.widgets.CheckboxSelectMultiple() )
2.9 date类型:
from django import forms from django.forms import widgets class BookForm(forms.Form): date = forms.DateField(widget=widgets.TextInput(attrs={'type':'date'})) #必须指定type,不然不能渲染成选择时间的input框
2.10 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): super(MyForm,self).__init__(*args, **kwargs) #注意重写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对象
方式三: models.py文件: class Student(models.Model): name = models.CharField(max_length=12, null=True) sex_choice = ((1, '男'), (2, '女'),) sex = models.IntegerField( choices=sex_choice, ) views.py文件: def test_choice(request): # 插入数据: # models.Student.objects.create(name="egon", sex="1") # models.Student.objects.create(name="egon", sex=1) # 插入数值与字符串是相同效果 new_obj = models.Student.objects.get(name='chao') print(new_obj.sex) # 1 print(new_obj.get_sex_display()) # 男 格式:get_字段名称_display print(new_obj.name) # chao return render(request, 'testchoice.html', {'new_obj': new_obj})
三、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类型
四、字段校验
4.1 RegexValidator验证器:
from django.forms import Form from django.forms import widgets from django.forms import fields from django.core.validators import RegexValidator class MyForm(Form): user = fields.CharField( validators=[RegexValidator(r'^[0-9]+$', '请输入数字'), RegexValidator(r'^159[0-9]+$', '数字必须以159开头')], )
4.2 自定义验证函数:
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类中定义钩子函数,来实现自定义的验证功能。
5.1 局部钩子:
我们在Fom类中定义 clean_字段名() 方法,就能够实现对特定字段进行校验。
举个例子:
from django.core.exceptions import ValidationError
class LoginForm(forms.Form): username = forms.CharField( min_length=8, label="用户名", initial="张三", error_messages={ "required": "不能为空", "invalid": "格式错误", "min_length": "用户名最短8位" }, widget=forms.widgets.TextInput(attrs={"class": "form-control"}) ) ... # 定义局部钩子,用来校验username字段,之前的校验股则还在,给你提供了一个添加一些校验功能的钩子 def clean_username(self): value = self.cleaned_data.get("username") if "666" in value: raise ValidationError("光喊666是不行的") else: return value
补充:
通过定义局部钩子,验证每个字段的合法性:不满足时返回错误
def clean_username(self):
val = self.cleaned_data.get('username')
user_obj = models.UserInfo.objects.filter(username=val).first()
if user_obj:
raise ValidationError('该用户名已经存在,请换个名字!') # 验证名字是否存在
else:
return val
def clean_password(self):
val = self.cleaned_data.get('password') # 验证密码合法性
if val.isdecimal(): # 这里用isdecimal,建议别用isdigit,不准确
raise ValidationError('密码不能为纯数字')
else:
return val
def clean_email(self):
val = self.cleaned_data.get('email')
if re.search('\w+@163.com$', val):
return val
else:
raise ValidationError('必须是163网易邮箱!') # 指定特定的邮箱格式
5.2 全局钩子:
我们在Fom类中定义 clean() 方法,就能够实现对字段进行全局校验,字段全部验证完,局部钩子也全部执行完之后,执行这个全局钩子校验。
class LoginForm(forms.Form): ... password = forms.CharField( min_length=6, label="密码", widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'}, render_value=True) ) re_password = forms.CharField( min_length=6, label="确认密码", widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'}, render_value=True) ) ... # 定义全局的钩子,用来校验密码和确认密码字段是否相同,执行全局钩子的时候,cleaned_data里面肯定是有了通过前面验证的所有数据 def clean(self): password_value = self.cleaned_data.get('password') re_password_value = self.cleaned_data.get('re_password') if password_value == re_password_value: return self.cleaned_data #全局钩子要返回所有的数据 else: self.add_error('re_password', '两次密码不一致') #在re_password这个字段的错误列表中加上一个错误,并且clean_data里面会自动清除这个re_password的值,
所以打印clean_data的时候会看不到它,这个错误信息放在re_password的局部错误中 # raise ValidationError('两次密码不一致') # 这个报错信息放在全局的错误中
六 进阶补充
6.1 应用Bootstrap样式:
Django form应用Bootstrap样式简单示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <title>login</title> </head> <body> <div class="container"> <div class="row"> <form action="/login2/" method="post" novalidate class="form-horizontal"> {% csrf_token %} <div class="form-group"> <label for="{{ form_obj.username.id_for_label }}" class="col-md-2 control-label">{{ form_obj.username.label }}</label> <div class="col-md-10"> {{ form_obj.username }} <span class="help-block">{{ form_obj.username.errors.0 }}</span> </div> </div> <div class="form-group"> <label for="{{ form_obj.pwd.id_for_label }}" class="col-md-2 control-label">{{ form_obj.pwd.label }}</label> <div class="col-md-10"> {{ form_obj.pwd }} <span class="help-block">{{ form_obj.pwd.errors.0 }}</span> </div> </div> <div class="form-group"> <label class="col-md-2 control-label">{{ form_obj.gender.label }}</label> <div class="col-md-10"> <div class="radio"> {% for radio in form_obj.gender %} <label for="{{ radio.id_for_label }}"> {{ radio.tag }}{{ radio.choice_label }} </label> {% endfor %} </div> </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <button type="submit" class="btn btn-default">注册</button> </div> </div> </form> </div> </div> <script src="/static/jquery-3.2.1.min.js"></script> <script src="/static/bootstrap/js/bootstrap.min.js"></script> </body> </html>
6.2 批量添加样式:
可通过重写form类的init方法来实现。
class LoginForm(forms.Form): username = forms.CharField( min_length=8, label="用户名", initial="张三", error_messages={ "required": "不能为空", "invalid": "格式错误", "min_length": "用户名最短8位" } ... def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) for field in iter(self.fields): self.fields[field].widget.attrs.update({ 'class': 'form-control' })
6.3 项目实例:
简单写一个小项目:book表的添加和数据展示
from django.db import models # 作者信息 class Author(models.Model): nid = models.AutoField(primary_key=True) name = models.CharField(max_length=32) age = models.IntegerField() authorDetail = models.OneToOneField(to="AuthorDetail", to_field="nid") def __str__(self): return self.name # 作者的详细信息 class AuthorDetail(models.Model): nid = models.AutoField(primary_key=True) birthday = models.DateField() telephone = models.BigIntegerField() addr = models.CharField(max_length=64) # 出版社信息 class Publish(models.Model): nid = models.AutoField(primary_key=True) name = models.CharField(max_length=32) city = models.CharField(max_length=32) email = models.EmailField() def __str__(self): return self.name # 书籍信息 class Book(models.Model): nid = models.AutoField(primary_key=True) title = models.CharField(max_length=32) publishDate = models.DateField() price = models.DecimalField(max_digits=5, decimal_places=2) publish = models.ForeignKey(to="Publish", to_field="nid") authors = models.ManyToManyField(to='Author', ) def __str__(self): return self.title
from django.shortcuts import render, HttpResponse, redirect from django import forms from app01 import models class MyForm1(forms.Form): # 标题 title = forms.CharField( max_length=32, min_length=2, # 自定义最大长度,最小长度 # required=False, # # initial='用户名', # 用户提示信息 help_text='这里是输入用户名的地方,别忘了不能小于2位!', # 帮助信息 error_messages={ # 自定义提示信息 "min_length": '长度不能小于2', "required": '该字段不能为空!', "max_length": '字段过长,不能超过32位!' }, label='书名', # 定义标签名字 widget=forms.widgets.TextInput(attrs={'placeholder': '用户名'}) # 通过widget自定义用户提示信息 ) # 价格 price = forms.IntegerField( label='价格', widget=forms.widgets.NumberInput(attrs={'placeholder': '价格'}) ) # 出版社日期 publishDate = forms.DateField( label='出版社日期', # widget=forms.widgets.DateInput(attrs={'class': 'form-control', 'type': 'date'}) widget=forms.widgets.DateInput(attrs={'type': 'date'}) # 必须指定type,不然不能渲染成选择时间的input框 ) # 出版社 publish = forms.ModelChoiceField( label='出版社名称', queryset=models.Publish.objects.all(), # 获取所有的出版社信息 # widget=forms.widgets.Select(attrs={'class': 'form-control'}) # 单一形式添加样式 widget=forms.widgets.Select() # 单选 ) # 作者 authors = forms.ModelMultipleChoiceField( label='作者', queryset=models.Author.objects.all(), widget=forms.widgets.SelectMultiple() # 多选 ) # 批量添加样式 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field in self.fields: self.fields[field].widget.attrs.update({ 'class': 'form-control' }) # 扩展 # 邮箱配置 # email = forms.EmailField( # error_messages={ # 'invalid':'请输入正确的邮箱格式' # } # ) # 指定choices值 # 这种方式要通过__init__方法来指定choices # publish = forms.ChoiceField() # def __init__(self,*args,**kwargs): # super().__init__(*args,**kwargs) # self.fields['publish'].choices = models.Publish.objects.all().values_list('pk','name') # 单选框 # sex = forms.ChoiceField( # choices=( # ('1', '男'), # ('2', '女'), # ), # # widget=forms.widgets.RadioSelect() # widget=forms.widgets.Select(attrs={'class': 'form-control'}) # ) # 多选框 # author = forms.MultipleChoiceField( # choices=( # ('1', '男'), # ('2', '女'), # ), # widget=forms.widgets.CheckboxSelectMultiple(), # ) def index1(request): if request.method == 'GET': form_obj = MyForm1() # 实例化这个MyForm return render(request, 'index1.html', {'form_obj': form_obj}) else: data = request.POST # print(data) form_obj = MyForm1(data) if form_obj.is_valid(): # 验证每个字段传过来的数据是不是正确的,正确返回True,否则返回False data = form_obj.cleaned_data # 验证后的数据,这里只会验证MyForm类里面指定的数据 print(data) author_data = data.pop('authors') # 删除多余字段 print(author_data) book_obj = models.Book.objects.create(**data) book_obj.authors.add(*author_data) return HttpResponse('ok') else: print(form_obj.errors) # 所有的错误信息 return render(request, 'index1.html', {'form_obj': form_obj}) # ##################################################################################################################### import re from django.core.exceptions import ValidationError from django.core.validators import RegexValidator # 自定义验证函数 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 MyForm2(forms.Form): # RegexValidator验证器 与 自定义验证函数 name = forms.CharField( label="手机号", max_length=32, # validators=[RegexValidator(r'^a','必须以a开头'),], # RegexValidator验证器 validators=[mobile_validate, ], # 自定义验证函数 widget=forms.widgets.TextInput(attrs={'class': 'form-control'}) ) # 密码 password = forms.CharField( label="密码", widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'}, render_value=True) # 密码错误时,保存当前错误密码 ) # 确认密码 r_password = forms.CharField( label="确认密码", widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'}, render_value=True) ) # 局部钩子 def clean_name(self): # 格式:clean_字段名字 value = self.cleaned_data.get('name') # {'name': '九阴真经'} if '6666' in value: raise ValidationError('手机号不能连续4个数字相同!') else: return value # 必须有个返回值 # 全局钩子 def clean(self): p1 = self.cleaned_data.get('password') p2 = self.cleaned_data.get('r_password') if p1 == p2: return self.cleaned_data # return所有的cleaned_data else: self.add_error('r_password', '两次输入的密码不一致') # 添加错误标签到当前标签 # raise ValidationError('两次输入的密码不一致!') # 添加错误到全局错误列表 def index2(request): if request.method == 'GET': form_obj = MyForm2() return render(request, 'index2.html', {'form_obj': form_obj}) else: data = request.POST # 接收到的所有数据 print(data) form_obj = MyForm2(data) if form_obj.is_valid(): print(form_obj.cleaned_data) else: return render(request, 'index2.html', {'form_obj': form_obj})
{% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}" rel="stylesheet"> </head> <body> <h1> 这是添加页面 </h1> {#{{ form_obj.as_p }} #} {# 标签自动生成,不需要自己写 #} {# 方式一: #} {#<div class="container-fluid">#} {# <div class="row">#} {# <div class="col-md-6 col-md-offset-3">#} {##} {# <form action="{% url 'index1' %}" method="post" novalidate> {# novalidate:忽略浏览器错误 #} {# {{ form_obj.errors }} {# 全局错误 #} {# {% csrf_token %}#} {# <div class="form-group">#} {# <label for="{{ form_obj.title.id_for_label }}"> {{ form_obj.title.label }}</label>#} {# {{ form_obj.title }}#} {# </div>#} {##} {# <div class="form-group">#} {# <label for="{{ form_obj.price.id_for_label }}"> {{ form_obj.price.label }}</label>#} {# {{ form_obj.price }}#} {# </div>#} {# <div class="form-group">#} {# <label for="{{ form_obj.publishDate.id_for_label }}"> {{ form_obj.publishDate.label }}</label>#} {# {{ form_obj.publishDate }}#} {# </div>#} {# <div class="form-group">#} {# <label for="{{ form_obj.publish.id_for_label }}"> {{ form_obj.publish.label }}</label>#} {# {{ form_obj.publish }}#} {# </div>#} {# <div class="form-group">#} {# <label for="{{ form_obj.authors.id_for_label }}"> {{ form_obj.authors.label }}</label>#} {# {{ form_obj.authors }}#} {# </div>#} {##} {# <input type="submit" class="btn btn-success pull-right" value="保存">#} {# </form>#} {##} {# </div>#} {# </div>#} {#</div>#} {# 方式二: #} <div class="container-fluid"> <div class="row"> <div class="col-md-6 col-md-offset-3"> <form action="{% url 'index1' %}" method="post" novalidate> {# novalidate:忽略浏览器错误 #} {# {{ form_obj.errors }} {# 全局错误 #} {% csrf_token %} {% for field in form_obj %} <div class="form-group {% if field.errors.0 %} has-error {% endif %}"> <label for="{{ field.id_for_label }}"> {{ field.label }}</label> {{ field }} <span class="text-success">{{ field.help_text }}</span> {# 帮助信息 #} <span class="text-danger">{{ field.errors.0 }}</span> {# 显示第一条错误 #} </div> {% endfor %} <input type="submit" class="btn btn-success pull-right" value="保存"> </form> </div> </div> </div> <script src="{% static 'jquery-3.4.1.js' %}"></script> <script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script> </body> </html>
{% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}" rel="stylesheet"> </head> <body> <div class="container-fluid"> <div class="row"> <div class="col-md-offset-3 col-md-6"> <form action="{% url 'index2' %}" method="post" novalidate> {% csrf_token %} <div> {# {{ form_obj.errors }}#} </div> {{ form_obj.name.label }} {{ form_obj.name }} {{ form_obj.name.errors.0 }} <div> {{ form_obj.password.label }} {{ form_obj.password }} {{ form_obj.password.errors.0 }} </div> <div> {{ form_obj.r_password.label }} {{ form_obj.r_password }} {{ form_obj.r_password.errors.0 }} </div> <input type="submit"> </form> </div> </div> </div> <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script> <script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script> <script> </script> </body> </html>
七、ModelForm
通常在Django项目中,我们编写的大部分都是与Django 的模型紧密映射的表单。 举个例子,你也许会有个Book 模型,并且你还想创建一个form表单用来添加和编辑书籍信息到这个模型中。 在这种情况下,在form表单中定义字段将是冗余的,因为我们已经在模型中定义了那些字段。
基于这个原因,Django 提供一个辅助类来让我们可以从Django 的模型创建Form,这就是ModelForm。
7.1 modelForm定义:
form与model的终极结合,会根据你model中的字段转换成对应的form字段,并且并你生成标签等操作。
比如你的models中的表是下面的内容:
class Book(models.Model): nid = models.AutoField(primary_key=True) title = models.CharField( max_length=32) publishDate=models.DateField() price=models.DecimalField(max_digits=5,decimal_places=2) publish=models.ForeignKey(to="Publish",to_field="nid") authors=models.ManyToManyField(to='Author',) def __str__(self): return self.title
modelform类的写法:
class BookForm(forms.ModelForm): class Meta: model = models.Book fields = "__all__" labels = { "title": "书名", "price": "价格" } widgets = { "password": forms.widgets.PasswordInput(attrs={"class": "c1"}), "publishDate": forms.widgets.DateInput(attrs={"type": "date"}), }
class Meta下常用参数:
model = models.Book # 对应的Model中的类 fields = "__all__" # 字段,如果是__all__,就是表示列出所有的字段 exclude = None # 排除的字段 labels = None # 提示信息 help_texts = None # 帮助提示信息 widgets = None # 自定义插件 error_messages = None # 自定义错误信息 error_messages = { 'title':{'required':'不能为空',...} #每个字段的所有的错误都可以写,...是省略的意思,复制黏贴我代码的时候别忘了删了... }
批量添加样式:和form的一样
class BookForm(forms.ModelForm): r_password = forms.CharField() #想多验证一些字段可以单独拿出来写,按照form的写法,写在Meta的上面或者下面都可以 class Meta: model = models.Book # fields = ['title','price'] fields = "__all__" #['title,'price'] 指定字段生成form # exclude=['title',] #排除字段 labels = { "title": "书名", "price": "价格" } error_messages = { 'title':{'required':'不能为空',} #每个字段的错误都可以写 } #如果models中的字段和咱们需要验证的字段对不齐的是,比如注册时,咱们需要验证密码和确认密码两个字段数据,但是后端数据库就保存一个数据就行,那么验证是两个,数据保存是一个,就可以再接着写form字段 r_password = forms.CharField()。 #同样的,如果想做一些特殊的验证定制,那么和form一昂,也是那两个钩子(全局和局部),写法也是form那个的写法,直接在咱们的类里面写: #局部钩子: def clean_title(self): pass #全局钩子 def clean(self): pass def __init__(self,*args,**kwargs): #批量操作 super().__init__(*args,**kwargs) for field in self.fields: #field.error_messages = {'required':'不能为空'} #批量添加错误信息,这是都一样的错误,不一样的还是要单独写。 self.fields[field].widget.attrs.update({'class':'form-control'})
7.2 ModelForm的验证:
与普通的Form表单验证类型类似,ModelForm表单的验证在调用is_valid() 或访问errors 属性时隐式调用。
我们可以像使用Form类一样自定义局部钩子方法和全局钩子方法来实现自定义的校验规则。
如果我们不重写具体字段并设置validators属性的话,ModelForm是按照模型中字段的validators来校验的。
每个ModelForm还具有一个save()方法。 这个方法根据表单绑定的数据创建并保存数据库对象。 ModelForm的子类可以接受现有的模型实例作为关键字参数instance;如果提供此功能,则save()将更新该实例。 如果没有提供,save() 将创建模型的一个新实例:
save方法: >>> from myapp.models import Book >>> from myapp.forms import BookForm # 根据POST数据创建一个新的form对象 >>> form_obj = BookForm(request.POST) # 创建书籍对象 >>> new_ book = form_obj.save() # 基于一个书籍对象创建form对象 >>> edit_obj = Book.objects.get(id=1) # 使用POST提交的数据更新书籍对象 >>> form_obj = BookForm(request.POST, instance=edit_obj) >>> form_obj.save()
通过form组件来保存书籍表数据的时候的写法:
def index(request): if request.method == 'GET': form_obj = BookForm() return render(request,'index.html',{'form_obj':form_obj}) else: form_obj = BookForm(request.POST) if form_obj.is_valid(): # authors_obj = form_obj.cleaned_data.pop('authors') # new_book_obj = models.Book.objects.create(**form_obj.cleaned_data) # new_book_obj.authors.add(*authors_obj) form_obj.save() #因为我们再Meta中指定了是哪张表,所以它会自动识别,不管是外键还是多对多等,都会自行处理保存,它完成的就是上面三句话做的事情,
并且还有就是如果你验证的数据比你后端数据表中的字段多,那么他自会自动剔除多余的不需要保存的字段,比如那个重复确认密码就不要保存 return redirect('show') else: print(form_obj.errors) return render(request,'index.html',{'form_obj':form_obj})
比如说我们图书管理系统页面之前是这样写的
{% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" href="{% static 'bootstrap-3.3.0-dist/dist/css/bootstrap.min.css' %}"> </head> <body> <h1>编辑页面</h1> <div class="container-fluid"> <div class="row"> <div class="col-md-6 col-md-offset-3"> <form action=""> <div class="form-group"> <label for="title">书名</label> <input type="text" class="form-control" id="title" placeholder="title" value="{{ book_obj.title }}"> </div> <div class="form-group"> <label for="publishDate">出版日期</label> <input type="text" class="form-control" id="publishDate" placeholder="publishDate" value="{{ book_obj.publishDate|date:'Y-m-d' }}"> </div> <div class="form-group"> <label for="price">价格</label> <input type="number" class="form-control" id="price" placeholder="price" value="{{ book_obj.price }}"> </div> <div class="form-group"> <label for="publish">书名</label> <select name="publish" id="publish" class="form-control"> {% for publish in all_publish %} {% if publish == book_obj.publish %} <option value="{{ publish.id }}" selected>{{ publish.name }}</option> {% else %} <option value="{{ publish.id }}">{{ publish.name }}</option> {% endif %} {% endfor %} </select> </div> <div class="form-group"> <label for="authors">书名</label> <select name="authors" id="authors" multiple class="form-control"> {% for author in all_authors %} {% if author in book_obj.authors.all %} <option value="{{ author.id }}" selected>{{ author.name }}</option> {% else %} <option value="{{ author.id }}" >{{ author.name }}</option> {% endif %} {% endfor %} </select> </div> </form> </div> </div> </div> </body> <script src="{% static 'bootstrap-3.3.0-dist/dist/jQuery/jquery-3.1.1.js' %}"></script> <script src="{% static 'bootstrap-3.3.0-dist/dist/js/bootstrap.min.js' %}"></script> </html>
views.py是这样写的:
def edit_book(request,n): book_obj = models.Book.objects.filter(pk=n).first() if request.method == 'GET': all_authors = models.Author.objects.all() # all_publish = models.Publish.objects.all() return render(request,'edit_book.html',{'book_obj':book_obj,'all_authors':all_authors,'all_publish':all_publish})
改成使用modelform之后,我们这样写:
{% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" href="{% static 'bootstrap-3.3.0-dist/dist/css/bootstrap.min.css' %}"> </head> <body> <h1>编辑页面</h1> <div class="container-fluid"> <div class="row"> <div class="col-md-6 col-md-offset-3"> <form action="{% url 'edit_book' n %}" novalidate method="post"> {% csrf_token %} {% for field in form %} <div class="form-group"> <label for="{{ field.id_for_label }}">{{ field.label }}</label> {{ field }} <span class="text-danger">{{ field.errors.0 }}</span> </div> {% endfor %} <div class="form-group"> <input type="submit" class="btn btn-primary pull-right"> </div> </form> </div> </div> </div> </body> <script src="{% static 'bootstrap-3.3.0-dist/dist/jQuery/jquery-3.1.1.js' %}"></script> <script src="{% static 'bootstrap-3.3.0-dist/dist/js/bootstrap.min.js' %}"></script> </html>
views.py这样写:
def edit_book(request,n): book_obj = models.Book.objects.filter(pk=n).first() if request.method == 'GET': # all_authors = models.Author.objects.all() # # all_publish = models.Publish.objects.all() form = BookForm(instance=book_obj) return render(request,'edit_book.html',{'form':form,'n':n}) #传递的这个n参数是给form表单提交数据的是的action的url用的,因为它需要一个参数来识别是更新的哪条记录 else: form = BookForm(request.POST,instance=book_obj) #必须指定instance,不然我们调用save方法的是又变成了添加操作 if form.is_valid(): form.save() return redirect('show') else: return render(request,'edit_book.html',{'form':form,'n':n})
7.3 ModelForm设置独立字段:
补充一: 示例: class ConsultRecordModelForm(forms.ModelForm): class Meta: model = models.ConsultRecord fields = "__all__" exclude = ["delete_status", ] def __init__(self, request, *args, **kwargs): super().__init__(*args, **kwargs) # 单独设置字段 # 查询当前用户 self.fields["consultant"].queryset = models.UserInfo.objects.filter(pk=request.user.id) # 当前用户对应的客户信息 self.fields["customer"].queryset = models.Customer.objects.filter(consultant=request.user) for field in self.fields: self.fields[field].widget.attrs.update({ "class": "form-control", })
补充二:
示例:
# ModelForm字段:客户表字段
class CustomerModelForm(forms.ModelForm):
class Meta:
model = models.Customer
fields = "__all__"
def __init__(self, *args, **kwargs):
super(CustomerModelForm, self).__init__(*args, **kwargs)
# print(self.fields)
from multiselectfield.forms.fields import MultiSelectFormField # 对某一字段不设置ModeForm样式
for field in self.fields:
if not isinstance(self.fields[field], MultiSelectFormField):
self.fields[field].widget.attrs.update({
"class": "form-control",
})
八、modelform实例
这是一个神奇的组件,通过名字我们可以看出来,这个组件的功能就是把model和form组合起来,先来一个简单的例子来看一下这个东西怎么用:比如我们的数据库中有这样一张学生表,字段有姓名,年龄,爱好,邮箱,电话,住址,注册时间等等一大堆信息,现在让你写一个创建学生的页面,你的后台应该怎么写呢?首先我们会在前端一个一个罗列出这些字段,让用户去填写,然后我们从后天一个一个接收用户的输入,创建一个新的学生对象,保存其实,重点不是这些,而是合法性验证,我们需要在前端判断用户输入是否合法,比如姓名必须在多少字符以内,电话号码必须是多少位的数字,邮箱必须是邮箱的格式这些当然可以一点一点手动写限制,各种判断,这毫无问题,除了麻烦我们现在有个更优雅(以后在Python相关的内容里,要多用“优雅”这个词,并且养成习惯)的方法:ModelForm先来简单的,生硬的把它用上,再来加验证条件。
8.1 创建modelform:
#首先导入ModelForm
from django.forms import ModelForm
#在视图函数中,定义一个类,比如就叫StudentList,这个类要继承ModelForm,在这个类中再写一个原类Meta(规定写法,并注意首字母是大写的)
#在这个原类中,有以下属性(部分):
class StudentList(ModelForm):
class Meta:
model =Student #对应的Model中的类
fields = "__all__" #字段,如果是__all__,就是表示列出所有的字段
exclude = None #排除的字段
#error_messages用法:
error_messages = {
'name':{'required':"用户名不能为空",},
'age':{'required':"年龄不能为空",},
}
#widgets用法,比如把输入用户名的input框给为Textarea
#首先得导入模块
from django.forms import widgets as wid #因为重名,所以起个别名
widgets = {
"name":wid.Textarea(attrs={"class":"c1"}) #还可以自定义属性
}
#labels,自定义在前端显示的名字
labels= {
"name":"用户名"
}
然后在url对应的视图函数中实例化这个类,把这个对象传给前端
def student(request):
if request.method == 'GET':
student_list = StudentList()
return render(request,'student.html',{'student_list':student_list})
然后前端只需要 {{ student_list.as_p }} 一下,所有的字段就都出来了,可以用as_p显示全部,也可以通过for循环这student_list,拿到的是一个个input框,现在我们就不用as_p,手动把这些input框搞出来,as_p拿到的页面太丑。
首先 for循环这个student_list,拿到student对象,直接在前端打印这个student,是个input框student.label ,拿到数据库中每个字段的verbose_name ,如果没有设置这个属性,拿到的默认就是字段名,还可以通过student.errors.0 拿到错误信息有了这些,我们就可以通过bootstrap,自己拼出来想要的样式了,比如:
<body> <div class="container"> <h1>student</h1> <form method="POST" novalidate> {% csrf_token %} {# {{ student_list.as_p }}#} {% for student in student_list %} <div class="form-group col-md-6"> {# 拿到数据字段的verbose_name,没有就默认显示字段名 #} <label class="col-md-3 control-label">{{ student.label }}</label> <div class="col-md-9" style="position: relative;">{{ student }}</div> </div> {% endfor %} <div class="col-md-2 col-md-offset-10"> <input type="submit" value="提交" class="btn-primary"> </div> </form> </div> </body>
现在还缺一个input框的form-contral样式,可以考虑在后台的widget里面添加
比如这样:
from django.forms import widgets as wid #因为重名,所以起个别名
widgets = {
"name":wid.TextInput(attrs={'class':'form-control'}),
"age":wid.NumberInput(attrs={'class':'form-control'}),
"email":wid.EmailInput(attrs={'class':'form-control'})
}
8.2 添加纪录:
# 保存数据的时候,不用挨个取数据了,只需要save一下:
def student(request):
if request.method == 'GET':
student_list = StudentList()
return render(request,'student.html',{'student_list':student_list})
else:
student_list = StudentList(request.POST)
if student_list.is_valid():
student_list.save()
return redirect(request,'student_list.html',{'student_list':student_list})
8.3 编辑数据:
如果不用ModelForm,编辑的时候得显示之前的数据吧,还得挨个取一遍值,如果ModelForm,只需要加一个instance=obj(obj是要修改的数据库的一条数据的对象)就可以得到同样的效果
保存的时候要注意,一定要注意有这个对象(instance=obj),否则不知道更新哪一个数据
from django.shortcuts import render,HttpResponse,redirect
from django.forms import ModelForm
# Create your views here.
from app01 import models
def test(request):
# model_form = models.Student
model_form = models.Student.objects.all()
return render(request,'test.html',{'model_form':model_form})
class StudentList(ModelForm):
class Meta:
model = models.Student #对应的Model中的类
fields = "__all__" #字段,如果是__all__,就是表示列出所有的字段
exclude = None #排除的字段
labels = None #提示信息
help_texts = None #帮助提示信息
widgets = None #自定义插件
error_messages = None #自定义错误信息
#error_messages用法:
error_messages = {
'name':{'required':"用户名不能为空",},
'age':{'required':"年龄不能为空",},
}
#widgets用法,比如把输入用户名的input框给为Textarea
#首先得导入模块
from django.forms import widgets as wid #因为重名,所以起个别名
widgets = {
"name":wid.Textarea
}
#labels,自定义在前端显示的名字
labels= {
"name":"用户名"
}
def student(request):
if request.method == 'GET':
student_list = StudentList()
return render(request,'student.html',{'student_list':student_list})
else:
student_list = StudentList(request.POST)
if student_list.is_valid():
student_list.save()
return render(request,'student.html',{'student_list':student_list})
def student_edit(request,pk):
obj = models.Student.objects.filter(pk=pk).first()
if not obj:
return redirect('test')
if request.method == "GET":
student_list = StudentList(instance=obj)
return render(request,'student_edit.html',{'student_list':student_list})
else:
student_list = StudentList(request.POST,instance=obj)
if student_list.is_valid():
student_list.save()
return render(request,'student_edit.html',{'student_list':student_list})
总结: 从上边可以看到ModelForm用起来是非常方便的,比如增加修改之类的操作。但是也带来额外不好的地方,model和form之间耦合了。如果不耦合的话,mf.save()方法也无法直接提交保存。 但是耦合的话使用场景通常局限用于小程序,写大程序就最好不用了。
九、其他补充(isdigit 与 isdecimal 区别)
isdigit与isdecimel用法比较: a = "1" print(a.isdigit()) # True print(a.isdecimal()) # True a = '1.111' print(a.isdigit()) # False print(a.isdecimal()) # False a = '⑴' print(a.isdigit()) # True print(a.isdecimal()) # False