modelform案例,(form组件结合model模型使用,大幅减少form类中字段的定义,直接使用models中的字段就行)
form组件数据校验底层逻辑:for循环遍历所有字段,每个字段都要经过自己的属性值校验和局部钩子函数校验.for循环遍历完成之后,进入全局钩子函数校验.
比如第1个字段经过了自己的属性值校验和局部钩子函数校验之后,不管有没有错都不影响下一个字段的校验.第2个字段再进入自己的属性值校验和局部钩子校验,所有字段都通过前两个阶段之后才能进入全局钩子函数):
数据校验的顺序3步
①经过字段的属性值校验 -------如字段中的max_length, validators属性值校验
②经过字段的局部钩子函数校验 ------clean_字段名()返回字段数据或者抛出(添加)错误信息
③经过全局钩子函数的校验 ------所有字段都经过第1,第2阶段之后才会进入第3阶段进行全局钩子校验
0 .ModelForm源码解读
# ModelForm源码解读
# ModelForm继承自BaseModelForm,BaseModelForm继承自BaseForm
class ModelForm(BaseModelForm, metaclass=ModelFormMetaclass):
pass
class BaseModelForm(BaseForm):
pass
# 以下是BaseForm部分源码摘抄
class BaseForm:
def full_clean(self):
self._clean_fields() #字段的属性值校验和局部钩子函数校验
self._clean_form() #全局钩子函数校验
def _clean_fields(self):
for name, field in self.fields.items():
if field.disabled:
value = self.get_initial_for_field(field, name)
else:
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, FileField):
initial = self.get_initial_for_field(field, name)
value = field.clean(value, initial)
else:
value = field.clean(value) # 经过字段的属性值校验,如经过validators属性值校验
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name): # 根据反射查看当前字段是否定义了局部钩子函数
value = getattr(self, 'clean_%s' % name)() #根据反射找到对应字段的局部钩子函数并调用,如果没有错误必须要返回字段的值
self.cleaned_data[name] = value
except ValidationError as e: #局部钩子函数raise ValidationError()之后被异常处理捕获,调用add_error(),
self.add_error(name, e) #raise ValidationError()和add_error这两个方法都能添加错误信息
def _clean_form(self):
try:
cleaned_data = self.clean() #调用全局钩子函数
except ValidationError as e:
#全局钩子函数的raise ValidationError只能添加全局错误信息,若要给单个字段添加错误信息需要手动调用add_error('字段名','错误信息')
self.add_error(None, e)
else:
if cleaned_data is not None: #全局钩子函数校验成功后必须返回所有字段的信息cleaned_data
self.cleaned_data = cleaned_data
1, model代码
from django.db import models
class Category(models.Model):
def __str__(self):
return self.name
name=models.CharField(verbose_name='文章类别',max_length=10)
class Education(models.Model):
def __str__(self):
return self.name
name=models.CharField(verbose_name='学历',max_length=10)
school=models.CharField(verbose_name='毕业院校',max_length=10,blank=True,null=True)
class User(models.Model):
def __str__(self):
return self.name
name=models.CharField(verbose_name="用户名",max_length=16,default="匿名",)
password=models.CharField(verbose_name='密码',max_length=20,)
email=models.CharField(verbose_name='邮箱',max_length=20,)
sex=models.SmallIntegerField(verbose_name='性别',choices=[(0,"男"),(1,"女")],default=0)
age=models.SmallIntegerField(verbose_name='年龄',default=21)
education=models.ForeignKey(verbose_name='学历',to='Education',default=1,on_delete=models.CASCADE)
cet6=models.BooleanField(verbose_name='是否通过英语六级',default=False)
'''
使用ManyToManyField生成第三方表user_hobby来保存多选字段,当前表并不会生成hobby字段,数据保存在user_hobby表中,
所以实例化User对象时不能传入hobby参数,实例化后也不能通过user.hobby=hobby给user_hobby表添加数据,
只能通过user.hobby.set(hobyy)给user_hobby表添加数据
'''
hobby=models.ManyToManyField(verbose_name='爱好',to="Category",related_name='user',default=[2,3])
birth=models.DateField(verbose_name='生日')
'''
upload_to设置图片在media文件中的保存位置(所以路径中不要再带前缀media了)
media文件夹用来保存用户上传的图片/文件等静态文件,static用来保存服务器本身自带的静态文件如js,css,图片
配置media需要在配置文件中设置访问路径:MEDIA_URL="/media/",存储路径:MEDIA_ROOT=os.path.join(BASE_DIR,'media')
在urls.py中添加media的路由:urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
###保存图片信息最好用ImageField(不仅包含了CharField的功能,还更强大),不要用CharField,虽然两者保存的都只是字符串(图片的地址),
但ImageField会保存图片到本地后再把图片地址这个字符串保存到数据库,ImageField字段既能接受图片对象也能接受字符串,取出的数据是图片对象,
而CharField即便接受到图片对象也不会保存图片,只会保存图片名称到数据库,取出的数据也是字符串,而不是图片对象.
'''
icon=models.ImageField(verbose_name='头像',upload_to='')
profiles=models.TextField(verbose_name='自我介绍',max_length=300)
#使用JSONField保存多选字段,值为列表,数据就保存在当前表里面
skill=models.JSONField(verbose_name='技能',blank=True,null=True)
2, form代码
from django import forms
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from app.models import *
import re
class BootstrapForm2(forms.ModelForm):
# 指定哪些字段生成的表单的class不使用form-control样式
no_use_bootstrap_fields= []
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
for name,field in self.fields.items():
if name not in self.no_use_bootstrap_fields:
field.widget.attrs['class']=field.widget.attrs.get("class","")+" form-control"
class UserForm2(BootstrapForm2):
no_use_bootstrap_fields = ['cet6','skill','hobby','icon']
class Meta:
# 在Meta类中批量添加widgets插件
model=User
# exclude=['sex']
# fields = '__all__'
fields = ['name','password','repassword','sex','email','age','education','cet6',
'hobby','skill','birth','icon','profiles','xxx']
widgets = {
"name": forms.TextInput(attrs={'style': 'color:red'}),
"cet6": forms.CheckboxInput,
"password": forms.PasswordInput,
"birth": forms.DateInput(attrs={'type': 'date'}),
}
repassword = forms.CharField(label='确认密码', max_length=20, min_length=5, widget=forms.PasswordInput,
validators=[RegexValidator(r"^[a-zA-Z_]\w{5,10}$","确认密码必须是以字母或下划线开头的6-11位字符")])
xxx=forms.CharField(label='只读字段',initial="哈哈哈哈哈",disabled=True)
skill=forms.MultipleChoiceField(label='技能*',widget=forms.CheckboxSelectMultiple,
# chioces列表中元素的第一个值是标签中实际显示的值,第二个值是value值,两者可以不同
choices=[('python','python'),('java','java'),('rust','rust'),('js','js')],initial=["python",'rust'],)
def clean_password(self):
pwd=self.cleaned_data.get('password')
pattern=re.compile(r"^[a-zA-Z_]\w{5,10}$")
if not pattern.match(pwd):
# 局部钩子中的ValidationError()是给当前字段添加错误信息,以下两种方式效果一样
raise ValidationError("密码必须是以字母或下划线开头的6-11位字符")
# self.add_error('password',"密码必须是以字母或下划线开头的6-11位字符")
else:
return pwd
def clean_cet6(self):
education=self.cleaned_data.get('education') # education获取到的是表对象!
cet6=self.cleaned_data.get('cet6') #复选框单用,没设置value,所以值是'on'或'',cleaned_data将其转成布尔值,
print(f'打印education={education}')
print(f'打印cet6={cet6}')
# 打印education=初中, education实际是对象可以调用id属性,只是类里面重写了__str__而返回了字符串信息
# 打印cet6=True
if education.id <3 and cet6:
raise ValidationError("中小学生无法过英语6级!")
return cet6
def clean(self):
'''
在全局钩子函数中校验两次密码是否一致,但校验两次密码是否一致也可以在局部钩子函数中获取!
因为password字段的校验在repassword前面
所以不能通过clean_password钩子函数来获取repassword字段的数据.应该在clean_repassword钩子中获取,
因为走到clean_repassword钩子时password的校验已经完成了,可以通过cleaned_data.get("password")获取password了
'''
pwd=self.cleaned_data.get('password')
repwd=self.cleaned_data.get('repassword') # 注意:repassword不删除直接form.save()也不报错
if pwd != repwd:
self.add_error('repassword',"两次密码输入不一致")
# 全局钩子中的ValidationError()添加全局错误信息,无法通过字段获取,要通过formobj.non_field_errors()获取
# raise ValidationError("密码必须是以字母或下划线开头的6-11位字符")
else:
return self.cleaned_data
3, views代码
from django.shortcuts import render,HttpResponse
from app.myforms import *
def register_modify(request):
uid=request.GET.get('uid')
instance = User.objects.filter(id=uid).first() if uid else None
# 获取注册页面,或者获取个人信息
if request.method=='GET':
form_obj = UserForm2(instance=instance)
print(f"打印form_obj.fields={form_obj.fields}")
print(f"打印form_obj['password']={form_obj['password']}")
# 如果是修改个人信息,不会显示密码,form_obj['password']获取不到密码,页面中显示空
return render(request,'注册页面.html',locals())
# 提交注册信息,或者修改个人信息
elif request.method=="POST":
form_obj = UserForm2(data=request.POST,files=request.FILES,instance=instance)
if form_obj.is_valid():
print("打印cleaned_data=",form_obj.cleaned_data)
'''
这是提交过来的修改个人信息的数据,icon没有修改所以类型不是UploadedFile类型
打印cleaned_data= {'name': '成吉思汗', 'password': 'qq12345', 'sex': 0,
'email': '1234214@qq.com', 'age': 25, 'education': <Education: 硕士>,
'cet6': True, 'hobby': <QuerySet [<Category: 军事>, <Category: 政治>]>,
'skill': ['python', 'rust'], 'birth': datetime.date(2023, 8, 9),
'icon': <ImageFieldFile: 12.png>, 'profiles': '收到公司当','xxx': '哈哈哈哈哈'}
'''
# if userform.has_changed():
# print("打印修改的字段form_obj.changed_data=",form_obj.changed_data)
form_obj.save() #多传了字段过来直接save()也不报错,如repassword
return HttpResponse('数据插入成功!')
else:
# 注册页面和修改个人信息用的是同一个html页面,同一个form类
return render(request,'注册页面.html',{'form_obj':form_obj})
return HttpResponse("请求不允许!")
4, html页面
<!DOCTYPE html>
<html lang="en">
<head>
{% load static %}
<script src="{% static 'jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'bootstrap.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'bootstrap.min.css' %}">
<meta charset="UTF-8">
<style>
ul{overflow: hidden}
ul li{float:left;margin:0 10px;list-style: none;}
.form-group,button{margin:20px 0;}
</style>
<title>注册页面</title>
</head>
<body>
<!--
{#<form action="" method="post" class="container">#}
{# form对象.as_xxx 可以一次性生成所有表单标签和错误信息,字段错误信息会显示在字段的上方 #}
{# {{ form_obj.as_p }}#}
{# {{ form_obj.as_table }}#}
{# {{ form_obj.as_ul }}#}
{# <button type="submit">提交哈哈哈</button>#}
{#</form>#}
-->
<div class="container">
<form action="" method="post" class="row" enctype="multipart/form-data">
{% for field in form_obj %}
<div class="col-sm-8">
<div class="form-group">
<label for="{{ field.id_for_label }}"
class="col-sm-3">{{ field.label }}:</label>
<div class="col-sm-9">
{{ field }}
{# 只显示单个字段的某一条错误信息:field.errors.索引值 #}
{# <span class="text-danger">{{ field.errors.0 }}</span>#}
{# 显示单个字段的所有错误信息:field.errors.as_text 全部在一行显示 #}
<span class="text-danger">{{ field.errors.as_text }}</span>
</div>
</div>
</div>
{% endfor %}
{# 显示全局的错误信息form_obj.non_field_errors,因为错误信息涉及到多个字段,所以不能通过单个字段对象来获取 #}
{% for error in form_obj.non_field_errors %}
<p class="text-danger col-md-8">{{ error }}</p>
{% endfor %}
<p class=" col-sm-8 text-center"><button type="submit" class="btn-primary">提交</button></p>
</form>
</div>
</body>
</html>