1-Django - form和modelform
before
form和modelform(也称forms和modelforms)组件主要的功能:
- 数据校验。
- 前端页面显示错误信息。
这个部分内容非常重要,在传统web项目中,这两个组件能大大的减轻我们的代码量。
比如form功能可以脱离数据库,当我们有对数据校验的需求时,就可以用form组件进行数据校验,校验成功的数据随你拿去干嘛。
而modelfrom则结合数据库,减轻你增删改查的代码。
另外,在前后端分离的项目中, 我们后段通常使用Django restful framework框架,其内部的序列化器内部也是用的form和modelform,所以这部分有必要好好学学。
下面的笔记,是我通过在不同的项目中使用form和modelfrom时遇到的问题,然后将解决方案,摘抄出来的代码片段,所以看起来可能不连贯。
当然,这不是form和modelfrom的所有知识点,我只是记录了我常用的。
Form
快速上手
这里的项目名称叫做demo,在项目目录下新建一个test.py
:
import os
import django
from django import forms
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") # demo:项目名称
django.setup()
class UserForm(forms.Form): # 类名随意,但必须继承forms.Form
# min_length、max_length都是规则
# 字段名字和数据库中的表字段保持一致
user = forms.CharField(min_length=3, max_length=6)
pwd = forms.CharField()
email = forms.EmailField()
if __name__ == '__main__':
"""
user = UserForm({"user": "a", "pwd": 123, "email": "123"})
# 校验数据,完全合法返回True,否则返回False
print(user.is_valid())
# 想要获取干净的数据,必须先经过is_valid校验,否则报错
# AttributeError: 'UserForm' object has no attribute 'cleaned_data'
print(user.cleaned_data) # 返回(校验成功的,可能是部分,也可能是全部)干净的数据{'pwd': '123'}
print(type(user.errors), user.errors) # 错误信息类型和错误信息
"""
# 校验时,只对类中定义的字段做校验,对于多余的字段,不做处理
user = UserForm({"user": "abc", "pwd": 123, "email": "123@qq.com", "more_field": "xxxx"})
print(user.is_valid()) # True
print(user.cleaned_data) # {'user': 'abc', 'pwd': '123', 'email': '123@qq.com'}
print(type(user.errors), user.errors) # <class 'django.forms.utils.ErrorDict'> {}
这里牢记三个方法:
is_valid()
,form组件类中定义的所有字段,都通过其规则校验的话,返回True,有一项或者若干项校验失败,返回False。cleaned_data
,只有先经过is_valid校验后,才能使用该方法进行获取干净的数据。errors
,校验失败的错误信息,是个字典,存放一个或多个错误信息。- 还有个
errors.as_json()
,校验失败的错误信息直接转为json数据。
如何将解决错误信息由英文改为中文
form组件校验失败的错误信息,默认是英文,如果改为中文提示呢?这里提供两种解决办法。
修改settings.py
# LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'zh-hans'
自定义错误信息error_messages字典
from django import forms
class UserForm(forms.Form):
user = forms.CharField(
min_length=5, max_length=13,
error_messages={"required": "该字段不能为空"}
)
pwd = forms.CharField(error_messages={"required": "该字段不能为空"})
email = forms.EmailField(error_messages={
"required": "该字段不能为空",
"invalid": "邮箱格式错误"
})
error_messages
字典中可以写多种错误信息,以应对多种校验规则。
简单示例
上一小节,大致了解了form组件的基本用法,这次我们结合前端页面来使用。
models.py
,然后自行进行数据库迁移:
from django.db import models
class User(models.Model):
user = models.CharField(max_length=32, verbose_name='用户名')
pwd = models.CharField(max_length=64, verbose_name='密码')
email = models.CharField(max_length=32, verbose_name='邮箱')
def __str__(self):
return self.user
urls.py
:
from django.contrib import admin
from django.urls import path, re_path
from app01 import views
urlpatterns = [
path('admin/', admin.site.urls),
path('register/', views.register)
]
views.py
:
import os
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from app01 import models
class UserForm(forms.Form):
user = forms.CharField(
min_length=5, max_length=13,
error_messages={"required": "该字段不能为空"}
)
pwd = forms.CharField(error_messages={"required": "该字段不能为空"})
email = forms.EmailField(error_messages={
"required": "该字段不能为空",
"invalid": "邮箱格式错误"
})
def register(request):
if request.method == "POST":
forms_obj = UserForm(request.POST)
if forms_obj.is_valid(): # 必须先进行is_valid,才能取
models.User.objects.create(**forms_obj.cleaned_data)
return HttpResponse("OK")
else:
# 错误信息包含在forms_obj对象中,我们在前端通过该模板对象进行提取
return render(request, 'register.html', {"forms_obj": forms_obj})
return render(request, 'register.html')
register.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form组件</title>
<style>
.errorMsg{
color: red;
margin-left: 10px;
}
</style>
</head>
<body>
<form action="" method="post">
{% csrf_token %}
<p>用户名 <input type="text" name="user"> <span class="errorMsg">{{ forms_obj.errors.user.0 }}</span></p>
<p>密 码 <input type="password" name="pwd"> <span class="errorMsg">{{ forms_obj.errors.pwd.0 }}</span></p>
<p>邮 箱 <input type="text" name="email"> <span class="errorMsg">{{ forms_obj.errors.email.0 }}</span></p>
<p><input type="submit"></p>
</form>
</body>
</html>
这个是最基础的示例,还有很多问题,比如,返回错误信息时,没有问题的字段内容,由于页面刷新也被清空了,以及我们怎么自定义更多更复杂的校验规则,比如两次密码输入不一致的问题等等,都是我们接下来要解决的。
前端标签的不同渲染方式
这一小节我们来了解下,form组件提供的前端标签的渲染的几种方式。
基础版
也就是上一个小节中,我们用的那种,前端代码自己手扣,后台代码也不用做特殊处理。
后台其他代码不变,views.py
:
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from app01 import models
class UserForm(forms.Form):
user = forms.CharField(
min_length=5, max_length=13,
error_messages={"required": "该字段不能为空"}
)
pwd = forms.CharField(error_messages={"required": "该字段不能为空"})
email = forms.EmailField(error_messages={
"required": "该字段不能为空",
"invalid": "邮箱格式错误"
})
def register(request):
if request.method == "POST":
forms_obj = UserForm(request.POST)
if forms_obj.is_valid(): # 必须先进行is_valid,才能取
models.User.objects.create(**forms_obj.cleaned_data)
return HttpResponse("OK")
else:
return render(request, 'register.html', {"forms_obj": forms_obj})
return render(request, 'register.html', {"forms_obj": forms_obj})
register.py
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form组件</title>
<style>
.errorMsg{
color: red;
margin-left: 10px;
}
</style>
</head>
<body>
<form action="" method="post">
{% csrf_token %}
<p>用户名 <input type="text" name="user"> <span class="errorMsg">{{ forms_obj.errors.user.0 }}</span></p>
<p>密 码 <input type="password" name="pwd"> <span class="errorMsg">{{ forms_obj.errors.pwd.0 }}</span></p>
<p>邮 箱 <input type="text" name="email"> <span class="errorMsg">{{ forms_obj.errors.email.0 }}</span></p>
<p><input type="submit"></p>
</form>
</body>
</html>
这种方式最大的问题,就是数据校验不通过,前端页面因为刷新,导致没问题的input的值也没了.....
进阶 省心省力版
这个版本倒是省心省力,前端input标签不需要自己写,form组件帮我们做了,但也因此少了灵活性,用的也少,后端代码不需要做特殊处理,记得form标签需要加novalidate(目的是避免浏览器帮我们做数据校验),csrf_token和提交按钮不会自动生成,还需要我们自己处理。
特点就是错误信息需要我们手动处理,但解决前端页面因为刷新,导致没问题的input的值没了的问题。
需要后台代码配合。
views.py
:
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from app01 import models
class UserForm(forms.Form):
user = forms.CharField(
min_length=5, max_length=13,
error_messages={"required": "该字段不能为空"}
)
pwd = forms.CharField(error_messages={"required": "该字段不能为空"})
email = forms.EmailField(error_messages={
"required": "该字段不能为空",
"invalid": "邮箱格式错误"
})
def register(request):
if request.method == "POST":
forms_obj = UserForm(request.POST)
if forms_obj.is_valid(): # 必须先进行is_valid,才能取
models.User.objects.create(**forms_obj.cleaned_data)
return HttpResponse("OK")
else:
# 前端干净的数据之所以保留,全在这里返回时,forms_obj对象在之前一次数据提交请求中,
# 校验后的干净数据,又给前端返回了,所以前端才保留了
return render(request, 'register.html', {"forms_obj": forms_obj})
# 需要在给页面的的时候,就传递一个forms_obj对象,方便生成相应的标签
forms_obj = UserForm()
return render(request, 'register.html', {"forms_obj": forms_obj})
register.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form组件</title>
<style>
.errorMsg{
color: red;
margin-left: 10px;
}
</style>
</head>
<body>
<form action="" method="post" novalidate>
{% csrf_token %}
<!-- 直接as_p 啥都有了 -->
{{ forms_obj.as_p }}
<p><input type="submit"></p>
</form>
</body>
</html>
进阶版 plus
这个版本就是前端代码部分手扣,用的较多,当有错误提示时,没有错误的字段内容还会保留,但需要后端配合,用的较多,记得form标签需要加novalidate。
views.py
:
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from app01 import models
class UserForm(forms.Form):
user = forms.CharField(
min_length=5, max_length=13,
error_messages={"required": "该字段不能为空"}
)
pwd = forms.CharField(error_messages={"required": "该字段不能为空"})
email = forms.EmailField(error_messages={
"required": "该字段不能为空",
"invalid": "邮箱格式错误"
})
def register(request):
if request.method == "POST":
forms_obj = UserForm(request.POST)
if forms_obj.is_valid(): # 必须先进行is_valid,才能取
models.User.objects.create(**forms_obj.cleaned_data)
return HttpResponse("OK")
else:
# 前端干净的数据之所以保留,全在这里返回时,forms_obj对象在之前一次数据提交请求中,
# 校验后的干净数据,又给前端返回了,所以前端才保留了
return render(request, 'register.html', {"forms_obj": forms_obj})
# 需要在给页面的的时候,就传递一个forms_obj对象,方便生成相应的标签
forms_obj = UserForm()
return render(request, 'register.html', {"forms_obj": forms_obj})
register.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form组件</title>
<style>
.errorMsg{
color: red;
margin-left: 10px;
}
</style>
</head>
<body>
<form action="" method="post" novalidate>
{% csrf_token %}
<!-- 这个版本呢,就是想渲染啥字段,就forms对象点啥字段 -->
<p>用户名 {{ forms_obj.user }}<span class="errorMsg">{{ forms_obj.errors.user.0 }}</span></p>
<p>密 码 {{ forms_obj.pwd }}<span class="errorMsg">{{ forms_obj.errors.pwd.0 }}</span></p>
<p>邮 箱 {{ forms_obj.email }}<span class="errorMsg">{{ forms_obj.errors.email.0 }}</span></p>
<p><input type="submit"></p>
</form>
</body>
</html>
进阶版 plus max
推荐这个版本,在这个版本中,后端加上了更多的内容,比如渲染标签时,添加一些属性值,让其具有相应的样式。
这里用到了booststrap,我引用的是在线的,省事儿。
views.py
:
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from django.forms import widgets
from app01 import models
class UserForm(forms.Form):
user = forms.CharField(
min_length=5, max_length=13,
error_messages={"required": "该字段不能为空"},
widget=widgets.TextInput(attrs={"class": "form-control"}),
label="用户名" # 如果不指定label属性,前端渲染时,label标签内容就是该字段的名字user
)
pwd = forms.CharField(
error_messages={"required": "该字段不能为空"},
widget=widgets.PasswordInput(attrs={"class": "form-control"}),
label="密码"
)
email = forms.EmailField(
error_messages={
"required": "该字段不能为空",
"invalid": "邮箱格式错误"
},
widget=widgets.EmailInput(attrs={"class": "form-control"}),
label="邮箱"
)
def register(request):
if request.method == "POST":
forms_obj = UserForm(request.POST)
if forms_obj.is_valid(): # 必须先进行is_valid,才能取
models.User.objects.create(**forms_obj.cleaned_data)
return HttpResponse("OK")
else:
return render(request, 'register.html', {"forms_obj": forms_obj})
forms_obj = UserForm()
return render(request, 'register.html', {"forms_obj": forms_obj})
register.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form组件</title>
<style>
.errorMsg {
color: red;
margin-left: 10px;
}
</style>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 col-lg-offset-2">
<form action="" method="post" novalidate>
{% csrf_token %}
{% for field in forms_obj %}
<div class="form-group">
<label for="">{{ field.label }}</label>
<!-- 渲染input标签,并在有错误提示时渲染该错误信息 -->
{{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
</div>
{% endfor %}
<p><input type="submit" class="btn btn-success pull-left"></p>
</form>
</div>
</div>
</div>
</body>
</html>
自定义规则:钩子方法
这部分就比较重要了,主要学习,在现有的校验基础上,比如最大长度、最短长度限制、不能输入为空这些基础上,根据需求实现一些具体的校验规则,我们通常将这些自定义的校验规则,称之为钩子方法。
钩子方法又可以范围:
- 局部钩子方法
- 全局钩子方法
加上基础校验的话,就有了三种,它们的执行流程是这样的:
- 基础校验,无论如何,都会执行,且是最开始执行,如果某个字段没有通过基础校验的话,该字段的自定义钩子方法也就不执行了。
- 自定义钩子方法,当该字段的基础校验通过后,才执行的自定义的钩子校验方法。
- 全局钩子校验,无论基础校验和和自定义钩子方法是否通过,最后都会走全局钩子方法。
先来看局部的钩子方法的用法。
局部钩子
局部钩子就是针对某个字段设定的自定义校验规则,钩子方法在form组件类中以方法的形式呈现,想要对哪个字段进行自定义钩子,就以clean_
开头,拼接上字段名就可以了,如clean_user
就是对user字段添加自定义钩子方法。
来两个示例,views.py
:
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from app01 import models
class UserForm(forms.Form):
user = forms.CharField(
min_length=5, max_length=13,
error_messages={"required": "该字段不能为空"},
widget=widgets.TextInput(attrs={"class": "form-control"}),
label="用户名" # 如果不指定label属性,前端渲染时,label标签内容就是该字段的名字
)
pwd = forms.CharField(
error_messages={"required": "该字段不能为空"},
widget=widgets.PasswordInput(attrs={"class": "form-control"}),
label="密码"
)
email = forms.EmailField(
error_messages={
"required": "该字段不能为空",
"invalid": "邮箱格式错误"
},
widget=widgets.EmailInput(attrs={"class": "form-control"}),
label="邮箱"
)
def clean_user(self):
""" 自定义关于user字段的钩子方法,作用:用户名不能重复 """
# 前端传过来的user字段的值
value = self.cleaned_data.get('user')
user_obj = models.User.objects.filter(user=value).first()
if user_obj: # 用户名已存在
# 固定写法
raise ValidationError("用户名已存在")
else:
# 如果用户名不存在,表示该字段值没问题,正常返回该值就行了
return value
def clean_pwd(self):
""" 自定义关于pwd字段的钩子方法,作用:密码不能是纯数字 """
value = self.cleaned_data.get('pwd')
if value.isdecimal(): # 如果用户输入是纯数字表示校验不通过
# 固定写法
raise ValidationError("密码不能是纯数字")
else:
return value
def register(request):
if request.method == "POST":
forms_obj = UserForm(request.POST)
if forms_obj.is_valid(): # 必须先进行is_valid,才能取
models.User.objects.create(**forms_obj.cleaned_data)
return HttpResponse("OK")
else:
return render(request, 'register.html', {"forms_obj": forms_obj})
forms_obj = UserForm()
return render(request, 'register.html', {"forms_obj": forms_obj})
其他代码不变。
使用局部钩子对密码一致性进行校验
在注册时,通常有密码和确认密码选项,但是数据库一般只有一个密码字段,所以,在开发中,要对用户输入的两个密码进行一致性校验。在校验时,注意点:
- form组件类中要重新生成一个确认密码的输入框字段,且该字段必须在密码字段下面,这是因为form组件中,校验是for循环每个字段校验的,在自定义钩子方法中,一般只能从cleaned_data中获取到当前字段和之前字段校验成功的数据,后面的字段数据由于还没循环到,导致没有添加到cleaned_data中,获取不到。
- 如果校验都通过了,写入到数据库之前,要把确认密码字段的数据删除掉,因为数据库中没有确认密码这个字段,不删除,可能会导致报错,因为多了个键值对,对应不上了!当然,这还要看你具体的orm语句是如何处理的。
上代码,models.py
、urls.py
都不变。
views.py
:
import os
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from app01 import models
class UserForm(forms.Form):
user = forms.CharField(
min_length=5, max_length=13,
error_messages={"required": "该字段不能为空"},
widget=widgets.TextInput(attrs={"class": "form-control"}),
label="用户名" # 如果不指定label属性,前端渲染时,label标签内容就是该字段的名字
)
pwd = forms.CharField(
min_length=3,
error_messages={"required": "该字段不能为空"},
widget=widgets.PasswordInput(attrs={"class": "form-control"}),
label="密码"
)
# 注意确认密码字段,要在密码字段下面
# 这是为了保证自定义确认密码的钩子中,能取到密码字段的值,既让密码字段先被校验成功
re_pwd = forms.CharField(
min_length=3,
error_messages={"required": "该字段不能为空"},
widget=widgets.PasswordInput(attrs={"class": "form-control"}),
label="确认密码"
)
email = forms.EmailField(
error_messages={
"required": "该字段不能为空",
"invalid": "邮箱格式错误"
},
widget=widgets.EmailInput(attrs={"class": "form-control"}),
label="邮箱"
)
def clean_user(self):
# 前端传过来的user字段的值
value = self.cleaned_data.get('user')
user_obj = models.User.objects.filter(user=value).first()
if user_obj: # 用户名已存在
# 固定写法
raise ValidationError("用户名已存在")
else:
# 如果用户名不存在,表示该字段值没问题,正常返回该值就行了
return value
def clean_pwd(self):
pwd = self.cleaned_data.get('pwd')
if pwd.isdecimal(): # 如果用户输入是纯数字表示校验不通过
# 固定写法
raise ValidationError("密码不能是纯数字")
else:
return pwd
def clean_re_pwd(self):
re_pwd = self.cleaned_data.get('re_pwd')
pwd = self.cleaned_data.get('pwd')
if pwd is None: # 表示密码字段没有输入值,这里直接放行,反正密码钩子校验也通不过,这里钩子不校验也无所谓了
return re_pwd
# 如果密码字段校验都没问题,这里直接判断两个值是否相等就行了
if re_pwd and pwd and re_pwd == pwd:
return re_pwd
else:
raise ValidationError("两次密码输入不一致")
# def clean_email(self):
# email = self.cleaned_data.get('email')
# if not email:
# raise ValidationError("邮箱不能为空")
# else:
# return email
def register(request):
if request.method == "POST":
forms_obj = UserForm(request.POST)
if forms_obj.is_valid(): # 必须先进行is_valid,才能取
print(forms_obj.cleaned_data)
# {'user': 'zhangkai123', 'pwd': '123a', 're_pwd': '123a', 'email': 'zhangkai123@qq.com'}
if forms_obj.cleaned_data.get('re_pwd'):
del forms_obj.cleaned_data['re_pwd']
print(forms_obj.cleaned_data)
# {'user': 'zhangkai123', 'pwd': '123a', 'email': 'zhangkai123@qq.com'}
models.User.objects.create(**forms_obj.cleaned_data)
return HttpResponse("OK")
else:
return render(request, 'register.html', {"forms_obj": forms_obj})
forms_obj = UserForm()
return render(request, 'register.html', {"forms_obj": forms_obj})
register.html
,代码不变。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form组件</title>
<style>
.errorMsg {
color: red;
margin-left: 10px;
}
</style>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 col-lg-offset-2">
<form action="" method="post" novalidate>
{% csrf_token %}
{% for field in forms_obj %}
<div class="form-group">
<label for="">{{ field.label }}</label>
<!-- 渲染input标签,并在有错误提示时渲染该错误信息 -->
{{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
</div>
{% endfor %}
<p><input type="submit" class="btn btn-success pull-left"></p>
</form>
</div>
</div>
</div>
</body>
</html>
全局钩子
根据form组件的源码执行流程,当基础钩子和局部钩子都通过后,这时的cleaned_data数据也基本上是合法且完整的了,可以在全局钩子做最后的校验规则的补充。
全局钩子的基本使用和注意事项
在form组件类中重写clean
方法,即是在定义全局钩子。
全局钩子的校验逻辑是,通过校验,返回cleaned_data,否则raise ValidationError错误,错误信息将会保存在forms对象的errors字典的__all__
这个特殊的key中,那么前端展示全局钩子的错误信息,也要手动处理。
使用起来还是需要前端配合的。
接下来的示例,就是通过全局钩子校验两次密码输入不一致。
上代码,models.py
、urls.py
都不变。
veiws.py
:
import os
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from app01 import models
class UserForm(forms.Form):
user = forms.CharField(
min_length=5, max_length=13,
error_messages={"required": "该字段不能为空"},
widget=widgets.TextInput(attrs={"class": "form-control"}),
label="用户名" # 如果不指定label属性,前端渲染时,label标签内容就是该字段的名字
)
pwd = forms.CharField(
min_length=3,
error_messages={"required": "该字段不能为空"},
widget=widgets.PasswordInput(attrs={"class": "form-control"}),
label="密码"
)
re_pwd = forms.CharField(
min_length=3,
error_messages={"required": "该字段不能为空"},
widget=widgets.PasswordInput(attrs={"class": "form-control"}),
label="确认密码"
)
email = forms.EmailField(
error_messages={
"required": "该字段不能为空",
"invalid": "邮箱格式错误"
},
widget=widgets.EmailInput(attrs={"class": "form-control"}),
label="邮箱"
)
def clean_user(self):
# 前端传过来的user字段的值
value = self.cleaned_data.get('user')
user_obj = models.User.objects.filter(user=value).first()
if user_obj: # 用户名已存在
# 固定写法
raise ValidationError("用户名已存在")
else:
# 如果用户名不存在,表示该字段值没问题,正常返回该值就行了
return value
def clean_pwd(self):
pwd = self.cleaned_data.get('pwd')
if pwd.isdecimal(): # 如果用户输入是纯数字表示校验不通过
# 固定写法
raise ValidationError("密码不能是纯数字")
else:
return pwd
# def clean_re_pwd(self):
# re_pwd = self.cleaned_data.get('re_pwd')
# pwd = self.cleaned_data.get('pwd')
# if pwd is None: # 表示密码字段没有输入值,这里直接放行,反正密码钩子校验也通不过,这里钩子不校验也无所谓了
# return re_pwd
# # 如果密码字段校验都没问题,这里直接判断两个值是否相等就行了
# if re_pwd and pwd and re_pwd == pwd:
# return re_pwd
# else:
# raise ValidationError("两次密码输入不一致")
def clean(self):
""" 全局钩子 """
re_pwd = self.cleaned_data.get('re_pwd')
pwd = self.cleaned_data.get('pwd')
# 如果这两个字段有一个输入为空,就跳过这个全局钩子,反正基础校验或者自定义钩子检验也通不过,这里跳过无所谓
if pwd is None or re_pwd is None:
return self.cleaned_data
# 如果密码字段校验都没问题,这里直接判断两个值是否相等就行了
if re_pwd and pwd and re_pwd == pwd:
return self.cleaned_data
else:
raise ValidationError("两次密码输入不一致")
def register(request):
if request.method == "POST":
forms_obj = UserForm(request.POST)
if forms_obj.is_valid(): # 必须先进行is_valid,才能取
print(forms_obj.cleaned_data)
# {'user': 'zhangkai123', 'pwd': '123a', 're_pwd': '123a', 'email': 'zhangkai123@qq.com'}
if forms_obj.cleaned_data.get('re_pwd'):
del forms_obj.cleaned_data['re_pwd']
print(forms_obj.cleaned_data)
# {'user': 'zhangkai123', 'pwd': '123a', 'email': 'zhangkai123@qq.com'}
models.User.objects.create(**forms_obj.cleaned_data)
return HttpResponse("OK")
else:
# 如果全局钩子中有错误信息产生,就单独取出来,交给前端单独处理
# 没有就返回None
all_error = forms_obj.errors.get('__all__')
if all_error:
all_error = forms_obj.errors.get('__all__')[0]
else:
# 没有错误信息,前端就不用显示了
all_error = ""
return render(request, 'register.html', {"forms_obj": forms_obj, "all_error": all_error})
forms_obj = UserForm()
return render(request, 'register.html', {"forms_obj": forms_obj})
register.html
也要单独做处理:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form组件</title>
<style>
.errorMsg {
color: red;
margin-left: 10px;
}
</style>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 col-lg-offset-2">
<form action="" method="post" novalidate>
{% csrf_token %}
{% for field in forms_obj %}
<div class="form-group">
<label for="">{{ field.label }}</label>
<!-- 渲染input标签,并在有错误提示时渲染该错误信息 -->
{{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
<!-- 如果后台使用的是全局钩子的话,要进行单独的判断,让两次密码输入不一致,显示在确认密码输入框下面 -->
{% if field.name == "re_pwd" %}
<span class="errorMsg">{{ all_error }}</span>
{% endif %}
</div>
{% endfor %}
<p><input type="submit" class="btn btn-success pull-left"></p>
</form>
</div>
</div>
</div>
</body>
</html>
重写__init__
方法
如果form组件类中,每个字段都要写required,添加属性值之类的,在每个字段中写是非常麻烦的,如下:
from django import forms
from django.forms import widgets
class UserForm(forms.Form):
user = forms.CharField(
min_length=5, max_length=13,
error_messages={"required": "该字段不能为空"},
widget=widgets.TextInput(attrs={"class": "form-control"}),
label="用户名" # 如果不指定label属性,前端渲染时,label标签内容就是该字段的名字
)
pwd = forms.CharField(
min_length=3,
error_messages={"required": "该字段不能为空"},
widget=widgets.PasswordInput(attrs={"class": "form-control"}),
label="密码"
)
re_pwd = forms.CharField(
min_length=3,
error_messages={"required": "该字段不能为空"},
widget=widgets.PasswordInput(attrs={"class": "form-control"}),
label="确认密码"
)
email = forms.EmailField(
error_messages={
"required": "该字段不能为空",
"invalid": "邮箱格式错误"
},
widget=widgets.EmailInput(attrs={"class": "form-control"}),
label="邮箱"
)
为了省事儿,有些相同的值可以在__init__
中写:
from django import forms
from django.forms import widgets
class UserForm(forms.Form):
user = forms.CharField(
min_length=5,
max_length=13,
widget=widgets.TextInput(),
label="用户名"
)
pwd = forms.CharField(
min_length=3,
widget=widgets.PasswordInput(),
label="密码"
)
re_pwd = forms.CharField(
min_length=3,
widget=widgets.PasswordInput(),
label="确认密码"
)
email = forms.EmailField(
error_messages={"invalid": "邮箱格式错误"},
widget=widgets.EmailInput(),
label="邮箱"
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有的字段添加相同的属性
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
forms处理下拉菜单
下拉菜单这里处理起来有多种情况:
- 单选下拉框:
- 值是固定的,例如用户表的性别字段,值是固定的,多选一,这种情况前端搞个单选下拉框即可。注意,我在偶然中发现,这种下拉框在form和modelform处理中,对应的模型类字段必须是IntegerField类型,不能是CharField类型,否则会报"选择一个有效的选项。 xxx 不在可用的选项中。"这种通不过校验的错误。
- 值是动态的,例如下拉框的值是外键字段关联的表中的数据,例如书籍表的出版社字段,它关联出版社表,前端展示书籍对应的出版社字段时,数据是从出版社表中动态获取的,所以这个单选下拉框跟上面的下拉框还不一样。
- 多选下拉框:
- 多选下拉框的值通常也来自外键字段,例如书籍的作者字段对应作者表,这种情况的多选下拉框跟上面的下拉框还不一样。
下面的示例,涵盖了上面的三种下拉框。
urls.py
:
from django.contrib import admin
from django.urls import path, re_path
from app01 import views
urlpatterns = [
path('admin/', admin.site.urls),
path('add_book/', views.add_book, name="add_book"),
path('add_author/', views.add_author, name="add_author"),
]
models.py
,自己手动添加几个出版社和作者,书籍和作者都可以从前端添加。
from django.db import models
class Publish(models.Model):
name = models.CharField(max_length=32, verbose_name='出版社')
# 前端下拉框中其实展示的是一个个的出版社对象,但之所以你能看到具体的出版社
# 全靠下面__str__方法,当你调试时,你可以先把下面的方法注释掉,然后你看前端
# 下拉框中的数据长啥样,你就知道我在说什么了
def __str__(self):
return self.name
class Book(models.Model):
""" 书籍表 """
title = models.CharField(max_length=32)
price = models.DecimalField(max_digits=8, decimal_places=2) # 999999.99
pub_date = models.DateTimeField() # "2012-12-12"
publish = models.ForeignKey(to="Publish", on_delete=models.CASCADE) # 级联删除
authors = models.ManyToManyField(to="Author")
def __str__(self):
return self.title
class Author(models.Model):
user = models.CharField(max_length=32, verbose_name='用户名')
gender = models.IntegerField(choices=(
(1, "男"),
(2, "女"),
), default=1, verbose_name='性别')
def __str__(self):
return self.user
views.py
:
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from django.forms import widgets
from django.core.exceptions import ValidationError
from app01.models import Publish, Author, Book
class AuthorForm(forms.Form):
user = forms.CharField(label="作者名称")
# 值是固定的单选下拉框
gender = forms.ChoiceField(choices=((1, "男"), (2, "女")), label="性别")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
class BookForm(forms.Form):
title = forms.CharField(label="书籍名称")
price = forms.DecimalField(max_digits=4, decimal_places=2, label="价格")
pub_date = forms.DateField(label='出版时间', widget=widgets.TextInput(attrs={"type": "date"}))
# 值是动态的单选下拉框
publish = forms.ModelChoiceField(queryset=Publish.objects.all(), label='出版社')
# 值是动态的多选下拉框
authors = forms.ModelMultipleChoiceField(queryset=Author.objects.all(), label='作者')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
def add_book(request):
""" 添加书籍 """
if request.method == "POST":
forms_obj = BookForm(request.POST)
if forms_obj.is_valid():
data = forms_obj.cleaned_data
# print(data)
# queryset对象
# print(list(data['authors']))
# print(data['publish'])
obj = Book.objects.create(
title=data['title'],
price=data['price'],
pub_date=data['pub_date'],
publish=data['publish']
)
obj.authors.add(*data['authors'])
return HttpResponse("OK")
else:
return render(request, 'register.html', {"forms_obj": forms_obj})
forms_obj = BookForm()
return render(request, 'add_author.html', {"forms_obj": forms_obj})
def add_author(request):
""" 添加作者 """
if request.method == "POST":
forms_obj = AuthorForm(request.POST)
if forms_obj.is_valid():
# print(forms_obj.cleaned_data)
Author.objects.create(**forms_obj.cleaned_data)
return HttpResponse("OK")
else:
return render(request, 'register.html', {"forms_obj": forms_obj})
forms_obj = AuthorForm()
return render(request, 'add_author.html', {"forms_obj": forms_obj})
add_author.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form组件</title>
<style>
.errorMsg {
color: red;
margin-left: 10px;
}
</style>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 col-lg-offset-2">
<form action="" method="post" novalidate>
{% csrf_token %}
{% for field in forms_obj %}
<div class="form-group">
<label for="">{{ field.label }}</label>
{{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
</div>
{% endfor %}
<p><input type="submit" class="btn btn-success pull-left"></p>
</form>
</div>
</div>
</div>
</body>
</html>
add_book.html
和add_author.html
代码一样:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form组件</title>
<style>
.errorMsg {
color: red;
margin-left: 10px;
}
</style>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 col-lg-offset-2">
<form action="" method="post" novalidate>
{% csrf_token %}
{% for field in forms_obj %}
<div class="form-group">
<label for="">{{ field.label }}</label>
{{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
</div>
{% endfor %}
<p><input type="submit" class="btn btn-success pull-left"></p>
</form>
</div>
</div>
</div>
</body>
</html>
forms组件实现下拉框保留默认值
例如在根据下拉框进行搜索时,当返回结果数据的页面中,仍然需要下拉框保留原来的搜索条件,也就是保留搜索的默认值时,就可以通过initial字典来完成这个需求。
来个示例,首先是models.py
和urls.py
:
# -------------- urls.py --------------
from django.contrib import admin
from django.urls import path
from api import views
urlpatterns = [
path('admin/', admin.site.urls),
path('users/', views.users),
]
# -------------- models.py --------------
from django.db import models
class UserInfo(models.Model):
user = models.CharField(max_length=32)
age = models.IntegerField()
gender = models.IntegerField(choices=((1, "男"), (2, "女"), (3, "保密")), default=3)
def __str__(self):
return self.user
接下来就是views.py
:
from django.shortcuts import render
from api.models import UserInfo
from django import forms
class UserForms(forms.Form):
gender = forms.ChoiceField(choices=((1, "男"), (2, "女"), (3, "保密")))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
def users(request):
if request.method == "GET":
form_obj = UserForms()
user_list = UserInfo.objects.all()
return render(request, 'users.html', {"user_list": user_list, "form_obj": form_obj})
# 根据前端传来的gender值,再返回给前端时,通过forms的initial字典作为默认值进行展示,这样前端就能达到保留下拉框搜索条件的目的了
# 这个套路同样也适用于modelform中
gender = request.POST.get("gender")
form_obj = UserForms(initial={"gender": gender})
user_list = UserInfo.objects.filter(gender=gender)
return render(request, 'users.html', {"user_list": user_list, "form_obj": form_obj})
最后的users.html
文件了:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户列表</title>
<link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap-theme.min.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
<script src="http://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<form action="" method="post">
{% csrf_token %}
<div class="col-lg-4 col-lg-offset-2">
{{ form_obj.gender }}
</div>
<div class="col-lg-3">
<input type="submit" class="btn btn-primary">
</div>
</form>
</div>
<div class="row">
<div class="col-lg-9 col-lg-offset-2">
<table class="table table-hover">
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>性别</th>
</tr>
</thead>
<tbody>
{% for user in user_list %}
<tr>
<td>{{ user.user }}</td>
<td>{{ user.age }}</td>
<td>{{ user.get_gender_display }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
实现效果:
forms类中使用request对象
例如实现某个功能时, 随机生成的图片验证码是存储在session中的, 我想在clean_code钩子函数中对验证码进行验证,而不是在视图函数中实现, 要达到这个目的clean_code钩子函数里就得有request对象....类似的的场景,我们重写__init__方法,把request传进去。
from django.shortcuts import render
from api.models import UserInfo
from django import forms
class UserForms(forms.Form):
gender = forms.ChoiceField(choices=((1, "男"), (2, "女"), (3, "保密")))
def __init__(self,request, *args, **kwargs):
# 重写__init__方法,把request对象传进来
self.request = request
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
def clean_gender(self):
# 在钩子方法中就能用了
gender = self.request.POST.get("gender")
return gender
def users(request):
if request.method == "GET":
form_obj = UserForms(request=request)
user_list = UserInfo.objects.all()
return render(request, 'users.html', {"user_list": user_list, "form_obj": form_obj})
gender = request.POST.get("gender")
form_obj = UserForms(data=request.POST, request=request )
form_obj.is_valid()
user_list = UserInfo.objects.filter(gender=gender)
return render(request, 'users.html', {"user_list": user_list, "form_obj": form_obj})
forms组件补充
django的内置字段如下:
Field
required=True, 是否允许为空
widget=None, HTML插件
label=None, 用于生成Label标签或显示内容
initial=None, 初始值
help_text='', 帮助信息(在标签旁边显示)
error_messages=None, 错误信息 {'required': '不能为空', 'invalid': '格式错误'}
show_hidden_initial=False, 是否在当前插件后面再加一个隐藏的且具有默认值的插件(可用于检验两次输入是否一直)
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的内置插件:
TextInput(Input)
NumberInput(TextInput)
EmailInput(TextInput)
URLInput(TextInput)
PasswordInput(TextInput)
HiddenInput(TextInput)
Textarea(Widget)
DateInput(DateTimeBaseInput)
DateTimeInput(DateTimeBaseInput)
TimeInput(DateTimeBaseInput)
CheckboxInput
Select
NullBooleanSelect
SelectMultiple
RadioSelect
CheckboxSelectMultiple
FileInput
ClearableFileInput
MultipleHiddenInput
SplitDateTimeWidget
SplitHiddenDateTimeWidget
SelectDateWidget
常用选择插件:
# 单radio,值为字符串
# user = fields.CharField(
# initial=2,
# widget=widgets.RadioSelect(choices=((1,'上海'),(2,'北京'),))
# )
# 单radio,值为字符串
# user = fields.ChoiceField(
# choices=((1, '上海'), (2, '北京'),),
# initial=2,
# widget=widgets.RadioSelect
# )
# 单select,值为字符串
# user = fields.CharField(
# initial=2,
# widget=widgets.Select(choices=((1,'上海'),(2,'北京'),))
# )
# 单select,值为字符串
# user = fields.ChoiceField(
# choices=((1, '上海'), (2, '北京'),),
# initial=2,
# widget=widgets.Select
# )
# 多选select,值为列表
# user = fields.MultipleChoiceField(
# choices=((1,'上海'),(2,'北京'),),
# initial=[1,],
# widget=widgets.SelectMultiple
# )
# 单checkbox
# user = fields.CharField(
# widget=widgets.CheckboxInput()
# )
# 多选checkbox,值为列表
# user = fields.MultipleChoiceField(
# initial=[2, ],
# choices=((1, '上海'), (2, '北京'),),
# widget=widgets.CheckboxSelectMultiple
# )
常见问题
AttributeError: 'XxxForm' object has no attribute 'cleaned_data'
django3.2 + python3.9 + win10
这个问题,非常典型,就是没有经过is_valid数据校验,就直接cleaned_data取值导致的报错,如下示例:
解决办法也就简单了,加上校验就行了:
ModelForm
基本用法
from django import forms
from django.core.exceptions import ValidationError
class UserModelForm(forms.ModelForm):
# 添加额外的字段
# 注意,在视图函数中,校验通过后,写入数据库之前,还要确认是否要删除这个额外的键值对
re_pwd = forms.CharField(
label='确认密码',
widget=forms.widgets.PasswordInput()
)
# 重写某个字段
pwd = forms.CharField(
label='密码',
widget=forms.widgets.PasswordInput(), # 这里是为了让页面输入是密文
)
# 元类
class Meta:
# 对应的模型类对象
model = models.User
# 展示模型类型中的所有字段
fields = "__all__"
# 或者这样写,只展示部分字段,和校验这部分字段
# fields = ['user', 'pwd', 're_pwd']
# 排除哪些字段不让其在页面中展示
# exclude = ['user']
# 插件,用法可以参考forms
widgets = {
# 为指定字段添加小组件,比如重写字段类型,添加placeholder等等属性或者自定义属性
"user": forms.widgets.TextInput(attrs={"type": "text"})
}
# 提示信息:labels,当然这个labels的优先级没有上面重写或者自定义字段中的的label优先级高
labels = {
"user": "叫啥",
"pwd": "输入你嘞密码"
}
# 自定义错误信息(整体错误信息from django.core.exceptions import NON_FIELD_ERRORS)
error_messages = {
"user": {"required": "输入不能为空!!!"}
}
# 帮助信息
help_texts = {
"user": "请输入中文"
}
# 注意这个__init__应该和元类平齐,不要写错缩进!!!!
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有的字段添加相同的属性
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
def clean_user(self):
""" 局部钩子 """
pass
def clean(self):
""" 全局钩子 """
pass
创建数据save()
:
def add_book(request):
if request.method == "POST":
#直接传request.POST,进行ModelForm的实例化传参
form_obj = forms.BookModelForm(request.POST)
if form_obj.is_valid(): # 校验数据
form_obj.save() #直接就可以保存数据到数据库,包括多对多,多对一,一对一的关系
return redirect("/book_list/")
#ModelForm实例化对象
form_obj = forms.BookModelForm()
return render(request, "v2/add_book.html", locals())
初始化数据instance=obj
:
def edit_book(request, pk):
book_obj = models.Book.objects.filter(id=pk).first()
#form_obj通过instance设置初始化的值,例如,图书管理系统中的编辑书籍功能,
#点击编辑后跳转到编辑书籍页面,跳转后需要用要编辑的书籍信息填充页面对应信息。
#不同于Form组件的是,ModelForm直接就可以传实例化的对象,而不需要将对象转化成字典的形式传。
form_obj = forms.BookModelForm(instance=book_obj)
return render(request, "v2/edit_book.html", locals())
更新 :ModelForm(request.POST, instance=book_obj)
def edit_book(request, pk):
book_obj = models.Book.objects.filter(id=pk).first()
if request.method == "POST":
#修改数据时,直接可以将用户数据包request.POST传进去,
#再传一个要修改的对象的示例,ModelForm就可以自动完成修改数据了。
form_obj = forms.BookModelForm(request.POST, instance=book_obj)
if form_obj.is_valid(): // 数据校验
form_obj.save() // 直接保存
return redirect("/book_list/")
#form_obj通过instance设置初始化的值,例如,图书管理系统中的编辑书籍功能,
#点击编辑后跳转到编辑书籍页面,跳转后需要用要编辑的书籍信息填充页面对应信息。
#不同于Form组件的是,ModelForm直接就可以传实例化的对象,而不需要将对象转化成字典的形式传。
form_obj = forms.BookModelForm(instance=book_obj)
return render(request, "v2/edit_book.html", locals())
参考或摘自:
- https://blog.csdn.net/weixin_45867017/article/details/116274035
- https://www.jianshu.com/p/43367afb5c5f
关于label
页面中展示的label内容,有4种展示方式,并且优先级不同:
- 模型类中字段没有添加
verbose_name
参数,且modelform类也没有指定labels属性,那么前端页面默认显示字段的首字母大写的字段名。这个优先级最低。 - 模型类中字段有
verbose_name
参数,前端默认显示verbose_name
参数值。优先级适中。 - modelform类中定义了labels属性,那么它的优先级高。
- 如果重写了某个字段,然后在字段的配置中的label属性优先级最高。
class Author(models.Model):
user = models.CharField(max_length=32, verbose_name='用户名')
class AuthorModelForm(forms.ModelForm):
# 例如重写user字段
user = forms.CharField(
label='叫啥', # 这里的label优先级最高
widget=forms.widgets.PasswordInput(), # 这里是为了让页面输入是密文
)
class Meta:
model = Author
fields = "__all__"
labels = {
"user": "第二优先级高的label"
}
empty_lable
通常对于外键字段,都会有个下拉框,没有设置默认值的话,就会默认显示empty_lable
值,也就是你看到的:
如果不想要这个empty_lable
,你可以这样:
import datetime
from django.shortcuts import render, HttpResponse
from django import forms
from api.models import Book, Publisher
class BookModelForm(forms.ModelForm):
class Meta:
model = Book
fields = '__all__'
widgets = {
# "pub_date": forms.widgets.DateInput(attrs={"type": 'date'})
}
# 这里的日期每次都要选择,比较麻烦,又不是咱们这里重点,我就给它排除了,前端不展示了
# 数据录入时单独处理
exclude = ['pub_date']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有的字段添加相同的属性
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
# 如果你不想看到那个提示的 '---------' 就给下面的值设置为None
self.fields['publish'].empty_label = None
def add(request):
form_obj = BookModelForm()
if request.method == 'GET':
return render(request, 'add.html', {"form_obj": form_obj})
else:
form_obj = BookModelForm(request.POST)
if form_obj.is_valid():
# 为页面中没有填值的字段,这里额外的添加上也可以
form_obj.instance.pub_date = datetime.datetime.now().strftime('%Y-%m-%d')
# 获取从别处传来的出版社id
pub_pk = 1
form_obj.instance.publish = Publisher.objects.filter(pk=pub_pk).first()
form_obj.save()
return HttpResponse("OK")
else:
return render(request, 'add.html', {"form_obj": form_obj})
效果:
让某些字段不可修改,即设置disable
import datetime
from django.shortcuts import render, HttpResponse
from django import forms
from api.models import Book, Publisher
class BookModelForm(forms.ModelForm):
class Meta:
model = Book
fields = '__all__'
widgets = {
# "pub_date": forms.widgets.DateInput(attrs={"type": 'date'})
# 法1,
'publish': forms.widgets.Select(attrs={"disabled": "disabled"})
}
# 这里的日期每次都要选择,比较麻烦,又不是咱们这里重点,我就给它排除了,前端不展示了
# 数据录入时单独处理
exclude = ['pub_date']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有的字段添加相同的属性
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
# 法2
self.fields['title'].widget.attrs.update({"disabled": "disabled"})
def add(request):
# 要结合initial设置默认值,不然页面中input框中值为空
pub_pk = 1
pub_obj = Publisher.objects.filter(pk=pub_pk)
form_obj = BookModelForm(initial={"title": "book1", 'publish': pub_obj.first()})
if request.method == 'GET':
return render(request, 'add.html', {"form_obj": form_obj})
else:
form_obj = BookModelForm(request.POST)
if form_obj.is_valid():
# 为页面中没有填值的字段,这里额外的添加上也可以
form_obj.instance.pub_date = datetime.datetime.now().strftime('%Y-%m-%d')
# 获取从别处传来的出版社id
pub_pk = 1
form_obj.instance.publish = Publisher.objects.filter(pk=pub_pk).first()
form_obj.save()
return HttpResponse("OK")
else:
return render(request, 'add.html', {"form_obj": form_obj})
效果:
关于help_text
禁用help_text:
from django import forms
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User
class AuthModelForm(forms.ModelForm):
re_password = forms.CharField(
label='确认密码',
widget=forms.widgets.PasswordInput()
)
class Meta:
model = User
fields = [
"username", 'password', 're_password', 'email',
'is_active', 'is_staff', 'is_superuser'
]
# 为指定字段添加help_text
# 注意,如果下面__init__中禁用了help_text,那么这里的配置不会生效
help_texts = {
'username': "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for filed in self.fields.values():
filed.widget.attrs.update({'class': 'form-control'})
# 禁用每个字段的help_text
filed.help_text = ''
def clean(self):
pwd = self.cleaned_data.get('password')
re_pwd = self.cleaned_data.get('re_password')
if pwd == re_pwd:
return self.cleaned_data
self.add_error('re_password', '两次密码不一致')
raise ValidationError('两次密码不一致')
initial
为页面中的input框设置初始值。
class AuthModelForm(forms.ModelForm):
re_password = forms.CharField(
label='确认密码',
widget=forms.widgets.PasswordInput()
)
password = forms.CharField(
label='密码',
widget=forms.widgets.PasswordInput(),
)
initial_test = forms.CharField(
label='初始值示例字段',
initial='初始值'
)
class Meta:
model = User
fields = [
"username", 'password', 're_password', 'email', 'phone', "initial_test",
'is_active', 'is_staff', 'is_superuser',
]
help_texts = {
'username': "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
}
# 视图函数中,直接这样写就完了,前端的input框就能展示初始的内容
form_obj = AuthModelForm()
但这样不太方便,因为modelform中通常不会大批量的单独定义字段,那样搞用form多好。
所以通常有另一种写法,定义modelfrom类不管初始值。
而是在视图函数中,去定义初始值:
form_obj = AuthModelForm(initial={"username": "zhangkai", 'email': 'zhangkai@qq.com', "phone": "18211101111"})
这样前端展示的效果也一样。
https://www.icode9.com/content-4-510590.html
modelform处理下拉菜单
django3.2 + python3.9
一般的ForeignKey的外键字段,modelform在前端默认渲染成这样:
如果有需求说,这个外键字段,我要指定让它显示某个出版社,或者固定显示某个出版社,亦或是不显示某个出版社,但保存时又要正常保存。这些需求如何实现。
首先把基本不变的代码列出来。
urls.py
:
from django.contrib import admin
from django.urls import path
from api import views
urlpatterns = [
path('admin/', admin.site.urls),
path('add/', views.add, name='add'),
]
models.py
:
from django.db import models
class Book(models.Model):
""" 书籍表 """
title = models.CharField(max_length=32, verbose_name='书籍名称')
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name='书籍价格') # 浮点型,9999.99
pub_date = models.DateField(verbose_name='出版日期')
publish = models.ForeignKey(
to='Publisher',
on_delete=models.CASCADE,
verbose_name='出版社'
) # Django2.x以上版本,必须手动指定on_delete
def __str__(self):
return self.title
class Publisher(models.Model):
""" 出版社表 """
name = models.CharField(max_length=32, verbose_name='出版社名称')
# email = models.EmailField(verbose_name='邮箱') # # 为了省事儿,这个字段就不创建了
address = models.CharField(max_length=32, verbose_name='出版社地址')
def __str__(self):
return self.name
add.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<style>
input{
margin-top: 10px;
}
ul{
padding: 0;
}
ul li{
color: red;
list-style: none;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 off-set-2">
<form action='' method="post" novalidate>
{% csrf_token %}
{{ form_obj }}
<input type="submit" class="btn btn-danger" />
</form>
</div>
</div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script></script>
</html>
上面的代码基本不变了,我们来看根据不同需求,需要修改的代码部分。
默认显示某个出版社
最简单的实现,那就是在给页面的时候,赋值上初始值,这里主要是views.py
:
import datetime
from django.shortcuts import render, HttpResponse
from django import forms
from api.models import Book, Publisher
class BookModelForm(forms.ModelForm):
class Meta:
model = Book
fields = '__all__'
# widgets = {
# "pub_date": forms.widgets.DateInput(attrs={"type": 'date'})
# }
# 这里的日期每次都要选择,比较麻烦,又不是咱们这里重点,我就给它排除了,前端不展示了
# 数据录入时单独处理
exclude = ['pub_date']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有的字段添加相同的属性
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
# 法1(同法3):为下拉菜单中只显示一个值,可以在这里做,但这这么做就写死了,不如在视图中根据条件过滤灵活
# 通常这么做可以筛选出,固定要在前端显示哪些字段
# self.fields['publish'].queryset = Publisher.objects.filter(address="上海")
# 如果你不想看到那个提示的 '---------' 就给下面的值设置为None
self.fields['publish'].empty_label = None
# 如果你不想让前端标签被选中,直接加disabled属性即可
self.fields['publish'].widget.attrs.update({"disabled": "disabled"})
def add(request):
# 例如我们这里接收到传来的书籍对应的出版社,也就是添加的这本书必须属于某个出版社,或者让前端默认显示某个出版社
pub_pk = 1
pub_obj = Publisher.objects.filter(pk=pub_pk)
# 法2,也很灵活
form_obj = BookModelForm(initial={'publish': pub_obj.first()})
# 法3,这个就是把上面init中的代码写到这里,可以根据条件进行过滤筛选,相对灵活
# 可以在这里根据传过来的条件进行筛选,注意一定是queryset,不能是单个模型类对象
# form_obj.fields['publish'].queryset = pub_obj
if request.method == 'GET':
return render(request, 'add.html', {"form_obj": form_obj})
else:
form_obj = BookModelForm(request.POST)
if form_obj.is_valid():
# 为页面中没有填值的字段,这里额外的添加上也可以
form_obj.instance.pub_date = datetime.datetime.now().strftime('%Y-%m-%d')
form_obj.save()
return HttpResponse("OK")
else:
return render(request, 'add.html', {"form_obj": form_obj})
页面效果:
这样的话,用户不选择别的出版社,保存时就是你指定的出版社了。
默认显示指定(多个)出版社
这个需求也是有的,比如外键字段很多,但是我在视图中要根据条件,展示某些记录,就可以这么做。
其它不变,views.py
中调整即可:
import datetime
from django.shortcuts import render, HttpResponse
from django import forms
from api.models import Book, Publisher
class BookModelForm(forms.ModelForm):
class Meta:
model = Book
fields = '__all__'
widgets = {
"pub_date": forms.widgets.DateInput(attrs={"type": 'date'})
}
# 这里的日期每次都要选择,比较麻烦,又不是咱们这里重点,我就给它排除了,前端不展示了
# 数据录入时单独处理
exclude = ['pub_date']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有的字段添加相同的属性
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
def add(request):
# 例如我们这里接收到传来的书籍对应的出版社名字,也就是添加的这本书必须属于某几个出版社,然后让前端某人显示某几个出版社
pub_address = '北京'
pub_obj = Publisher.objects.filter(address=pub_address)
form_obj = BookModelForm(initial={'publish': pub_obj})
if request.method == 'GET':
return render(request, 'add.html', {"form_obj": form_obj})
else:
form_obj = BookModelForm(request.POST)
if form_obj.is_valid():
# 为页面中没有填值的字段,这里额外的添加上也可以
form_obj.instance.pub_date = datetime.datetime.now().strftime('%Y-%m-%d')
form_obj.save()
return HttpResponse("OK")
else:
return render(request, 'add.html', {"form_obj": form_obj})
页面效果:
默认不显示某个出版社,在保存时处理
这个需求,参考处理日期字段的套路即可,就是页面压根不显示出版社信息,在后端保存书籍时,直接根据需求绑定对应的出版社。
其它不变,views.py
中调整即可:
import datetime
from django.shortcuts import render, HttpResponse
from django import forms
from api.models import Book, Publisher
class BookModelForm(forms.ModelForm):
class Meta:
model = Book
fields = '__all__'
# widgets = {
# "pub_date": forms.widgets.DateInput(attrs={"type": 'date'})
# }
# 这里的日期每次都要选择,比较麻烦,又不是咱们这里重点,我就给它排除了,前端不展示了
# 数据录入时单独处理
exclude = ['pub_date', 'publish']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有的字段添加相同的属性
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
def add(request):
form_obj = BookModelForm()
if request.method == 'GET':
return render(request, 'add.html', {"form_obj": form_obj})
else:
form_obj = BookModelForm(request.POST)
if form_obj.is_valid():
# 为页面中没有填值的字段,这里额外的添加上也可以
form_obj.instance.pub_date = datetime.datetime.now().strftime('%Y-%m-%d')
# 获取从别处传来的出版社id
pub_pk = 1
form_obj.instance.publish = Publisher.objects.filter(pk=pub_pk).first()
form_obj.save()
return HttpResponse("OK")
else:
return render(request, 'add.html', {"form_obj": form_obj})
页面效果:
参考:https://blog.csdn.net/slamx/article/details/51095066
modelform处理radio
django3.2
这里主要是为了解决modelform在处理radio时,还是会显示---------
的问题。
怎么解决呢?
我这里提供个示例。
models.py
:
class TestDemo(models.Model):
name = models.CharField(max_length=32, unique=True)
level = models.IntegerField(choices=((0, "普通会员"), (1, '黄金会员'), (2, "白金会员")), verbose_name='vip等级')
views.py
:
from django.shortcuts import render, redirect
from django import forms
from django.http import HttpResponse
from .models import TestDemo
class TestModelForm(forms.ModelForm):
level2 = forms.CharField(widget=forms.RadioSelect(choices=((0, "普通会员"), (1, '黄金会员'), (2, "白金会员"))))
class Meta:
model = TestDemo
fields = ['name', 'level', 'level2']
widgets = {
# 在这里重写level字段,页面中还是会渲染出来 ---------
# 解决方案有两种
# 1. 在models.py中,给level字段一个default值
# default的值在choices中的话,页面会有默认选中效果,如default=1,页面中默认选中黄金会员
# default=None 这样写,页面中既没有了---------,也不会默认选中某个radio了
# 2. 就是不在widgets中写level了,而是在类中直接重写level字段,模型类中也不用定义default值,参考level2字段示例
"level": forms.RadioSelect(
attrs={"class": "form-radio"},
choices=((0, "普通会员"), (1, '黄金会员'), (2, "白金会员")),
)
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有的字段添加相同的属性
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
# 下面这两个是控制页面标签的渲染效果
self.fields['level'].widget.attrs.update({"class": "form-radio"})
self.fields['level2'].widget.attrs.update({"class": "form-radio"})
def add(request):
form_obj = TestModelForm()
if request.method == 'GET':
return render(request, 'add.html', {"form_obj": form_obj})
else:
form_obj = TestModelForm(request.POST)
if form_obj.is_valid():
form_obj.cleaned_data.pop('level2')
form_obj.save()
return HttpResponse("OK")
else:
return render(request, 'add.html', {"form_obj": form_obj})
add.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<style>
input, ul, label{
margin-top: 10px;
}
ul{
padding: 0;
display: flex;
flex-direction: row;
}
ul li{
list-style: none;
padding: 5px;
}
.errorlist li{
color: red;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 off-set-2">
<form action='' method="post" novalidate>
{% csrf_token %}
{{ form_obj }}
<input type="submit" class="btn btn-danger" />
</form>
</div>
</div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script></script>
</html>
注意:django3.x版本,modelform会将radio标签渲染成ul套li的形式;而django4.x的modelform会将radio标签渲染成div套div的形式。
modelform对用户名进行校验
需求是,注册时,如果用户名已存在,就在前端提示下用户名已存在。
这个需求比较好解决,使用局部钩子就行了,对传过来的用户名做个校验。
重点在局部钩子这里:
from django.shortcuts import render, HttpResponse
from django import forms
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from api import models
class UserInfoModelForm(forms.ModelForm):
class Meta:
model = models.UserInfo
fields = "__all__"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有的字段添加相同的属性
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
def clean_user(self):
""" 自定义关于user字段的钩子方法,作用:用户名不能重复 """
# 前端传过来的user字段的值
value = self.cleaned_data.get('user')
user_obj = models.UserInfo.objects.filter(user=value).first()
if user_obj: # 用户名已存在
# 固定写法
raise ValidationError("用户名已存在")
else:
# 如果用户名不存在,表示该字段值没问题,正常返回该值就行了
return value
def register(request):
if request.method == "POST":
form_obj = UserInfoModelForm(request.POST)
if form_obj.is_valid(): # 必须先进行is_valid,才能取
form_obj.save()
return HttpResponse("OK")
else:
return render(request, 'register.html', {"form_obj": form_obj})
form_obj = UserInfoModelForm()
return render(request, 'register.html',{"form_obj": form_obj})
前端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-2">
<form action="" method="post" novalidate>
{% csrf_token %}
{{ form_obj }}
<input type="submit" value="提交" class="btn btn-primary" style="margin-top: 10px;">
</form>
</div>
</div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="http://libs.baidu.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
<script></script>
</html>
modelform确认密码字段处理
其实这里照抄forms部分即可,views.py
:
from django.shortcuts import render, HttpResponse
from api import models
from django import forms
from django.core.exceptions import ValidationError
class UserModelForm(forms.ModelForm):
re_pwd = forms.CharField(
label='确认密码',
widget=forms.widgets.PasswordInput()
)
pwd = forms.CharField(
label='密码',
widget=forms.widgets.PasswordInput(), # 这里是为了让页面输入是密文
)
class Meta:
model = models.User
fields = "__all__"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有的字段添加相同的属性
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"}) # 密码和确认密码不能输入为空的校验就是从这加的
# ******************* 法1,通过局部钩子对密码和确认密码字段进行校验 *******************
def clean_pwd(self):
""" 这里要对密码进行所有的校验,让下面确认密码字段只要校验两次输入不一致就行了 """
pwd = self.cleaned_data.get('pwd')
if pwd.isdecimal(): # 如果用户输入是纯数字表示校验不通过
# 固定写法
raise ValidationError("密码不能是纯数字")
else:
return pwd
def clean_re_pwd(self):
""" 如果确认密码不输入值,也就走不到局部钩子这里,在基础钩子就被拦截了 """
re_pwd = self.cleaned_data.get('re_pwd')
pwd = self.cleaned_data.get('pwd')
if pwd is None: # 表示密码字段没有输入值,这里直接放行,反正密码钩子校验也通不过,这里钩子不校验也无所谓了
return re_pwd
# 如果密码字段校验都没问题,这里直接判断两个值是否相等就行了
if re_pwd and pwd and re_pwd == pwd:
return re_pwd
else:
raise ValidationError("两次密码输入不一致")
# ******************* 法2,通过全局钩子对密码和确认密码字段进行校验 *******************
# def clean(self):
# """ 全局钩子 """
# re_pwd = self.cleaned_data.get('re_pwd')
# pwd = self.cleaned_data.get('pwd')
# # 如果这两个字段有一个输入为空,就跳过这个全局钩子,反正基础校验或者自定义钩子检验也通不过,这里跳过无所谓
# if pwd is None or re_pwd is None:
# return self.cleaned_data
# # 如果密码字段校验都没问题,这里直接判断两个值是否相等就行了
# if re_pwd and pwd and re_pwd == pwd:
# return self.cleaned_data
# else:
# raise ValidationError("两次密码输入不一致")
def register(request):
if request.method == "POST":
form_obj = UserModelForm(request.POST)
if form_obj.is_valid(): # 必须先进行is_valid,才能取
form_obj.save()
return HttpResponse("OK")
else:
return render(request, 'register.html', {"form_obj": form_obj})
form_obj = UserModelForm()
return render(request, 'register.html', {"form_obj": form_obj})
register.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
<style>
ul{
list-style: none;
padding-left: 0;
color:red;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<form action="" method="post" novalidate>
{% csrf_token %}
{{ form_obj }}
<input type="submit" value="提交" class="btn btn-primary" style="margin-top: 10px;">
</form>
</div>
</div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="http://libs.baidu.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
<script></script>
</html>
modelform处理日期时间字段
django3.2
modelform对于日期时间字段的处理,要用到一个叫做小组件东西,其实就是自定义一个类。
这里提供一个demo。
models.py
from django.db import models
class Test1(models.Model):
d1 = models.DateField(verbose_name='日期字段')
d2 = models.DateTimeField(verbose_name='日期时间')
urls.py
:
from django.urls import path, re_path
from . import views
urlpatterns = [
path('add1/', views.add1),
re_path('edit1/(?P<pk>\d+)/$', views.edit1),
]
views.py
,这个是重点,重点观察两个自定义类,和在modelfrom类中进行重写日期时间字段。
from django.shortcuts import render, redirect, HttpResponse
from django import forms
from . import models
class DateInput(forms.DateInput):
""" 自定义日期类 """
input_type = "date" # 日期 2022/9/2
class DateTimeInput(forms.DateTimeInput):
""" 自定义日期时间类 """
input_type = "datetime-local" # 日期时间 2022/9/2 12:12
class Test1ModelForm(forms.ModelForm):
# 重写日期时间字段,通过widget属性用上咱们定义的两个类
d1 = forms.DateField(
widget=DateInput(format='%Y-%m-%d'), # format中连接符,必须是-,不能是/, 否则编辑时前端渲染不出来值
label='日期'
)
d2 = forms.DateTimeField(
widget=DateTimeInput(format='%Y-%m-%d %H:%M:%S'),
label='日期时间'
)
class Meta:
model = models.Test1
fields = ['d1', 'd2']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有的字段添加相同的属性
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
def add1(request):
form_obj = Test1ModelForm()
if request.method == 'GET':
return render(request, 'add1.html', {"form_obj": form_obj})
else:
form_obj = Test1ModelForm(request.POST)
if form_obj.is_valid():
form_obj.save()
return HttpResponse("OK")
else:
return render(request, 'add1.html', {"form_obj": form_obj})
def edit1(request, pk):
obj = models.Test1.objects.filter(id=pk).first()
form_obj = Test1ModelForm(instance=obj)
if request.method == 'GET':
return render(request, 'add1.html', {"form_obj": form_obj})
else:
form_obj = Test1ModelForm(request.POST, instance=obj)
if form_obj.is_valid():
form_obj.save()
return HttpResponse("OK")
else:
return render(request, 'add1.html', {"form_obj": form_obj})
add1.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<style>
input, ul, label{
margin-top: 10px;
}
ul{
padding: 0;
display: flex;
flex-direction: row;
}
ul li{
list-style: none;
padding: 5px;
}
.errorlist li{
color: red;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 off-set-2">
<form action='' method="post" novalidate>
{% csrf_token %}
{{ form_obj }}
<input type="submit" class="btn btn-danger" />
</form>
</div>
</div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script></script>
</html>
效果:
添加和编辑页面没有秒,但是保存到数据库,秒这个单位就有默认值了。
参考:
- https://cloud.tencent.com/developer/ask/sof/978936
- https://blog.csdn.net/weixin_42202489/article/details/94564934
基于ModelForm的文件上传
有个学生问 modelform中怎么限制file字段上传文件的后缀?比如说一定要上传zip或者rar的压缩文件。
这真是个好问题,所以,我把简单的示例写写。
首先是media的相关配置,settings.py:
# ----------------- media ------------
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = "/media/"
然后是models.py,重点也在这里,在validators对应的列表中,进行文件类型的限制。
from django.db import models
from django.core import validators
class Asset(models.Model):
user = models.CharField(max_length=32, verbose_name='用户名')
avatar = models.ImageField(
upload_to='avatars/',
verbose_name='用户头像',
default='avatars/xxx.png',
validators=[validators.FileExtensionValidator(['jpg', 'png', 'jpeg'])]
)
file = models.FileField(
upload_to='files/',
verbose_name='用户文件',
default='files/xxx.txt',
validators=[validators.FileExtensionValidator(['pdf', 'zip', 'rar'])]
)
def __str__(self):
return self.user
然后就是modelform类和视图函数了,views.py:
from django.shortcuts import render, HttpResponse, redirect
from django.core.exceptions import ValidationError
from django import forms
from app01 import models
class FileModelForm(forms.ModelForm):
class Meta:
model = models.Asset
fields = ['user', 'avatar', 'file']
error_messages = {
"file": {"invalid_extension": "必须上传文件, 且上传文件的类型必须是 pdf zip rar"},
"avatar": {"invalid_extension": "必须上传图片,且上传文件的类型必须是 jpg png jpeg"},
}
labels = {
"file": "文件上传",
"avatar": "图片上传"
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为所有的字段添加相同的属性
for field in self.fields.values():
field.widget.attrs.update({"class": "form-control"})
field.error_messages.update({"required": "该字段不能为空"})
def add(request):
""" 添加用户信息 """
if request.method == "GET":
form_obj = FileModelForm()
return render(request, 'add_user.html', {"form_obj": form_obj})
else:
# 必须把request.POST, request.FILES传进去,否则文件上传失败
form_obj = FileModelForm(request.POST, request.FILES)
if form_obj.is_valid():
# print(11, request.POST)
# print(11, request.FILES)
# user = request.POST.get('user')
# avatar = request.FILES.get('avatar')
# print(222, avatar)
# file = request.FILES.get('file')
form_obj.save()
return HttpResponse('ok')
else:
return render(request, 'add_user.html', {"form_obj": form_obj})
路由就不用说了,urls.py:
from django.contrib import admin
from django.urls import path, re_path, include
from django.views.static import serve
from django.conf import settings
from app01 import views
urlpatterns = [
path('admin/', admin.site.urls),
re_path('media/(?P<path>.*)$', serve, {"document_root": settings.MEDIA_ROOT}),
path('add/', views.add),
]
html页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<style>
ul li{
color: red;
list-style: none;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<h3>文件上传</h3>
<form action="" method="post" enctype="multipart/form-data" novalidate>
{{ form_obj }}
<button type="submit" class="btn btn-danger">提交</button>
</form>
</div>
</div>
</div>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script></script>
</html>
参考:Django框架学习——14—(ModelForm、save方法、文件上传、限制上传的文件拓展名、cookie和session、上下文处理器)class Test1ModelForm(forms.ModelForm)
validators
Django内置的validators
模块内置了一些数据校验类和函数,再结合我们自己定义的校验类,可以很方便的在form和modelform中对各种各样的数据进行校验,比如校验URL、邮箱、IP地址等等,还有比较方便的正则校验类。
导入:
from django.core import validators
常见用法:
import re
from django.shortcuts import render, HttpResponse, redirect
from django import forms
from django.core import validators
from api import models
class RegexValidator(object):
def __init__(self, rex):
self.rex = str(rex)
def __call__(self, value):
match_obj = re.match(self.rex, value)
if not match_obj: # 校验失败
raise validators.ValidationError("自定义校验类:email不合法[{}]".format(value))
return value
class UserForm(forms.Form):
email1 = forms.EmailField(
label='邮箱1',
error_messages={
"required": "该字段不能为空",
"invalid": "邮箱格式错误1" # 这个错误提示是结合EmailField类来实现的,EmailField内部实现了邮箱格式的校验
})
email2 = forms.CharField(
label='邮箱2',
# 这种是普通的input框,但校验是通过自定义校验类来实现的,一看是个列表,表示可以有多个校验类
validators=[
RegexValidator(r"^\w+@\w+\.\w+$")
],
error_messages={
"required": "该字段不能为空"
})
email3 = forms.CharField(
label='邮箱3',
# 这种方式是通过validators.EmailValidator来实现的,我们只需要传递错误提示就行,内部有关于邮箱的校验
validators=[
# validators.EmailValidator(message='邮箱格式错误')
# 也可以用validators.RegexValidator这个正则的校验类来实现邮箱格式的校验
validators.RegexValidator(r"^\w+@\w+\.\w+$", message="邮箱格式错误3")
],
error_messages={
"required": "该字段不能为空"
})
mobile = forms.CharField(
label='手机号',
error_messages={
"required": "该字段不能为空",
},
# 而validators的列表中则可以写多个自定义校验规则,如果有多个自定义校验类,则必须所有的都通过才算通过
# validators.RegexValidator正则校验类相对用途比较广泛,只要能用正则校验的地方,都可以用这个校验类
validators=[
validators.RegexValidator(r"^((0\d{2,3}-\d{7,8})|(1[3584]\d{9}))$", message="手机号格式错误")
]
)
def register(request):
if request.method == "POST":
forms_obj = UserForm(request.POST)
if forms_obj.is_valid(): # 必须先进行is_valid,才能取
# 这里只介绍几种校验规则的用法,就不涉及写入数据库了
# models.User.objects.create(**forms_obj.cleaned_data)
return HttpResponse("OK")
else:
# 错误信息包含在forms_obj对象中,我们在前端通过该模板对象进行提取
return render(request, 'register.html', {"forms_obj": forms_obj})
forms_obj = UserForm()
return render(request, 'register.html', {"forms_obj": forms_obj})
对应的前端页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form组件</title>
<style>
.errorMsg {
color: red;
margin-left: 10px;
}
</style>
</head>
<body>
<form action="" method="post" novalidate>
{% csrf_token %}
{% for field in forms_obj %}
<div class="form-group">
<label for="">{{ field.label }}</label>
<!-- 渲染input标签,并在有错误提示时渲染该错误信息 -->
{{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
</div>
{% endfor %}
<p><input type="submit" class="btn btn-success pull-left"></p>
</form>
</body>
</html>
当然了validators
也可以对文件类型和图片类型进行校验,比如实现上传时,对上传的文件类型进行限制,这个示例在本篇博客的上面示例中有用到。
from django.db import models
from django.core import validators
class Asset(models.Model):
user = models.CharField(max_length=32, verbose_name='用户名')
avatar = models.ImageField(
upload_to='avatars/',
verbose_name='用户头像',
default='avatars/xxx.png',
validators=[validators.FileExtensionValidator(['jpg', 'png', 'jpeg'])]
)
file = models.FileField(
upload_to='files/',
verbose_name='用户文件',
default='files/xxx.txt',
validators=[validators.FileExtensionValidator(['pdf', 'zip', 'rar'])]
)
def __str__(self):
return self.user