- Forms组件
- 钩子函数
- forms组件字段参数
- forms组件字段类型
- 1. BooleanField
- 2. CharField
- 3. ChoiceField
- 4. TypedChoiceField
- 5. DateField
- 6. DateTimeField
- 7. DecimalField
- 8. DurationField
- 9. EmailField
- 10. FileField
- 11. FilePathField
- 12. FloatField
- 13. ImageField
- 14. IntegerField
- 15.JSONField
- 16. GenericIPAddressField
- 17. MultipleChoiceField
- 18. TypedMultipleChoiceField
- 19. NullBooleanField
- 20.RegexField
- 21. SlugField
- 22. TimeField
- 23. URLField
- 24. UUIDField
- 25. ComboField
- 26. MultiValueField
- 27. SplitDateTimeField
- forms组件源码分析
- ModelForm组件
Forms组件
基本使用
- 导入forms组件
- 定义一个类, 并继承Form
- 在类中书写要校验的字段, 字段的属性就是要校验的规则
- 实例化得到一个Form对象, 把要校验的数据传入
- 调用
[form对象].is_valid( )
方法进行校验, 校验通过返回True - 校验通过调用
[form对象].cleaned_data
获得校验后的数据 - 校验失败调用
[form对象].errors
获得错误信息
编写一个校验用户名和密码是否合法的功能
view.py
from django.shortcuts import render, HttpResponse
from django import forms
class MyForm(forms.Form):
# 用户名最少三个字符最多八个字符
user = forms.CharField(min_length=3, max_length=8, label='用户名',
error_messages={
'min_length': '用户名最短3位',
'max_length': '用户名最长8位',
'required': '用户名必填'
}
)
# 用户名最小不能小于0,最大不能超过150
age = forms.IntegerField(min_value=0, max_value=150, label='年龄',
error_messages={
'min_value': '年龄最小0岁',
'max_value': '年龄最大150岁',
'required': '年龄必填'
}
)
# 邮箱必须符合邮箱格式(关键符号@)
email = forms.EmailField(label='邮箱',
error_messages={
'invalid': '邮箱格式不正确',
'required': '邮箱必填'
}
)
def index(request):
# 1.先产生一个空对象
form_obj = MyForm()
if request.method == 'POST':
# 2.获取用户数据
form_obj = MyForm(request.POST)
# 3.校验用户数据
if form_obj.is_valid():
print(form_obj.cleaned_data)
return HttpResponse('数据没问题')
return render(request, 'index.html', locals())
test.py
(测试环境)
from app01 import views
# 1.将数据传入实例化对象
form_obj = views.MyForm({'user': 'kevin', 'age': 22, 'email': '123'})
# 2.查看数据是否合法(全部合法结果才是True)
print(form_obj.is_valid())
# 3.查看不符合条件的数据及原因
print(form_obj.errors)
# 4.查看符合条件的数据
print(form_obj.cleaned_data)
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="" method="post" novalidate>
{% for form in form_obj %}
<p>
{{ form.label }}
{{ form }}
<span style="color: red">{{ form.errors.0 }}</span>
</p>
{% endfor %}
<input type="submit">
</form>
</body>
</html>
补充
forms
类中所有的字段数据默认都是必填的,可以添加required=False
字段就不必填写forms
类中额外传入的字段数据不会做任何的校验forms
组件只负责渲染获取用户数据的标签,form
表单标签和提交按钮需要自己写- 渲染标签中文提示可以使用参数
label
指定,不指定默认英文提醒 forms
类中填写的校验性参数前端浏览器会识别并添加校验操作,但是前端的校验是可有可无的后端必须要再次校验form
表单可以取消浏览器自动添加校验功能的操作novalidate
钩子函数
在Form类中定义钩子函数,来实现自定义的验证功能,
局部钩子
在Fom类中定义 clean_字段名()
方法,就能够实现对特定字段进行校验。
views.py
from django.shortcuts import render, HttpResponse
from django import forms
class MyForm(forms.Form):
name = forms.CharField(min_length=3, max_length=8, label='用户名',
error_messages={
'min_length': '用户名至少三位',
'max_length': '用户名最大八位',
'required': '用户名不能为空',
}
)
# 局部钩子:效验用户名是否存在
"""钩子函数是数据经过了字段一层校验之后才会执行"""
def clean_name(self): # 自动生成的函数名,专门用于对name字段添加额外的校验规则
name_list = ['kevin', 'jerry', 'tom']
# 1.先获取用户名
name = self.cleaned_data.get('name')
# 2.判断用户名是否重复
if name in name_list:
# 3.提示信息
self.add_error('name', '用户已存在')
# 4.最后将勾上来的name返回回去
return name
def index(request):
form_obj = MyForm()
if request.method == 'POST':
form_obj = MyForm(request.POST)
if form_obj.is_valid():
return HttpResponse('OK')
return render(request, 'index.html', locals())
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="" method="post" novalidate>
{% for foo in form_obj %}
<p>{{ foo.label }}:{{ foo }}<span style="color: red">{{ foo.errors.0 }}</span></p>
{% endfor %}
<input type="submit">
</form>
</body>
</html>
全局钩子
在Fom类中定义 clean()
方法,就能够实现对字段进行全局校验
views.py
from django.shortcuts import render, HttpResponse
from django import forms
class MyForm(forms.Form):
password = forms.CharField(min_length=3, max_length=8, label='密码')
confirm_password = forms.CharField(min_length=3, max_length=8, label='确认密码')
# 全局钩子:校验密码与确认密码是否一致(一次性可以勾多个字段)
def clean(self):
# 1.获取多个字段数据
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
# 2.判断两次密码是否一致
if not password == confirm_password:
# 3.提示信息
self.add_error('confirm_password', '两次密码不一致')
# 4.最后将整个数据返回
return self.cleaned_data
def index(request):
form_obj = MyForm()
if request.method == 'POST':
form_obj = MyForm(request.POST)
if form_obj.is_valid():
return HttpResponse('OK')
return render(request, 'index.html', locals())
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="" method="post" novalidate>
{% for foo in form_obj %}
<p>{{ foo.label }}:{{ foo }}<span style="color: red">{{ foo.errors.0 }}</span></p>
{% endfor %}
<input type="submit">
</form>
</body>
</html>
forms组件字段参数
字段参数 | 说明 |
---|---|
min_length |
最小长度 |
max_length |
最大长度 |
label |
字段名称 |
error_messages |
错误提示 |
min_value |
最小值 |
max_value |
最大值 |
initial |
默认值 |
validators |
正则校验器 |
widget |
控制渲染出来的标签各项属性 |
help_text |
设置字段的描述文本 |
核心字段参数
required
给字段添加必填属性
initial
class MyForm(forms.Form):
username = forms.CharField(
min_length=8,
label="用户名",
initial="kevin" # 设置默认值
)
)
validators
指定一个列表,其中包含了为字段进行验证的函数
class MyForm(forms.Form):
phone = forms.CharField(
validators=[
RegexValidator(r'^[0-9]+$', '请输入数字'),
RegexValidator(r'^138[0-9]+$', '数字必须是138开头'),
]
)
widget
指定渲染Widget时使用的widget类,也就是这个form字段在HTML页面中是显示为什么框
class MyForm(forms.Form):
password = forms.CharField(
widget=forms.widgets.PasswordInput(
attrs={'class': 'form-control'}
)
)
disabled
设置有该属性的字段在前端页面中将显示为不可编辑状态
forms组件字段类型
1. BooleanField
- 默认的Widget:CheckboxInput
- 空值:False
- 规范化为:Python的True或者False
- 可用的错误信息键:required
2. CharField
- 默认的Widget:TextInput
- 空值:与empty_value给出的任何值。
- 规范化为:一个Unicode 对象。
- 验证
max_length
或min_length
,如果设置了这两个参数。 否则,所有的输入都是合法的。 - 可用的错误信息键:min_length, max_length, required
有四个可选参数:
- max_length,min_length:设置字符串的最大和最小长度。
- strip:如果True(默认),去除输入的前导和尾随空格。
- empty_value:用来表示“空”的值。 默认为空字符串。
3. ChoiceField
- 默认的Widget:Select
- 空值:’’(一个空字符串)
- 规范化为:一个Unicode 对象。
- 验证给定的值是否在选项列表中。
- 可用的错误信息键:required, invalid_choice
参数choices:用来作为该字段选项的一个二元组组成的可迭代对象(例如,列表或元组)或者一个可调用对象。格式与用于和ORM模型字段的choices参数相同。
4. TypedChoiceField
像ChoiceField一样,只是还有两个额外的参数:coerce和empty_value。
- 默认的Widget:Select
- 空值:empty_value参数设置的值。
- 规范化为:coerce参数类型的值。
- 验证给定的值在选项列表中存在并且可以被强制转换。
- 可用的错误信息的键:required, invalid_choice
5. DateField
- 默认的Widget:DateInput
- 空值:None
- 规范化为:datetime.date对象。
- 验证给出的值是一个datetime.date、datetime.datetime 或指定日期格式的字符串。
- 错误信息的键:required, invalid
接收一个可选的参数:input_formats。一个格式的列表,用于转换字符串为datetime.date对象。
如果没有提供input_formats,默认的输入格式为:
['%Y-%m-%d', # '2006-10-25'
'%m/%d/%Y', # '10/25/2006'
'%m/%d/%y'] # '10/25/06'
另外,如果你在设置中指定USE_L10N=False
,以下的格式也将包含在默认的输入格式中:
['%b %d %Y', # 'Oct 25 2006'
'%b %d, %Y', # 'Oct 25, 2006'
'%d %b %Y', # '25 Oct 2006'
'%d %b, %Y', # '25 Oct, 2006'
'%B %d %Y', # 'October 25 2006'
'%B %d, %Y', # 'October 25, 2006'
'%d %B %Y', # '25 October 2006'
'%d %B, %Y'] # '25 October, 2006'
6. DateTimeField
- 默认的Widget:DateTimeInput
- 空值:None
- 规范化为:Python的datetime.datetime对象。
- 验证给出的值是一个datetime.datetime、datetime.date或指定日期格式的字符串。
- 错误信息的键:required, invalid
接收一个可选的参数:input_formats
如果没有提供input_formats,默认的输入格式为:
['%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
'%Y-%m-%d', # '2006-10-25'
'%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59'
'%m/%d/%Y %H:%M', # '10/25/2006 14:30'
'%m/%d/%Y', # '10/25/2006'
'%m/%d/%y %H:%M:%S', # '10/25/06 14:30:59'
'%m/%d/%y %H:%M', # '10/25/06 14:30'
'%m/%d/%y'] # '10/25/06'
7. DecimalField
- 默认的Widget:当Field.localize是False时为NumberInput,否则为TextInput。
- 空值:None
- 规范化为:Python decimal对象。
- 验证给定的值为一个十进制数。 忽略前导和尾随的空白。
- 错误信息的键:
max_whole_digits
,max_digits
,max_decimal_places
,max_value
, invalid, required,min_value
接收四个可选的参数:
max_value,min_value:允许的值的范围,需要赋值decimal.Decimal对象,不能直接给个整数类型。
max_digits:值允许的最大位数(小数点之前和之后的数字总共的位数,前导的零将被删除)。
decimal_places:允许的最大小数位。
8. DurationField
- 默认的Widget:TextInput
- 空值:None
- 规范化为:Python timedelta。
- 验证给出的值是一个字符串,而且可以转换为timedelta对象。
- 错误信息的键:required, invalid.
9. EmailField
- 默认的Widget:EmailInput
- 空值:’’(一个空字符串)
- 规范化为:Unicode 对象。
- 使用正则表达式验证给出的值是一个合法的邮件地址。
- 错误信息的键:required, invalid
两个可选的参数用于验证,max_length 和min_length。
10. FileField
- 默认的Widget:ClearableFileInput
- 空值:None
- 规范化为:一个UploadedFile对象,它封装文件内容和文件名到一个对象内。
- 验证非空的文件数据已经绑定到表单。
- 错误信息的键:missing, invalid, required, empty, max_length
具有两个可选的参数用于验证:max_length 和 allow_empty_file。
11. FilePathField
- 默认的Widget:Select
- 空值:None
- 规范化为:Unicode 对象。
- 验证选择的选项在选项列表中存在。
- 错误信息的键:required, invalid_choice
这个字段允许从一个特定的目录选择文件。 它有五个额外的参数,其中的path是必须的:
path:要列出的目录的绝对路径。 这个目录必须存在。
recursive:如果为False(默认值),只用直接位于path下的文件或目录作为选项。如果为True,将递归访问这个目录,其内所有的子目录和文件都将作为选项。
match:正则表达模式;只有具有与此表达式匹配的文件名称才被允许作为选项。
allow_files
:可选。默认为True。表示是否应该包含指定位置的文件。它和allow_folders
必须有一个为True。
allow_folders
可选。默认为False。表示是否应该包含指定位置的目录。
12. FloatField
- 默认的Widget:当Field.localize是False时为NumberInput,否则为TextInput。
- 空值:None
- 规范化为:Float 对象。
- 验证给定的值是一个浮点数。
- 错误信息的键:max_value, invalid, required, min_value
接收两个可选的参数用于验证,max_value和min_value,控制允许的值的范围。
13. ImageField
- 默认的Widget:ClearableFileInput
- 空值:None
- 规范化为:一个UploadedFile 象,它封装文件内容和文件名为一个单独的对象。
- 验证文件数据已绑定到表单,并且该文件是Pillow可以解析的图像格式。
- 错误信息的键:missing, invalid, required, empty, invalid_image
使用ImageField需要安装Pillow(pip install pillow)。如果在上传图片时遇到图像损坏错误,通常意味着使用了Pillow不支持的格式。
>>> from PIL import Image
>>> from django import forms
>>> from django.core.files.uploadedfile import SimpleUploadedFile
>>> class ImageForm(forms.Form):
... img = forms.ImageField()
>>> file_data = {'img': SimpleUploadedFile('test.png', <file data>)}
>>> form = ImageForm({}, file_data)
# Pillow closes the underlying file descriptor.
>>> form.is_valid()
True
>>> image_field = form.cleaned_data['img']
>>> image_field.image
<PIL.PngImagePlugin.PngImageFile image mode=RGBA size=191x287 at 0x7F5985045C18>
>>> image_field.image.width
191
>>> image_field.image.height
287
>>> image_field.image.format
'PNG'
>>> image_field.image.getdata()
# Raises AttributeError: 'NoneType' object has no attribute 'seek'.
>>> image = Image.open(image_field)
>>> image.getdata()
<ImagingCore object at 0x7f5984f874b0>
14. IntegerField
- 默认的Widget:当Field.localize是False时为NumberInput,否则为TextInput。
- 空值:None
- 规范化为:Python 整数或长整数。
- 验证给定值是一个整数。 允许前导和尾随空格,类似Python的int()函数。
- 错误信息的键:max_value, invalid, required, min_value
两个可选参数:max_value和min_value,控制允许的值的范围。
15.JSONField
Django3.1新增。
接收JSON编码的字段。
- 默认的Widget:Textarea
- 空值:None
- 规范化为:一个JSON对象。
- 验证给定值是否合法的JSON。
- 错误信息的键:required, invalid
- 可接受两个参数:encoder和decoder,编码器和解码器
16. GenericIPAddressField
包含IPv4或IPv6地址的字段。
- 默认的Widget:TextInput
- 空值:’’(一个空字符串)
- 规范化为:一个Unicode对象。
- 验证给定值是有效的IP地址。
- 错误信息的键:required, invalid
有两个可选参数:protocol和unpack_ipv4
17. MultipleChoiceField
- 默认的Widget:SelectMultiple
- 空值:[](一个空列表)
- 规范化为:一个Unicode 对象列表。
- 验证给定值列表中的每个值都存在于选择列表中。
- 错误信息的键:invalid_list, invalid_choice, required
18. TypedMultipleChoiceField
类似MultipleChoiceField,除了需要两个额外的参数,coerce和empty_value。
- 默认的Widget:SelectMultiple
- 空值:empty_value
- 规范化为:coerce参数提供的类型值列表。
- 验证给定值存在于选项列表中并且可以强制。
- 错误信息的键:required, invalid_choice
19. NullBooleanField
- 默认的Widget:NullBooleanSelect
- 空值:None
- 规范化为:Python None, False 或True 值。
- 不验证任何内容(即,它从不引发ValidationError)。
20.RegexField
- 默认的Widget:TextInput
- 空值:’’(一个空字符串)
- 规范化为:一个Unicode 对象。
- 验证给定值与某个正则表达式匹配。
- 错误信息的键:required, invalid
需要一个必需的参数:regex,需要匹配的正则表达式。
还可以接收max_length,min_length和strip参数,类似CharField。
21. SlugField
- 默认的Widget:TextInput
- 空值:’’(一个空字符串)
- 规范化为:一个Unicode 对象。
- 验证给定的字符串只包括字母、数字、下划线及连字符。
- 错误信息的键:required, invalid
此字段用于在表单中表示模型的SlugField。
22. TimeField
- 默认的Widget:TextInput
- 空值:None
- 规范化为:一个Python 的datetime.time 对象。
- 验证给定值是datetime.time或以特定时间格式格式化的字符串。
- 错误信息的键:required, invalid
接收一个可选的参数:input_formats,用于尝试将字符串转换为有效的datetime.time对象的格式列表。
如果没有提供input_formats,默认的输入格式为:
'%H:%M:%S', # '14:30:59'
'%H:%M', # '14:30'
23. URLField
- 默认的Widget:URLInput
- 空值:’’(一个空字符串)
- 规范化为:一个Unicode 对象。
- 验证给定值是个有效的URL。
- 错误信息的键:required, invalid
可选参数:max_length和min_length
24. UUIDField
- 默认的Widget:TextInput
- 空值:’’(一个空字符串)
- 规范化为:UUID对象。
- 错误信息的键:required, invalid
25. ComboField
- 默认的Widget:TextInput
- 空值:’’(一个空字符串)
- 规范化为:Unicode 对象。
- 根据指定为ComboField的参数的每个字段验证给定值。
- 错误信息的键:required, invalid
接收一个额外的必选参数:fields,用于验证字段值的字段列表(按提供它们的顺序)。
>>> from django.forms import ComboField
>>> f = ComboField(fields=[CharField(max_length=20), EmailField()])
>>> f.clean('test@example.com')
'test@example.com'
>>> f.clean('longemailaddress@example.com')
Traceback (most recent call last):
...
ValidationError: ['Ensure this value has at most 20 characters (it has 28).']
26. MultiValueField
- 默认的Widget:TextInput
- 空值:’’(一个空字符串)
- 规范化为:子类的compress方法返回的类型。
- 根据指定为MultiValueField的参数的每个字段验证给定值。
- 错误信息的键:incomplete, invalid, required
27. SplitDateTimeField
- 默认的Widget:SplitDateTimeWidget
- 空值:None
- 规范化为:Python datetime.datetime 对象。
- 验证给定的值是datetime.datetime或以特定日期时间格式格式化的字符串。
- 错误信息的键:invalid_date, invalid, required, invalid_time
forms组件源码分析
为什么局部钩子要写成`clean_字段名?
从is_valid()
进行源码分析
def is_valid(self):
"""
Returns True if the form has no errors. Otherwise, False. If errors are
being ignored, returns False.
"""
return self.is_bound and not self.errors
self.is_bound
(可以点击看一下)是只要传了数据data就一定true
,着重点放在self.errors
点击查看源码
def errors(self):
"Returns an ErrorDict for the data provided for the form"
if self._errors is None:
self.full_clean()
return self._errors
点击_errors
查看默认是None
,所以肯定会走self.full_clean()
方法,查看源码
def full_clean(self):
"""
Cleans all of self.data and populates self._errors and
self.cleaned_data.
"""
self._errors = ErrorDict # 继承字典用来存放不符合的字段
if not self.is_bound: # 传入数据肯定不走
return
self.cleaned_data = {} # 符合的字段都放在这里
.
if self.empty_permitted and not self.has_changed(): # self.empty_permitted 默认false 不会走,不用考虑(可以点击查看)
return
self._clean_fields() # 局部钩子方法
self._clean_form() # 全局钩方法
self._post_clean() # 内部为pass
先查看self._clean_fields()
(局部钩子执行位置 )
def _clean_fields(self):
for name, field in self.fields.items(): # 循环获取字段名和字段对象
# fields是一个字典,forms组件的实例化就会自动创建一个fields
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进行校验
value = field.clean(value)
self.cleaned_data[name] = value # 把校验后的数据放到cleaned_data
if hasattr(self, 'clean_%s' % name): # 判断有没有局部钩子
value = getattr(self, 'clean_%s' % name)() # 执行局部钩子
self.cleaned_data[name] = value # 校验通过,把数据替换一下
except ValidationError as e: # 如果校验不通过,会抛异常,会被捕获
self.add_error(name, e) # 捕获后执行,添加到错误信息,errors是个列表,错误可能有多个
点击 add_error
查看关键信息
def add_error(self, field, error):
......
......
if field in self.cleaned_data: # 如果field字段对象在cleaned_data
del self.cleaned_data[field] # 那么就将其从中删除
查看源码发现校验数据的整个过程内部都有异常处理机制
把上面局部钩子不写self.add_error
,直接主动报错
# 主动报错
from django.core.exceptions import ValidationError
raise ValidationError('用户名已存在!!!!!!!!!')
class MyForm(forms.Form):
name = forms.CharField(min_length=3, max_length=8, label='用户名',
error_messages={
'min_length': '用户名至少三位',
'max_length': '用户名最大八位',
'required': '用户名不能为空',
}
)
# 局部钩子:效验用户名是否存在
"""钩子函数是数据经过了字段一层校验之后才会执行"""
def clean_name(self): # 自动生成的函数名,专门用于对name字段添加额外的校验规则
name_list = ['kevin', 'jerry', 'tom']
# 1.先获取用户名
name = self.cleaned_data.get('name')
# 2.判断用户名是否重复
if name in name_list:
# 3.提示信息
# self.add_error('name', '用户已存在')
# 主动报错
from django.core.exceptions import ValidationError
raise ValidationError('用户名已存在!!!!!!!!!')
# 4.最后将勾上来的name返回回去
return name
再看self._clean_form()
(全局钩子执行位置)
def _clean_form(self):
try:
cleaned_data = self.clean() # 执行全局钩子并拿到返回值(父类中有clea()方法,如果你自己写了则执行你写的)
except ValidationError as e:
self.add_error(None, e) # key作为None就是__all__
else:
if cleaned_data is not None: # 没出错则判断全局钩子的返回值是否为空
self.cleaned_data = cleaned_data # 不是空则原封不动的返回给elf.cleaned_data
所以在全局钩子可以看到最后要返回cleaned_data
,因为我们重写了这个方法
下面的_post_clean
是一些可以自己写的功能接口,重新具备一定的功能
ModelForm组件
ModelForm(基于forms组件)比form减少代码代码冗余
models.py
class User(models.Model):
name = models.CharField(max_length=32,)
password = models.CharField(max_length=32)
views.py
from django.shortcuts import render, HttpResponse
from django import forms
from app01 import models
class MyUser(forms.ModelForm):
class Meta:
model = models.User # 指定关联的表
fields = '__all__' # 所有的字段全部生成对应的forms字段
# exclude = ('name',) # 排除的字段
labels = {
'name': '用户名',
'password': '密码',
}
widgets = {
'name': forms.widgets.TextInput(attrs={'class': 'form-control'}),
'password': forms.widgets.PasswordInput(attrs={'class': 'form-control'}),
}
def index(request):
form_obj = MyUser()
if request.method == 'POST':
form_obj = MyUser(request.POST)
if form_obj.is_valid():
edit_obj = models.User.objects.filter(pk=1).first()
form_obj = MyUser(request.POST, instance=edit_obj) # 新增还是保存,取决于instance 参数,没有则新增
form_obj.save() # 编辑并保存
return render(request, 'index.html', locals())
index.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.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
<form action="" method="post" novalidate style="max-width: 300px;margin: 10px auto" >
{% for foo in form_obj %}
<p>{{ foo.label }}:{{ foo }}<span style="color: red">{{ foo.errors.0 }}</span></p>
{% endfor %}
<input type="submit">
</form>
</body>
</html>