
8. 表单与模型
表单是搜集用户数据信息的各种表单元素的集合, 其作用是实现网页上的数据交互,
比如用户在网站输入数据信息, 然后提交到网站服务器端进行处理(如数据录入和用户登录注册等).
网页表单是Web开发的一项基本功能, Django的表单功能由Form类实现,
主要分为两种: django.forms.Form和django.forms.ModelForm.
前者是一个基础的表单功能, 后者是在前者的基础上结合模型所生成的数据表单.
8.1 初识表单
传统的表单生成方式是在模板文件中编写HTML代码实现, 在HTML语言中, 表单由<form>标签实现.
表单生成方式如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标题</title>
</head>
<body>
<form action="" method="post">
<label for="name">
姓名: <input type="text" id="name" name="name" value="kid">
</label>
<br>
<br>
<label for="age">
年龄: <input type="text" id="age" name="age" value="18">
</label>
<br>
<br>
<input type="submit" value="提交">
</form>
</body>
</html>
一个完整的表单主要由4部分组成: 提交地址, 请求方式, 元素控件和提交按钮, 分别说明如下:
● 提交地址(form标签的action属性)用于设置用户提交的表单数据应由哪个路由接收和处理.
当用户向服务器提交数据时, 若属性action为空, 则提交的数据应由当前的路由来接收和处理,
否则网页会跳转到属性action所指向的路由地址.
● 请求方式用于设置表单的提交方式, 通常是GET请求或POST请求, 由form标签的属性method决定.
● 元素控件是供用户输入数据信息的输入框, 由HTML的<input>控件实现,
控件属性type用于设置输入框的类型, 常用的输入框类型有文本框, 下拉框和复选框等.
● 提交按钮供用户提交数据到服务器, 该按钮也是由HTML的<input>控件实现的.
但该按钮具有一定的特殊性, 因此不归纳到元素控件的范围内.
在模板文件中, 通过HTML语言编写表单是一种较为简单的实现方式, 如果表单元素较多或一个网页里使用多个表单,
就会在无形之中增加模板的代码量, 对日后的维护和更新造成极大的不便.
为了简化表单的实现过程和提高表单的灵活性, Django提供了完善的表单功能.
在5.2.1小节已简单演示过表单的定义过程, 并且将表单交由视图类FormView使用, 从而在浏览器上生成网页表单.
为了深入了解表单的定义过程和使用方式, 以MyDjango为例,
在index文件夹中创建form.py文件, 然后在index的models.py中定义分别模型PersonInfo和Vocation, 模型定义的代码如下:
from django.db import models
class PersonInfo(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=20)
age = models.IntegerField()
def __str__(self):
return str(self.name)
class Meta:
verbose_name = '人员信息'
class Vocation(models.Model):
id = models.AutoField(primary_key=True)
job = models.CharField(max_length=20)
title = models.CharField(max_length=20)
salary = models.DecimalField(max_digits=10, decimal_places=2)
personinfo = models.ForeignKey(PersonInfo, on_delete=models.CASCADE)
def __str__(self):
return str(self.id)
class Meta:
verbose_name = '职业信息'
python manage.py makemigrations
python manage.py migrate

分别对模型PersonInfo和Vocation执行数据迁移, 在项目的db.sqlite3文件里生成相应的数据表,
然后在数据表index_vocation和index_personinfo里分别新建数据信息, 如图8-1所示.
-- 为职业表插入数据
INSERT INTO
index_personinfo(id, name, age)
VALUES
(1, '张三', 26),
(2, '李四', 23),
(3, '王五', 25),
(4, '赵六', 28);
-- 为员工表插入数据
INSERT INTO
index_vocation(id, job, title, salary, personinfo_id)
VALUES
(1, '软件开发', 'Python开发', 10000, 2),
(2, '软件测试', '自动化测试', 8000, 3),
(3, '需求分析', '需求分析', 6000, 1),
(4, '项目关联', '项目经理', 12000, 4);

图8-1 数据表index_vocation和index_personinfo
项目数据创建成功后, 我们在form.py中定义表单类VocationForm,
然后在index的urls.py, views.py和模板文件index.html中使用表单类VocationForm, 在浏览器上生成网页表单, 实现代码如下:
from django import forms
from .models import *
class VocationForm(forms.Form):
job = forms.CharField(max_length=20, label='职位')
title = forms.CharField(max_length=20, label='职称')
salary = forms.DecimalField(label='薪资')
value = PersonInfo.objects.values('name')
choices = [(i + 1, v['name']) for i, v in enumerate(value)]
personinfo = forms.ChoiceField(choices=choices, label='姓名')

from django.urls import path
from .views import *
urlpatterns = [
path('', index, name='index')
]

from django.shortcuts import render
from .form import *
def index(request):
v = VocationForm()
return render(request, 'index.html', locals())

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标题</title>
</head>
<body>
{# 校验的字段如果不通过会将错误信息存放在errors属性中 #}
{% if v.errors %}
<p>数据错误: {{ v.errors }}</p>
{% else %}
<form action="" method="post">
{% csrf_token %}
<table>
{# 生成table表单 #}
{{ v.as_table }}
</table>
<input type="submit" value="提交">
</form>
{% endif %}
</body>
</html>

上述代码演示表单Form的使用过程, 整个过程包含表单类VocationForm的定义(index的form.py),
实例化表单类(index的views.py)和使用表单对象(模板文件index.html), 说明如下:
(1) 在form.py中定义表单VocationForm, 表单以类的形式表示.
在表单中定义不同类型的类属性, 这些属性在表单中称为表单字段, 每个表单字段代表HTML的一个表单控件, 这是表单的基本组成单位.
(2) 在views.py中导入form.py定义的表单类VocationForm,
视图函数index对VocationForm实例化并生成表单对象v, 再将表单对象v传递给模板文件index.html.
(3) 表单对象v在模板文件index.html里使用errors判断表单对象是否存在异常信息,
若存在, 则将异常信息输出, 否则使用as_table将表单对象v以HTML的<table>标签生成网页表单.
运行MyDjango项目, 在浏览器上访问: 127.0.0.1:8000 即可看到网页表单, 如图8-2所示.

图8-2 网页表单
综上所述, Django的表单功能是通过定义表单类, 再由类的实例化生成HTML的表单元素控件, 这样可以在模板文件中减少HTML的硬编码.
每个HTML的表单元素控件由表单字段来决定, 下面是使用表单后生成的源代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标题</title>
</head>
<body>
<form action="" method="post">
<input type="hidden" name="csrfmiddlewaretoken"
value="VpIsWJc5Nsx9Lsq4E4mrqn6LRJiBmCQQCC9f0JWFU5c21Zz6WZhMmsySvSaPm78C">
<table>
<tr>
<th><label for="id_job">职位:</label></th>
<td><input type="text" name="job" maxlength="20" required id="id_job"></td>
</tr>
<tr>
<th><label for="id_title">职称:</label></th>
<td><input type="text" name="title" class="c1" maxlength="20" required id="id_title"></td>
</tr>
<tr>
<th><label for="id_salary">薪资:</label></th>
<td><input type="number" name="salary" step="any" required id="id_salary"></td>
</tr>
<tr>
<th><label for="id_personinfo">姓名:</label></th>
<td><select name="personinfo" id="id_personinfo">
<option value="1">张三</option>
<option value="2">李四</option>
<option value="3">王五</option>
<option value="4">赵六</option>
</select></td>
</tr>
<input type="submit" value="提交">
</table>
</form>
</body>
</html>

通过表单字段和HTML元素控件对比可以发现(以job字段为列):
● 字段job的参数label将转换成HTML的标签<label>.
● 字段job的forms.CharField类型转换成HTML的<inputtype="text">控件.
标签<input>是一个输入框控件; 参数type设置输入框的数据类型, 如type="text"代表当前输入框为文本类型.
● 字段job的命名将转换成<input>控件的参数name; 表单字段的参数max_length将转换成<input>控件的参数maxlength.
8.2 源码分析Form
从8.1节发现, 表单类的定义过程与模型有相似之处, 只不过两者所继承的类有所不同,
也就是说, Django内置的表单功能也是使用自定义元类实现定义过程.
在PyCharm里打开表单类Form的源码文件, 其定义过程如图8-3所示.

图8-3 表单类Form的源码文件
表单类Form继承BaseForm和元类DeclarativeFieldsMetaclass, 而该元类又继承另一个元类, 因此表单类Form的继承关系如图8-4所示.

图8-4 表单类Form的继承关系
从图8-4得知, 元类没有定义太多的属性和方法, 大部分的属性和方法都是由父类BaseForm定义的,
8.1节的模板文件index.html所使用的errors和as_table方法也是来自父类BaseForm.
我们分析父类BaseForm的定义过程, 列举开发中常用的属性和方法.
● data: 默认值为None, 以字典形式表示, 字典的键为表单字段, 代表将数据绑定到对应的表单字段.
● auto_id: 默认值为id_%s, 以字符串格式化表示, 若设置HTML元素控件的id属性,
比如表单字段job, 则元素控件id属性为id_job, %s代表表单字段名称.
● prefix: 默认值为None, 以字符串表示, 设置表单的控件属性name和id的属性值,
如果一个网页里使用多个相同的表单, 那么设置该属性可以区分每个表单.
● initial: 默认值为None, 以字典形式表示, 在表单的实例化过程中设置初始化值.
● label_suffix: 若参数值为None, 则默认为冒号,
以表单字段job为例, 其HTML控件含有label标签(<label for="id_job">职位:</label>),
其中label标签里的冒号由参数label_suffix设置.
● field_order: 默认值为None, 则以表单字段定义的先后顺序进行排列,
若要自定义排序, 则将每个表单字段按先后顺序放置在列表里, 并把列表作为该参数的值.
● use_required_attribute: 默认值为None(或为True),
为表单字段所对应的HTML控件设置required属性, 该控件为必填项, 数据不能为空, 若设为False, 则HTML控件为可填项.
● errors(): 验证表单的数据是否存在异常, 若存在异常, 则获取异常信息, 异常信息可设为字典或JSON格式.
● is_valid(): 验证表单数据是否存在异常, 若存在, 则返回False, 否则返回True.
● as_table(): 将表单字段以HTML的<table>标签生成网页表单.
● as_ul(): 将表单字段以HTML的<ul>标签生成网页表单.
● as_p(): 将表单字段以HTML的<p>标签生成网页表单.
● has_changed(): 对比用于提交的表单数据与表单初始化数据是否发生变化.
了解表单类Form的定义过程后, 接下来讲述表单的字段类型.
表单字段与模型字段有相似之处, 不同类型的表单字段对应不同的HTML控件, 在PyCharm中打开表单字段的源码文件, 如图8-5所示.

图8-5 表单字段
从源码文件fields.py中找到26个不同类型的表单字段, 每个字段的说明如下:
● CharField: 文本框, 参数max_length和min_length分别设置文本长度.
● IntegerField: 数值框, 参数max_value设置最大值, min_value设置最小值.
● FloatField: 数值框, 继承IntegerField, 验证数据是否为浮点数.
● DecimalField: 数值框, 继承IntegerField, 验证数值是否设有小数点,
参数max_digits设置最大位数, 参数decimal_places设置小数点最大位数.
● DateField: 文本框, 继承BaseTemporalField, 具有验证日期格式的功能, 参数input_formats设置日期格式.
● TimeField: 文本框, 继承BaseTemporalField, 验证数据是否为datetime.time或特定时间格式的字符串.
● DateTimeField: 文本框, 继承BaseTemporalField,
验证数据是否为datetime.datetime, datetime.date或特定日期时间格式的字符串.
● DurationField: 文本框, 验证数据是否为一个有效的时间段.
● RegexField: 文本框, 继承CharField, 验证数据是否与某个正则表达式匹配, 参数regex设置正则表达式.
● EmailField: 文本框, 继承CharField, 验证数据是否为合法的邮箱地址.
● FileField: 文件上传框, 参数max_length设置上传文件名的最大长度, 参数allow_empty_file设置是否允许文件内容为空.
● ImageField: 文件上传控件, 继承FileField, 验证文件是否为Pillow库可识别的图像格式.
● FilePathField: 文件选择控件, 在特定的目录选择文件, 参数path是必需参数, 参数值为目录的绝对路径;
参数recursive, match, allow_files和allow_folders为可选参数.
● URLField: 文本框, 继承CharField, 验证数据是否为有效的路由地址.
● BooleanField: 复选框, 设有选项True和False, 如果字段带有required=True, 复选框就默认为True.
● NullBooleanField: 复选框, 继承BooleanField, 设有3个选项, 分别为None, True和False.
● ChoiceField: 下拉框, 参数choices以元组形式表示, 用于设置下拉框的选项列表.
● TypedChoiceField: 下拉框, 继承ChoiceField, 参数coerce代表强制转换数据类型, 参数empty_value表示空值, 默认为空字符串.
● MultipleChoiceField: 下拉框, 继承ChoiceField, 验证数据是否在下拉框的选项列表.
● TypedMultipleChoiceField: 下拉框, 继承MultipleChoiceField, 验证数据是否在下拉框的选项列表, 并且可强制转换数据类型,
参数coerce代表强制转换数据类型, 参数empty_value表示空值, 默认为空字符串.
● ComboField: 文本框, 为表单字段设置验证功能, 比如字段类型为CharField, 为该字段添加EmailField, 使字段具有邮箱验证功能.
● MultiValueField: 文本框, 将多个表单字段合并成一个新的字段.
● SplitDateTimeField: 文本框, 继承MultiValueField, 验证数据是否为datetime.datetime或特定日期时间格式的字符串.
● GenericIPAddressField: 文本框, 继承CharField, 验证数据是否为有效的IP地址.
● SlugField: 文本框, 继承CharField, 验证数据是否只包括字母, 数字, 下画线及连字符.
● UUIDField: 文本框, 继承CharField, 验证数据是否为UUID格式.
表单字段除了转换HTML控件之外, 还具有一定的数据格式规范, 比如EmailField字段, 它设有邮箱地址验证功能.
不同类型的表单字段设有一些特殊参数, 但每个表单字段都继承父类Field, 因此它们具有以下的共同参数.
● required: 输入的数据是否为空, 默认值为True.
● widget: 设置HTML控件的样式.
● label: 用于生成label标签的网页内容.
● initial: 设置表单字段的初始值.
● help_text: 设置帮助提示信息.
● error_messages: 设置错误信息, 以字典形式表示, 比如{'required': '不能为空', 'invalid': '格式错误'}.
● show_hidden_initial: 参数值为True/False,
是否在当前控件后面再加一个隐藏的且具有默认值的控件(可用于检验两次输入的值是否一致).
● validators: 自定义数据验证规则. 以列表格式表示, 列表元素为函数名.
● localize: 参数值为True/False, 设置本地化, 不同时区自动显示当地时间.
● disabled: 参数值为True/False, HTML控件是否可以编辑.
● label_suffix: 设置label的后缀内容.
综上所述, 表单的定义过程, 表单的字段类型和表单字段的参数类型是表单的核心功能.
以8.1节的MyDjango为例, 优化form.py定义的VocationForm, 代码如下:
from django import forms
from .models import *
from django.core.exceptions import ValidationError
def salary_validate(value):
if value > 30000:
raise ValidationError('请输入合适的薪资!')
class VocationForm(forms.Form):
job = forms.CharField(max_length=20, label='职位')
title = forms.CharField(max_length=20, label='职称',
widget=forms.widgets.TextInput(attrs={'class': 'c1'}),
error_messages={'required': '职位不能为空!'})
salary = forms.DecimalField(label='薪资', validators=[salary_validate])
value = PersonInfo.objects.values('name')
choices = [(i + 1, v['name']) for i, v in enumerate(value)]
personinfo = forms.ChoiceField(choices=choices, label='姓名')
def clean_title(self):
data = self.cleaned_data['title']
return '初级' + data

优化的代码分别使用了字段参数widget, error_messages, validators以及自定义表单字段title的数据清洗函数clean_title(),
说明如下:
● 参数widget是一个forms.widgets对象, 其作用是设置表单字段的CSS样式, 参数的对象类型必须与表单字段类型相符合.
例如表单字段为CharField, 参数widget的类型应为forms.widgets.TextInput,
两者的含义与作用是一致的, 都是文本输入框, 若表单字段改为ChoiceField, 而参数widget的类型不变,
前者是下拉选择框, 后者是文本输入框, 则在网页上会优先显示为文本输入框.
● 参数error_messages设置数据验证失败后的错误信息, 参数值以字典的形式表示, 字典的键为表单字段的参数名称, 字典的值为错误信息.
● 参数validators是自定义的数据验证函数, 当用户提交表单数据后, 首先在视图里执行自定义的验证函数,
当数据验证失败后, 会抛出自定义的异常信息.
因此, 如果字段中设置了参数validators, 就无须设置参数error_messages, 因为数据验证已由参数validators优先处理.
● 自定义表单字段title的数据清洗函数clean_title()只适用于表单字段title的数据清洗,
函数名的格式必须为clean_表单字段名称(), 而且函数必须有return返回值,
如果在函数中设置主动抛出异常ValidationError, 那么该函数可视为带有数据验证的数据清洗函数.
参数widget是一个forms.widgets对象(forms.widgets对象也称为小部件),
而且forms.widgets的类型必须与表单的字段类型相互对应, 不同的表单字段对应不同的forms.widgets类型,
对应规则分为4大类: 文本框类型, 下拉框(复选框)类型, 文件上传类型和复合框类型, 如表8-1所示.
表8-1 表单字段与小部件的对应规则
文本框类型 | | |
---|
Django Form Widget(部件) | Django Model Field(字段) | 描述 |
TextInput | CharField | 文本框内容设置为文本格式. |
NumberInput | IntegerField | 文本框内容只允许输入数值. |
EmailInput | EmailField | 验证输入值是否为邮箱地址格式. |
URLInput | URLField | 验证输入值是否为路由地址格式. |
PasswordInput | CharField | 输入值以'*'显示. |
HiddenInput | CharField | 隐藏文本框,不显示在网页上. |
DateInput | DateField | 验证输入值是否为日期格式. |
DateTimeInput | DateTimeField | 验证输入值是否为日期时间格式. |
TimeInput | TimeField | 验证输入值是否为时间格式. |
Textarea | harField | 将文本框设为Textarea格式. |
下拉框(复选框)类型 | | |
---|
Django Form Widget | Django Model Field | 描述 |
CheckboxInput | BooleanField | 设置复选框, 选项为True和False. |
Select | ChoiceField | 设置下拉框, 用于从多个选项中选择一个. |
NullBooleanSelect | NullBooleanField | 设置复选框, 选项为None、True和False. |
SelectMultiple | ChoiceField | 设置下拉框, 允许选择多个值. |
RadioSelect | ChoiceField | 将数据列表设置为单选按钮, 从多个选项中选择一个. |
CheckboxSelectMultiple | ChoiceField | 设置为复选框列表, 允许选择多个值. |
文件上传类型 | | |
---|
Django Form Widget | Django Model Field | 描述 |
FileInput | FileField 或 ImageField | 仅提供一个文件选择框, 用户可以通过这个选择框来选择要上传的文件. |
ClearableFileInput | FileField 或 ImageField | 增强的文件上传部件, 多了复选框, 允许清除上传的文件和图像. |
文件上传类型 | |
---|
Django Form Widget | 描述 |
MultipleHiddenInput | 用于隐藏一个或多个HTML的控件, 使其不在网页上显示. |
SplitDateTimeWidget | 组合控件, 它结合了DateInput和TimeInput的功能, 允许用户分别选择日期和时间. |
SplitHiddenDateTimeWidget | 与SplitDateTimeWidget类似, 但不同之处在于它会隐藏控件, 使其不在网页上可见. |
SelectDateWidget | 组合了三个Select下拉框, 分别用于选择年, 月, 日. 用户可以通过这三个下拉框来选择一个特定的日期. |
当我们为表单字段的参数widget设置对象类型时, 可以根据实际情况进行选择.
假设表单字段为SlugField类型, 该字段继承CharField,
因此可以选择文本框类型的任意一个对象类型作为参数widget的值, 如Textarea或URLInput等.
为了进一步验证优化后的表单是否正确运行, 我们对views.py的视图函数index进行代码优化, 代码如下:
from django.shortcuts import render
from django.http import HttpResponse
from .form import *
def index(request):
if request.method == 'GET':
v = VocationForm()
return render(request, 'index.html', locals())
else:
v = VocationForm(request.POST)
if v.is_valid():
ctitle = v.cleaned_data['title']
print(ctitle)
return HttpResponse('提交成功')
else:
error_msg = v.errors.as_json()
print(error_msg)
return render(request, 'index.html', locals())

视图函数index通过判断用户的请求方式(不同的请求方式执行不同的处理方案), 分别对GET和POST请求做了不同的响应处理, 说明如下:
● 用户在浏览器中访问: 127.0.0.1:8000 , 等同于向Django发送一个GET请求,
函数index将表单VocationForm实例化并传递给模板, 由模板引擎生成空白的网页表单显示在浏览器上.
● 当用户在网页表单上输入数据后, 单击'提交'按钮,
等同于向Django发送一个POST请求, 函数index将请求参数传递给表单类VocationForm, 生成表单对象v,
然后调用is_valid()对表单对象v进行数据验证.
● 如果验证成功, 就可以使用v.cleaned_data['title']来获取某个HTML控件的数据.
由于表单字段title设有自定义的数据清洗函数, 因此使用v.is_valid()验证表单数据时, Django自动执行数据清洗函数clean_title().
● 如果验证失败, 就使用errors.as_json()方法获取验证失败的错误信息, 然后将验证失败的信息通过模板返回给用户.
在模板文件index.html里, 我们使用errors和as_table方法生成网页表单,
除此之外, 还可以在模板文件里使用以下方法生成其他HTML标签的网页表单:
# 将表单生成HTML的ul标签
{{ v.as_ul }}
# 将表单生成HTML的p标签
{{ v.as_p }}
# 生成单个HTML元素控件
{{ v.title }}
# 获取表单字段的参数lable属性值
{{ v.title.label }}
运行MyDjango项目, 在浏览器访问: 127.0.0.1:8000 , 并在表单里输入数据信息,
当薪资的数值大于30000时, 提交表单就会触发自定义数据验证函数salary_validate, 网页上显示错误信息, 如图8-6所示.

图8-6 错误信息
(在Django的表单系统中, 执行is_valid()方法后, 数据的校验和清洗会经历以下步骤:
* 1. 字段级验证: Django会遍历表单中的每个字段, 并调用每个字段的clean()方法进行初步验证.
这包括检查字段是否为空(如果设置了required=True), 数据类型是否正确, 以及任何字段特定的验证器(validators)是否通过.
如果字段的clean()方法返回了错误, 该错误会被添加到表单的errors字典中, 与相应的字段名对应.
* 2. 自定义字段清洗: 如果表单定义了clean_<fieldname>()方法, Django会调用这些方法进行额外的字段清洗和验证.
这些方法可以从cleaned_data字典中获取已经通过初步验证的字段值, 执行自定义的验证逻辑,
并返回清洗后的值或抛出ValidationError.
如果clean_<fieldname>()方法返回了一个新值, 这个值会替换cleaned_data中原来的字段值.
* 3. 表单级验证: 在字段级(包括自定义字段)验证之后, Django会调用表单的clean()方法进行表单级的验证.
clean()方法用于执行跨字段的验证, 检查表单中多个字段之间的关系.
例如, 需要确保一个字段的值不大于另一个字段的值, 或者两个字段的值组合起来是唯一的.
这些跨字段的验证逻辑无法在字段级验证中单独实现, 因此需要通过表单级验证来完成.
如果clean()方法返回了错误, 这个错误会被添加到表单的non_field_errors列表中.
* 4. 更新cleaned_data: 一旦所有的字段级和表单级验证都完成, 并且没有抛出ValidationError,
Django会更新cleaned_data字典, 确保它包含所有已经通过验证和清洗的字段值.
* 5. 返回结果: 最后, is_valid()方法会根据是否有任何验证错误来决定返回True还是False.
如果返回True, 表示表单的数据是有效的, 你可以安全地从cleaned_data中获取并使用这些数据.
如果返回False, 表示表单的数据包含错误, 你应该检查errors和non_field_errors以了解具体的错误详情,
并在视图中适当处理这些错误.
需要注意的是, 如果在验证过程中的任何阶段抛出了ValidationError, Django会立即停止进一步的验证, 并将错误信息添加到相应的位置.
这意味着后面的验证步骤将不会被执行, 除非前面的步骤成功通过.
在自定义字段清洗方法中, self.cleaned_data的数据是初步验证的(比如非空检查, 数据类型检查等).
而视图中的cleaned_data中的数据是'完全清洗'的.)
( v = VocationForm(data=request.POST, prefix='vv') 这行代码创建了一个名为VocationForm的表单对象.
它使用request.POST数据(通常是从前端表单提交的数据)来填充表单字段.
prefix='vv'意味着这个表单的所有字段名都会有一个前缀'vv', 这在处理多个相同类型的表单时很有用, 可以避免字段名冲突.
返回值v: 根据request.POST数据(即用户通过 HTTP POST 请求提交到服务器的数据)创建的表单实例, 并且使用了'vv'作为字段名前缀.
打印表单对象会得到HTML格式的字符串(部分代码):
<tr>
<th><label for="id_vv-job">职位:</label></th>
<td><input type="text" name="vv-job" value="提交的数据" maxlength="20" required id="id_vv-job"></td>
</tr>
...
v['title']: 获取到BoundField对象.
BoundField对象代表表单中的一个字段, 并包含字段的验证状态, 值, 错误等属性.
直接打印这个对象通常会显示一个表示该字段的字符串,
如: <input type="text" name="vv-job" value="提交的数据" maxlength="20" required id="id_vv-job">.
v['title'].value(): 获取表单中'title'字段的当前值.
这是从request.POST数据中提取的, 如果用户提交了表单, 那么这将是用户输入的值.
如果request.POST中没有'title'字段, 那么它可能会返回空字符串或其他默认值(取决于VocationForm如何定义这个字段).
v.data: 表单使用的原始数据, 即request.POST数据(实例化时设置data=request.POST, 也是就v.data==request.POST).
这通常是一个QueryDict对象, 它类似于Python的字典, 但用于处理可能包含多个键的POST数据.
想要获取校验前的数据使用v.data, 获取校验后的数据使用v.cleaned_data. )
8.3 源码分析ModelForm
我们知道Django的表单分为两种: django.forms.Form和django.forms.ModelForm.
前者是一个基础的表单功能, 后者是在前者的基础上结合模型所生成的模型表单.
模型表单是将模型字段转换成表单字段, 由表单字段生成HTML控件, 从而生成网页表单.
本节讲述如何使用表单类ModelForm实现表单数据与模型数据之间的交互开发.
表单类ModelForm继承父类BaseModelForm, 其元类为ModelFormMetaclass.
在PyCharm中打开类ModelForm的定义过程, 如图8-7所示.

图8-7 表单类ModelForm的定义过程
在源码文件里分析并梳理表单类ModelForm的继承过程, 将结果以流程图的形式表示, 如图8-8所示.

图8-8 表单类ModelForm的继承关系
从表单类ModelForm的继承关系得知, 元类没有定义太多的属性和方法, 大部分的属性和方法都是由父类BaseModelForm和BaseForm定义的.
表单类BaseForm的属性方法在8.2节里已讲述过了, 因此这里只列举BaseModelForm的核心属性和方法.
● instance: 将模型查询的数据传入模型表单, 作为模型表单的初始化数据.
● clean(): 重写父类BaseForm的clean()方法, 并将属性_validate_unique设为True.
● validate_unique(): 验证表单数据是否存在异常.
● _save_m2m(): 将带有多对多关系的模型表单保存到数据库里.
● save(): 将模型表单的数据保存到数据库里.
如果参数commit为True, 就直接保存在数据库; 否则生成数据库实例对象.
表单类ModelForm与Form对比, 前者只增加了数据保存方法, 但是ModelForm与模型之间没有直接的数据交互.
模型表单与模型之间的数据交互是由函数modelform_factory实现的,
该函数将自定义的模型表单与模型进行绑定, 从而实现两者之间的数据交互.
函数modelform_factory与ModelForm定义在同一个源码文件中, 它定义了9个属性, 每个属性的作用说明如下:
● model: 必需属性, 用于绑定Model对象.
● fields: 可选属性, 设置模型内哪些字段转换成表单字段, 默认值为None,
代表所有的模型字段, 也可以将属性值设为'__all__', 同样表示所有的模型字段.
若只需部分模型字段, 则将模型字段写入一个列表或元组里, 再把该列表或元组作为属性值.
● exclude: 可选属性, 与fields相反, 禁止模型字段转换成表单字段.
属性值以列表或元组表示, 若设置了该属性, 则属性fields无须设置.
● labels: 可选属性, 设置表单字段的参数label, 属性值以字典表示, 字典的键为模型字段.
● widgets: 可选属性, 设置表单字段的参数widget, 属性值以字典表示, 字典的键为模型字段.
● localized_fields: 可选参数, 将模型字段设为本地化的表单字段, 常用于日期类型的模型字段.
● field_classes: 可选属性, 将模型字段重新定义, 默认情况下, 模型字段与表单字段遵从Django内置的转换规则.
● help_texts: 可选属性, 设置表单字段的参数help_text.
● error_messages: 可选属性, 设置表单字段的参数error_messages.
为了进一步了解模型表单ModelForm, 我们以8.2节的MyDjango为例,
在项目应用index的form.py中重新定义表单类VocationForm, 代码如下:
from django import forms
from .models import *
class VocationForm(forms.ModelForm):
LEVEL = (
('L1', '初级'),
('L2', '中级'),
('L3', '高级'),
)
level = forms.ChoiceField(choices=LEVEL, label='级别')
class Meta:
model = Vocation
exclude = []
label = {
'job': '职位',
'title': '职称',
'salary': '薪资',
'personinfo': '姓名',
}
widgets = {
'job': forms.widgets.TextInput(attrs={'class': 'c1'}),
}
field_classes = {
'job': forms.CharField
}
help_texts = {
'job': '请输入职位名称',
}
error_messages = {
'__all__': {
'required': '信息不能不空',
'invalid': '信息不符合要求',
},
'title': {
'required': '信息不能不空',
'invalid': '信息不符合要求',
},
}
def clean_salary(self):
data = self.cleaned_data['salary'] + 1
return data

上述代码中, 模型表单VocationForm可分为3大部分:
添加模型外的表单字段, 模型与表单的关联设置和自定义表单字段salary的数据清洗函数, 说明如下:
● 添加模型外的表单字段是在模型已有的字段下添加额外的表单字段.
● 模型与表单的关联设置是将模型字段转换成表单字段, 在模型表单的Meta属性里设置函数modelform_factory的属性.
● 自定义表单字段salary的数据清洗函数只适用于表单字段salary的数据清洗,
也可以将该函数视为表单字段的验证函数, 只需在函数里设置ValidationError异常抛出即可.
我们只重写表单类VocationForm, 项目里的其他代码不做任何修改, 运行MyDjango项目,
在浏览器访问127.0.0.1:8000, 运行结果如图8-9所示.

图8-9 运行结果
综上所述, 模型字段转换表单字段遵从Django内置的规则进行转换, 两者的转换规则如表8-2所示.
表8-2 模型字段与表单字段的转换规则
模型字段类型 | 表单字段类型 |
---|
AutoField | 不能转换表单字段(无需设置) |
BigAutoField | 不能转换表单字段 |
BigIntegerField | IntegerField |
BinaryField | CharField |
BooleanField | BooleanField 或者 NullBooleanField |
CharField | CharField |
DateField | DateField |
DateTimeField | DateTimeField |
DecimalField | DecimalField |
EmailField | EmailField |
FileField | FileField |
FilePathField | FilePathField |
ForeignKey | ModelChoiceField |
ImageField | ImageField |
IntegerField | IntegerField |
IPAddressField | IPAddressField |
GenericIPAddressField | GenericIPAddressField |
ManyToManyField | ModelMultipleChoiceField |
NullBooleanField | NullBooleanField |
PositiveIntegerField | IntegerField |
PositiveSmallIntegerField | IntegerField |
SlugField | SlugField |
SmallIntegerField | IntegerField |
TextField | CharField |
TimeField | TimeField |
URLField | URLField |
8.4 视图里使用Form
表单类型分为Form和ModelForm, 不同类型的表单有不同的使用方法, 本节将深入讲述如何在视图里使用表单Form和模型实现数据交互.
在8.2节的MyDjango项目里, 项目应用index的views.py中已简单演示了表单类Form的使用过程.
我们对视图函数index进行重写, 将表单Form和模型Vocation结合使用, 实现表单与模型之间的数据交互.
视图函数index的代码如下:
from django.shortcuts import render
from django.http import HttpResponse
from .form import *
from .models import *
def index(request):
if request.method == 'GET':
pk = request.GET.get('id', '')
if pk:
d = Vocation.objects.filter(pk=pk).values()
d = list(d)[0]
d['personinfo'] = d['personinfo_id']
i = dict(initial=d, label_suffix='*', prefix='vv')
v = VocationForm(**i)
else:
v = VocationForm(prefix='**')
return render(request, 'index.html', locals())
else:
v = VocationForm(data=request.POST, prefix='vv')
if v.is_valid():
ctitle = v.cleaned_data['title']
print(ctitle)
pk = request.GET.get('id', '')
d = v.cleaned_data
Vocation.objects.filter(pk=pk).update(**d)
return HttpResponse('提交成功!')
else:
error_msg = v.errors.as_json()
print(error_msg)
return render(request, 'index.html', locals())

(当Django处理带有表单的POST请求时, 内部处理过程涉及几个关键步骤, 这些步骤确保了数据能够正确地与表单字段进行匹配和验证.
以下是在处理POST请求时, 如果未设置与GET请求相同的prefix, Django表单内部处理过程可能遇到的问题:
* 1. 数据解析: Django首先会解析POST请求中的数据.
这些数据通常是一个键值对的集合, 代表了表单字段的名称和值.
* 2. 表单实例化: 如果没有为VocationForm的实例设置prefix参数, Django表单将尝试将POST数据中的键与表单字段的名称直接进行匹配.
* 3. 前缀问题: 如果在GET请求中设置了prefix='vv', 那么在HTML模板中渲染的表单字段名称都会带有vv-前缀.
例如, 一个名为name的字段在渲染时会变成vv-name.
当用户提交表单时, POST数据中的键也会带有这个前缀.
但是, 如果实例化VocationForm时没有设置相同的prefix, Django表单将不会知道这些带有前缀的键对应的是它的字段.
* 4. 字段匹配: Django表单会遍历其定义的字段, 并尝试在POST数据中找到与字段名称相匹配的键.
如果POST数据中的键带有前缀(如vv-name), 而表单实例没有设置相同的prefix,
Django将无法找到匹配的字段, 因为表单正在寻找不带前缀的键(如name).
* 5. 数据绑定: 对于每个匹配的字段, Django会将POST数据中对应的值绑定到该字段上.
如果因为前缀不匹配而无法找到字段, 那么该字段将不会被绑定任何值, 并可能被视为空或未提供.
* 5. 表单验证: 一旦数据被绑定到表单字段上, Django将进行表单验证.
如果因为前缀问题导致某些字段没有被正确绑定值, 那么这些字段可能会因为“必填字段未提供”等原因而验证失败.
* 6. 错误处理: 如果表单验证失败, Django会将错误信息存储在每个字段的errors属性中.
这些错误信息可以被用于在模板中显示给用户, 告诉他们哪些字段需要修正.
* 7. 重新渲染表单: 通常, 如果表单验证失败, 会重新渲染包含该表单的页面, 并显示错误信息.
如果在重新渲染时没有再次设置正确的prefix, 问题将仍然存在, 用户将无法成功提交表单.)
注意, 使用的是8.2小节使用的FROM表单, 不是8.3小节中的ModelForm表单.
视图函数index根据请求方式有3种处理方法: 不带请求参数的GET请求, 带请求参数的GET请求和POST请求, 详细说明如下.
当访问: 127.0.0.1:8000 时, Django接收一个不带请求参数的GET请求, 函数index将表单类VocationForm实例化并设置参数prefix.
如果在一个网页里使用同一个表单类生成多个不同的网页表单,
就可以设置参数prefix, 自定义每个表单的控件属性name和id的值, 从而使Django能识别每个网页表单.
最后将表单实例化对象传递给模板文件index.html, 并在浏览器上生成空白的网页表单, 如图8-10所示.

图8-10 不带请求参数的GET请求
当访问: 127.0.0.1:8000/?id=1 时, 浏览器向Django发送带参数id的GET请求,
视图函数index获取请求参数id的值, 并在模型Vocation中查找主键id等于1的数据,
然后将数据作为表单VocationForm的初始化数据, 在网页上显示相应的数据信息, 如图8-11所示.
在Django中, 通过GET方法提交的数据确实都是以字符串的形式传递的.
GET方法通常用于请求数据, 其数据被附加在URL后面, 通过查询字符串(query string)的形式传递.
由于URL和查询字符串的本质是文本, 所以GET方法提交的数据会自动被编码为字符串.
例如, 如果你有一个URL像这样: http://example.com/search?q=hello&page=1,
这里的q=hello和page=1都是通过GET方法提交的数据, hello和1都是以字符串的形式传递的.
当你查询数据库时, 即使模型的主键是整数类型(如AutoField), 也可以使用字符串形式的ID来查询记录.
Django的ORM会自动尝试将字符串转换为整数(如果模型的主键是整数类型)以执行查询.
SQL: SELECT * FROM table1 WHERE id = '1'; 的隐式类型转换

图8-11 带请求参数的GET请求
查询模型Vocation的数据时, 模型的外键字段为personinfo_id, 而表单类VocationForm的字段为personinfo,
因此在模型Vocation的查询结果上增加键为personinfo的键值对, 否则查询结果无法作为表单类VocationForm的初始化数据.
(生成表单的时候, 不是展示外键id值, 而是直接展示了关联的人员的名称, 修改数据的时候使用的需要外键id值, 而不是人员名称.
VocationForm的字段为personinfo可以看成是额外字段, 用于方便查看展示用户的名称,
在提交数据的时候, 提交的value值是人员表的主键, 将这个主键的值作为Vocation表的外键personinfo_id的值.
设置表单类VocationForm的personinfo设置值, 是为下拉框option的默认值, 展示对应的用户名称.
不设置, 则查看所有职业表的记录, 都默认展示下拉框的第一条数据.)



在图8-11上修改表单数据并单击'提交'按钮, 这时就会触发POST请求,
将表单的数据一并发送到Django, 视图函数index将请求参数(网页表单的数据)以data形式传递给表单类VocationForm,
由于在GET请求的表单VocationForm中设置了参数prefix,
因此在处理POST请求时, 实例化VocationForm必须设置参数prefix, 否则无法获取POST的请求数据.
也就是说, 使用同一个表单并且需要多次实例化表单时, 除了参数initial和data的数据不同之外,
其他参数设置必须相同, 否则无法接收上一个表单对象所传递的数据信息.

提交之后修改数据, 修改了两处:
1. clean_title 在title前面加上了'初级'两个字, (再次修改这个数据还会叠加初级两个字不用管.)
2. 修改salary的值, 10000 -> 12000.

表单VocationForm接收POST的请求参数后, 使用is_valid()方法执行表单验证.
如果验证通过, 就将表单数据更新到模型Vocation, 由于模型字段personinfo_id和表单字段personinfo都代表数据表的外键字段,
但两者的命名不同, 因此需要将表单字段personinfo转换成模型字段personinfo_id 最后才执行数据修改操作.
(Django的模型会自动处理_id, 这段其实可以自己省略.)
如果表单验证失败, 就将验证的错误信息显示在模板文件index.html中.
综上所述, 表单类Form和模型实现数据交互需要注意以下事项:
● 表单字段最好与模型字段相同, 否则两者在进行数据交互时, 必须将两者的字段进行转化.
● 使用同一个表单并且需要多次实例化表单时, 除了参数initial和data的数据不同之外,
其他参数设置必须相同, 否则无法接收上一个表单对象所传递的数据信息.
● 参数initial是表单实例化的初始化数据, 它只适用于模型数据传递给表单, 再由表单显示在网页上;
参数data是在表单实例化之后, 再将数据传递给实例化对象, 只适用于表单接收HTTP请求的请求参数.
● 参数prefix设置表单的控件属性name和id的值,
若在一个网页里使用同一个表单类生成多个不同的网页表单, 参数prefix可区分每个网页表单,
则在接收或设置某个表单数据时不会与其他的表单数据混淆.
8.5 视图里使用ModelForm
从8.4节的例子可以看出, 表单类Form和模型实现数据交互最主要的问题是表单字段和模型字段的匹配性.
如果将表单类Form改为ModelForm, 就无须考虑字段匹配性的问题.
以8.4节的MyDjango为例, 在项目应用index的form.py中定义模型表单VocationForm, 代码如下:
from django import forms
from .models import *
class VocationForm(forms.ModelForm):
class Meta:
model = Vocation
fields = '__all__'
labels = {
'job': '职位',
'title': '职称',
'salary': '薪资',
'personinfo': '姓名',
}
error_messages = {
'__all__': {
'required': '请输入内容!',
'invalid': '请检查输入内容!',
}
}
def clean_salary(self):
data = self.cleaned_data['salary'] + 10
return data
由于模型表单ModelForm比表单Form新增了数据保存方法save(),
因此视图函数index分别对模型Vocation进行数据修改和新增操作, 代码如下:
from django.shortcuts import render
from django.http import HttpResponse
from .form import *
from .models import *
def index(request):
if request.method == 'GET':
pk = request.GET.get('id', '')
if pk:
i = Vocation.objects.filter(id=1).first()
v = VocationForm(instance=i, prefix='vv')
else:
v = VocationForm(prefix='vv')
return render(request, 'index.html', locals())
else:
v = VocationForm(data=request.POST, prefix='vv')
if v.is_valid():
pk = request.GET.get('id')
result = Vocation.objects.filter(id=pk)
if result:
v1 = v.save(commit=False)
v1.title = '初级' + v1.title
v1.save()
return HttpResponse('新增成功!')
else:
d = v.cleaned_data
d['title'] = '中级' + d['title']
result.update(**d)
return HttpResponse('修改成功!')
else:
error_msg = v.errors.as_json()
print(error_msg)
return render(request, 'index.html', locals())
上述代码与8.4节的视图函数index所实现的功能大致相同, 但从中可以发现,
模型表单ModelForm与表单Form的使用过程存在差异, 具体的分析说明如下.
(在HTTP协议中, 有两种主要的方式来发送数据到服务器: GET和POST.
这两种方式在Django框架中通过request.GET和request.POST对象来处理.
GET请求: GET请求通常用于请求数据.
URL的查询字符串(即URL中?后面的部分)包含了GET请求的参数.
在Django中, 通过request.GET对象来获取这些参数。
例如, URL http://127.0.0.1:8000/?id=1 中的 id=1 就是查询字符串.
如果你通过GET请求访问这个URL, 可以通过request.GET.get('id')来获取 id 的值, 即 1.
POST请求: POST请求通常用于提交数据, 如表单数据.
数据包含在请求体中, 而不是URL中.
在Django中, 可以通过request.POST对象来获取POST请求体中的数据.
如果通过POST方法提交一个表单, 表单字段的值会作为POST请求体发送.
因此, 要获取这些值, 可以使用request.POST.get('field_name'), 其中field_name是表单中的一个字段名。
看见有人提问: '为什么是request.GET.get('id')而不是request.POST.get('id')来尝试获取id参数的值?'
答案是: 取决于id是如何发送到服务器的.
如果id是作为URL的一部分(即查询字符串)发送的, 那么应该使用request.GET.get('id')来获取它, 因为GET请求的参数是通过URL传递的.
如果id是作为表单数据通过POST请求体发送的, 那么你应该使用request.POST.get('id')来获取它.
在上面的场景中, id是作为URL查询字符串的一部分发送的(http://127.0.0.1:8000/?id=1),
因此你应该使用request.GET.get('id')来获取它, 而不是request.POST.get('id'). )
当访问 127.0.0.1:8000/?id=1 时, 视图函数index获取请求参数id的值(id=1),
并在模型Vocation中查询主键id等于1的数据, 然后将查询对象作为表单VocationForm的参数instance并执行表单实例化,
最后将表单实例化对象传递给模板文件, 在网页上生成网页表单, 如图8-12所示.

图8-12 运行结果
如果模型的查询对象为空, 就代表请求参数id的值在模型Vocation里找不到相应的数据,
如果表单的参数instance为None, 网页上就会生成一个空白的网页表单, 如图8-13所示.

图8-13 运行结果
在表单上填写数据并单击'提交'按钮后, Django将接收一个POST请求,
视图函数使用模型表单VocationForm接收POST的请求参数, 生成表单对象v; 然后调用is_valid()方法验证表单数据.
如果表单验证失败, 就将验证信息传递给模板文件index.html并显示在网页上.
如果表单验证成功, 就从POST请求的路由地址里获取请求参数id, 并将参数id的值作为模型Vocation的查询条件.
若查询结果不为空, 则执行数据修改; 若查询结果为空, 则由表单对象调用save()方法实现模型的数据新增.

修改薪资为14000点击提交, 在执行字段清理的时候会自动+10, 在携带id访问可查询数据修改情况.

视图函数index列举了模型表单ModelForm保存数据的3种方法, 实质上实现数据保存只有save()和save_m2m()两种方法.
使用save()保存数据时, 参数commit的值会影响数据的保存方式.
如果参数commit为True, 就直接将表单数据保存到数据库;
如果参数commit为False, 就会生成一个数据库对象, 然后可以对该对象进行增, 删, 改, 查等数据操作, 再将修改后的数据保存到数据库.
值得注意的是, save()只适合将数据保存在非多对多关系的数据表中, 而save_m2m()只适合将数据保存在多对多关系的数据表中.
8.6 同一网页多个表单
一个网页中可能存在多个不同的表单, 每个表单可能有一个或多个提交按钮.
如果一个网页中有多个不同表单, 每个表单仅有一个提交按钮, 当点击某个表单的某个提交按钮的时候,
程序如何在多个表单中获取某个表单的数据呢? Django又是如何将多个表单逐一区分呢?
如果网页表单是由Django生成, 并且同一个表单类实例化多个表单对象, 在实例化过程中可以设置参数prefix,
该参数是对每个表单对象进行命名, Django通过表单对象的命名进行区分和管理.
以8.4节为例, 由项目应用index的form.py定义的表单类VocationForm创建多个表单对象.
我们打开MyDjango的urls.py, index的urls.py, views.py和templates的index.html,
分别定义路由index, 视图函数indexView()和模板文件index.html, 代码如下:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include(('index.urls', 'index'), namespace='index'))
]

from django.urls import path
from .views import *
urlpatterns = [
path('', index_view, name='index')
]

from django.shortcuts import render
from django.http import HttpResponse
from .form import *
def index_view(request):
if request.method == 'GET':
v = VocationForm(prefix='vv')
w = VocationForm(prefix='ww')
return render(request, 'index.html', locals())
else:
v = VocationForm(data=request.POST, prefix='vv')
w = VocationForm(data=request.POST, prefix='ww')
if v.is_valid():
print(v.data)
return HttpResponse('表单1提交成功!')
elif w.is_valid():
print(w.data)
return HttpResponse('表单2提交成功!')
else:
error_msg = v.errors.as_json()
print(error_msg)
return render(request, 'index.html', locals())

!!! 使用8.2小节的form.py文件, 下面的html中只显示表单v的错误信息.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标题</title>
</head>
<body>
{# 校验的字段如果不通过会将错误信息存放在errors属性中 #}
{% if v.errors %}
<p>数据错误: {{ v.errors }}</p>
{% else %}
<form action="" method="post">
{% csrf_token %}
<table>
{# 生成table表单 #}
{{ v.as_table }}
</table>
<input type="submit" value="提交">
</form>
<form action="" method="post">
{% csrf_token %}
<table>
{# 生成table表单 #}
{{ w.as_table }}
</table>
<input type="submit" value="提交">
</form>
{% endif %}
</body>
</html>

运行上述代码, 在浏览器中访问: http://127.0.0.1:8000/ , 可以看到网页上生成了两个不同的表单, 如图8-14所示.

图8-14 网页表单
查看网页的源码, 两个表单的标签设置不同的了前缀.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标题</title>
</head>
<body>
<form action="" method="post">
<input type="hidden" name="csrfmiddlewaretoken"
value="1bvx7pwf9sFsZtmHeRVldnMwyAKVhFokIoWkbpgPg5klf0vJwMQG9seDcJC9haG6">
<table>
<tr>
<th><label for="id_vv-job">职位:</label></th>
<td><input type="text" name="vv-job" maxlength="20" required id="id_vv-job"></td>
</tr>
<tr>
<th><label for="id_vv-title">职称:</label></th>
<td><input type="text" name="vv-title" class="c1" maxlength="20" required id="id_vv-title"></td>
</tr>
<tr>
<th><label for="id_vv-salary">薪资:</label></th>
<td><input type="number" name="vv-salary" step="any" required id="id_vv-salary"></td>
</tr>
<tr>
<th><label for="id_vv-personinfo">姓名:</label></th>
<td><select name="vv-personinfo" id="id_vv-personinfo">
<option value="1">张三</option>
<option value="2">李四</option>
<option value="3">王五</option>
<option value="4">赵六</option>
</select></td>
</tr>
</table>
<input type="submit" value="提交">
</form>
<form action="" method="post">
<input type="hidden" name="csrfmiddlewaretoken"
value="1bvx7pwf9sFsZtmHeRVldnMwyAKVhFokIoWkbpgPg5klf0vJwMQG9seDcJC9haG6">
<table>
<tr>
<th><label for="id_ww-job">职位:</label></th>
<td><input type="text" name="ww-job" maxlength="20" required id="id_ww-job"></td>
</tr>
<tr>
<th><label for="id_ww-title">职称:</label></th>
<td><input type="text" name="ww-title" class="c1" maxlength="20" required id="id_ww-title"></td>
</tr>
<tr>
<th><label for="id_ww-salary">薪资:</label></th>
<td><input type="number" name="ww-salary" step="any" required id="id_ww-salary"></td>
</tr>
<tr>
<th><label for="id_ww-personinfo">姓名:</label></th>
<td><select name="ww-personinfo" id="id_ww-personinfo">
<option value="1">张三</option>
<option value="2">李四</option>
<option value="3">王五</option>
<option value="4">赵六</option>
</select></td>
</tr>
</table>
<input type="submit" value="提交">
</form>
</body>
</html>
当表单绑定到数据时(如VocationForm(data=request.POST)) Django表单会查找与表单字段名称匹配的数据.
如果设置了prefix, 则它会查找带有该前缀的字段名称. 这样, 即使页面上有多个相同的表单实例, Django也能正确区分它们各自的输入数据.

上述代码中, 视图函数indexView()分别对GET请求和POST请求执行了不同处理, 详细说明如下:
(1) 当用户在浏览器中访问路由index的时候, 即访问: http://127.0.0.1:8000/ , 视图函数indexView()将收到一个GET请求,
首先使用表单类VocationForm实例化生成两个表单对象v和w, 表单对象v和w分别设置参数prefix等于vv和ww;
再把表单对象v和w传给模板文件index.html, 由模板语法解析表单对象v和w, 生成相应的网页表单.
(2) 当用户在网页表单上输入数据后, 并单击表单的'提交'按钮, 浏览器向Django发送POST请求, 视图函数indexView()收到POST请求后,
首先使用表单类VocationForm实例化生成两个表单对象v和w, 分别设置参数prefix等于vv和ww,
并将POST请求的请求参数加载到两个表单对象v和w;
然后由表单对象v和w调用is_valid()方法验证表单数据, 只要表单对象v或表单对象w验证成功, 就能确定用户使用了哪一个表单.

综合上述, 如需一个网页中生成多个表单, Django的实现过程如下:
(1) 使用同一表单类生成多个表单对象, 在实例化表单对象之前, 设置参数prefix即可生成不同的表单对象,
Django通过参数prefix区分和管理多个表单对象.
(2) 在验证表单数据的时候, Django是通过参数prefix确定当前请求参数是来自哪一个表单对象.
index_view中对错误信息处理的不够完整, 目前能只能显示表单v的错误信息:

小改if的判断逻辑, 这样更加合理.
from django.shortcuts import render
from django.http import HttpResponse
from .form import *
def index_view(request):
if request.method == 'GET':
v = VocationForm(prefix='vv')
w = VocationForm(prefix='ww')
return render(request, 'index.html', locals())
else:
if 'vv-job' in request.POST:
v = VocationForm(data=request.POST, prefix='vv')
if v.is_valid():
print(v.data)
return HttpResponse('表单1提交成功!')
else:
error_msg = v.errors.as_json()
return render(request, 'index.html', locals())
if 'ww-job' in request.POST:
w = VocationForm(data=request.POST, prefix='ww')
if w.is_valid():
print(w.data)
return HttpResponse('表单2提交成功!')
else:
error_msg = w.errors.as_json()
return render(request, 'index.html', locals())
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标题</title>
</head>
<body>
{# 校验的字段如果不通过会将错误信息存放在errors属性中 #}
{% if v.errors or w.errors %}
{% if v.errors %}
<p>表单1数据错误: {{ v.errors }}</p>
{% else %}
<p>表单2数据错误: {{ w.errors }}</p>
{% endif %}
{% else %}
<form action="" method="post">
{% csrf_token %}
<table>
{# 生成table表单 #}
{{ v.as_table }}
</table>
<input type="submit" value="提交">
</form>
<form action="" method="post">
{% csrf_token %}
<table>
{# 生成table表单 #}
{{ w.as_table }}
</table>
<input type="submit" value="提交">
</form>
{% endif %}
</body>
</html>
测试salary > 30000的异常.

8.7 一个表单多个按钮
在一个网页表单中可能存在多个不同功能的按钮, 比如用户登录页面的验证码获取按钮, 登录按钮, 注册按钮等,
这都是在同一个表单中设置了多个按钮, 并且每个按钮触发的功能各不相同.
如果表单所有功能都是由Django实现(排除Ajax异步请求这类实现方式),
那么多个按钮所触发的功能应在同一个视图函数中实现, 视图函数可以根据当前请求进行判断, 以分辨出当前请求是由哪一个按钮触发.
以8.6节的实例为例, 表单还是由表单类VocationForm实例化生成,
只需修改路由index的视图函数indexView()和模板文件index.html即可, 代码如下所示:
from django.shortcuts import render
from django.http import HttpResponse
from .form import *
def index_view(request):
if request.method == 'GET':
v = VocationForm(prefix='vv')
return render(request, 'index.html', locals())
else:
v = VocationForm(request.POST, prefix='vv')
if 'add' in request.POST:
return HttpResponse('提交成功!')
else:
return HttpResponse('修改成功!')

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标题</title>
</head>
<body>
{# 校验的字段如果不通过会将错误信息存放在errors属性中 #}
{% if v.errors %}
<p>数据出错啦, 错误信息: {{ v.errors }}</p>
{% else %}
<form action="" method="post">
{% csrf_token %}
<table>
{# 生成table表单 #}
{{ v.as_table }}
</table>
<input type="submit" name="add" value="提交">
<input type="submit" name="update" value="修改">
</form>
{% endif %}
</body>
</html>

上述代码中, 视图函数indexView()分别对GET请求和POST请求执行了不同处理, 详细说明如下:
(1) 用户在浏览器访问路由index的时候, 视图函数indexView()将收到GET请求,
首先使用表单类VocationForm实例化生成表单对象v, 并传递给模板文件index.html, 由模板语法解析表单对象v生成网页表单.
(2) 用户在网页表单上输入数据后, 并单击表单某个按钮, 浏览器向Django发送POST请求, 视图函数indexView()收到POST请求后,
使用表单类VocationForm实例化生成表单对象v, 将请求参数传入对象v中, 并判断当前请求是由哪一个按钮触发.
代码中"if 'add' in request.POST"的add是模板文件index.html定义'提交'按钮的name属性.
如果add在当前请求里面, 说明当前请求是由'提交'按钮触发, 否则是由'修改'按钮触发.
综合上述, 如需在表单里设置多个不同功能的按钮, 其实现过程如下:
(1) 模板文件必须对每个按钮设置name属性, 并且每个按钮的name属性值是唯一的.
(2) 同一表单中, HTML的<form>标签的action属性只能设置一次,
也就是说, 一个表单只能设置一个路由, 表单中多个按钮的业务逻辑只能由同一个视图函数处理.
(3) 视图函数从当前请求信息与每个按钮的name属性进行判断, 如果符合判断条件, 则说明当前请求是由该按钮触发,
比如"if 'add' in request.POST"等于True的时候, 当前请求是由name="add"的按钮触发.

(HTML表单的提交机制是基于用户交互的, 特别是基于用户点击某个提交按钮的行为.
当用户点击表单中的一个提交按钮时, 浏览器会收集该表单中所有具有name属性的表单控件的值,
并将它们作为表单数据的一部分发送给服务器.
这个过程中, 只有被点击的提交按钮的name和value(如果存在)才会被包含在提交的数据中.
* HTML规范定义了表单提交的行为, 即只提交被点击的提交按钮的name和value(如果存在).
这是为了确保表单提交的语义清晰和可预测.)
8.8 表单的批量处理
正常情况下, 表单的每个字段只会在网页上出现一次, 如果要录入多条数据, 就要重复多次提交表单.
比如我们向财务系统录入多条报销数据, 每一次报销操作都是在相同的表单字段中填写不同的数据内容,
每一条报销数据都要单击'提交'按钮才能将数据保存到系统中.
为了减少'提交'按钮的单击次数, 可以将多条数据在一个表单中填写,
只要表单能生成多个相同的表单字段, 当单击'提交'按钮后, 系统将对表单数据执行批量操作, 并在数据库中保存多条数据.
使用Django实现数据的批量处理, 可以在表单类实例化的时候重新定义表单的工厂函数,
表单的实例化对象在生成网页表单的时候, Django自动为每个表单字段创建多个网页元素.
以MyDjango为例, 在项目应用index创建form.py文件,
分别在models.py和form.py中定义模型PersonInfo和表单类PersonInfoForm, 代码如下:
from django.db import models
class PersonInfo(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=20)
age = models.IntegerField()
def __str__(self):
return str(self.name)
class Meta:
verbose_name = '人员信息'

from django import forms
from .models import *
class PersonInfoForm(forms.ModelForm):
class Meta:
model = PersonInfo
fields = '__all__'
labels = {
'name': '姓名',
'age': '年龄'
}

定义模型和表单类之后, 下一步执行数据迁移, 根据模型的定义过程在数据库中创建相应的数据表, 数据库采用SQLite3数据库.
在PyChram的Terminal窗口输入数据迁移指令, 如下所示:
D:\MyDjango> python manage.py makemigrations
D:\MyDjango> python manage.py migrate
最后在MyDjango的urls.py, 项目应用index的urls.py, views.py和templates的index.html中
分别编写路由index, 视图indexView和模板文件index.html, 代码如下:
from django.urls import path, include
urlpatterns = [
path('', include(('index.urls', 'index'), namespace='index'))
]

from django.urls import path
from .views import *
urlpatterns = [
path('', index_view, name='index')
]

from django.shortcuts import render
from django.http import HttpResponse
from .form import *
def index_view(request):
pfs = forms.formset_factory(PersonInfoForm, extra=2, max_num=5)
if request.method == 'GET':
p = pfs()
return render(request, 'index.html', locals())
else:
p = pfs(request.POST)
if p.is_valid():
for i in p:
i.save()
return HttpResponse('新增成功!')
else:
error_msg = p.errors.as_json()
print(error_msg)
return render(request, 'index.html', locals())

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标题</title>
</head>
<body>
{# 校验的字段如果不通过会将错误信息存放在errors属性中 #}
{% if p.errors %}
<p>数据出错啦, 错误信息: {{ p.errors }}</p>
{% else %}
<form action="" method="post">
{% csrf_token %}
<table>
{# 生成table表单 #}
{{ p.as_table }}
</table>
<input type="submit" name="add" value="提交">
</form>
{% endif %}
</body>
</html>

<input type="hidden" name="form-TOTAL_FORMS" value="2" id="id_form-TOTAL_FORMS">
<input type="hidden" name="form-INITIAL_FORMS" value="0"id="id_form-INITIAL_FORMS">
<input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS">
<input type="hidden" name="form-MAX_NUM_FORMS" value="5" id="id_form-MAX_NUM_FORMS">
<tr>
<th><label for="id_form-0-name">姓名:</label></th>
<td><input type="text" name="form-0-name" maxlength="20" id="id_form-0-name"></td>
</tr>
<tr>
<th><label for="id_form-0-age">年龄:</label></th>
<td><input type="number" name="form-0-age" id="id_form-0-age"></td>
</tr>
<tr>
<th><label for="id_form-1-name">姓名:</label></th>
<td><input type="text" name="form-1-name" maxlength="20" id="id_form-1-name"></td>
</tr>
<tr>
<th><label for="id_form-1-age">年龄:</label></th>
<td><input type="number" name="form-1-age" id="id_form-1-age"></td>
</tr>
<tr>
<th><label for="id_form-0-name">姓名:</label></th>
<td><input type="text" name="form-0-name" value="kid" maxlength="20" id="id_form-0-name"></td>
</tr>
<tr>
<th><label for="id_form-0-age">年龄:</label></th>
<td><input type="number" name="form-0-age" value="1" id="id_form-0-age"></td>
</tr>
-------------------
<tr>
<th><label for="id_form-1-name">姓名:</label></th>
<td><input type="text" name="form-1-name" value="qq" maxlength="20" id="id_form-1-name"></td>
</tr>
<tr>
<th><label for="id_form-1-age">年龄:</label></th>
<td><input type="number" name="form-1-age" value="2" id="id_form-1-age"></td>
</tr>
从视图函数indexView()中看到, 表单类PersonInfoForm是通过内置的表单工厂函数formset_factory()生成实例化对象的.
从分析工厂函数formset_factory()得知, 它一共设置了9个参数, 每个参数的说明如下:
(1) 参数form设置表单类, 表单类可以为Form或ModelForm类型.
(2) 参数formset在表单初始化的时候设置初始数据.
(3) 参数extra的默认值为1, 它用于设置表单字段的数量.
(4) 参数can_order的默认值为False, 这是对表单数据进行排序操作.
(5) 参数can_delete的默认值为False, 这是删除表单数据.
(6) 参数max_num的默认值为None, 最多生成1000个表单字段, 它是限制表单字段的最大数量.
(7) 参数validate_max验证并检查表单所有的数据量, 减去已被删除的数据量, 剩余的数据量必须小于或等于参数max_num的值.
(8) 参数min_num限制表单字段的最小数量.
(9) 参数validate_min验证并检查表单所有的数据量, 减去已被删除的数据量, 剩余的数据量必须大于或等于参数min_num的值.
工厂函数formset_factory()能支持表单类Form或ModelForm的实例化,
但表单类ModelForm是在模型基础上定义, 所以Django还定义工厂函数modelformset_factory(),
它一共设置了19个函数参数, 每个参数的说明如下:
(1) 参数model代表已定义的模型.
(2) 参数form是需要实例化的表单类, 表单类必须为ModelForm类型.
(3) 参数formfield_callback设置回调函数, 函数参数是模型字段, 返回值是表单字段, 可以对模型字段进行加工处理, 然后生成表单字段.
(4) 参数formset在表单初始化的时候设置初始数据.
(5) 参数extra的默认值为1, 它是设置表单字段的数量.
在渲染表单集合时默认应包含多少个额外的空白表单实例
(6) 参数can_order的默认值为False, 这是对表单数据进行排序操作.
(7) 参数can_delete的默认值为False, 这是删除表单数据.
用于在每个表单旁边添加一个删除复选框, 允许用户通过勾选这个复选框来标记要删除的对象.
在配置文件中设置: LANGUAGE_CODE = 'zh-hans' , 将标签设置为简体中文.
(8) 参数max_num的默认值为None, 最多生成1000个表单字段, 它是限制表单字段的最大数量.
(9) 参数fields是选择某些表单字段能生成表单的网页元素.
(10) 参数exclude是选择某些表单字段不能生成表单的网页元素.
(11) 参数widgets是为模型字段设置窗口小部件(即网页元素的属性, 样式等设置).
(12) 参数validate_max验证并检查表单所有的数据量, 减去已被删除的数据量, 剩余的数据量必须小于或等于参数max_num的值.
(13) 参数localized_fields设置字段的本地化名称, 即中英文切换时, 字段名称的中英翻译.
(14) 参数labels设置字段在网页表单上显示的名称.
(15) 参数help_texts是为每个字段设置提示信息.
(16) 参数error_messages是为每个字段设置错误信息.
(17) 参数min_num限制表单字段的最小数量.
(18) 参数validate_min验证并检查表单所有的数据量, 减去已被删除的数据量, 剩余的数据量必须大于或等于参数min_num的值.
(19) 参数field_classes是表单字段和模型字段的映射关系.
由于表单类PersonInfoForm是ModelForm类型, 所以它能使用工厂函数formset_factory()或modelformset_factory()创建表单对象,
上述例子使用了工厂函数formset_factory()对表单类PersonInfoForm执行实例化的过程.
运行MyDjango, 在浏览器访问: http://127.0.0.1:8000/ , Django分别为表单字段name和age创建了两个网页元素, 如图8-15所示.

图8-15 运行结果
当我们在网页表单上输入数据并单击'提交'按钮, 浏览器向Django发送POST请求,
视图函数indexView()使用表单对象pfs接收表单数据, 再由表单对象pfs调用save()方法就能将数据批量保存到数据库中.
综上所述, Django实现表单数据的批量处理, 可以使用内置工厂函数formset_factory()或modelformset_factory()
改变表单类的实例化过程, 再由表单对象生成网页表单.
当浏览器将网页表单提交到Django时, 必须由同一个表单对象接收表单数据才能将数据保存在数据库中.
from django.shortcuts import render
from .form import *
from .models import PersonInfo
def index_view(request):
pfs = forms.modelformset_factory(model=PersonInfo, form=PersonInfoForm, extra=0, can_delete=True, max_num=5)
if request.method == 'GET':
p = pfs()
return render(request, 'index.html', locals())
else:
p = pfs(request.POST)
if p.is_valid():
for form in p:
if form.cleaned_data.get('DELETE'):
form.instance.delete()
else:
form.save()
return redirect('/')
error_msg = p.errors.as_json()
print(error_msg)
return render(request, 'index.html', locals())

在Django中, modelformset_factory用于创建一个表单集(formset), 它允许你同时处理多个模型实例的表单.
通过modelformset_factory创建一个表单集并设置can_delete=True时,
Django会为每个表单添加一个额外的字段(通常是一个隐藏的复选框), 用于标记该实例是否应该被删除.
当用户提交表单集时, Django会验证每个表单的数据.
如果某个表单的DELETE字段被选中(即用户选择了要删除该实例),
则Django会在表单的cleaned_data字典中为该字段设置一个值(通常是True).
在index_view视图中, 遍历了表单集中的每个表单, 并检查了cleaned_data字典中的DELETE字段.
如果DELETE字段存在并且其值为True, 则知道用户想要删除该实例.
此时, 可以通过form.instance访问与该表单关联的模型实例.
由于form是一个模型表单(ModelForm)的实例, form.instance将是一个模型实例(在上面的例子中是PersonInfo实例).
调用form.instance.delete()实际上是调用了Django ORM提供的delete()方法,
该方法将删除与form.instance关联的数据库记录.
因此, 通过form.instance.delete(), 能够删除与表单集中的特定表单关联的模型实例.
在Django的表单(Form)和表单集(FormSet)中, instance属性和cleaned_data字典在功能和数据来源上有显著的区别.
* 1. instance 属性: 通常与模型表单(ModelForm)相关, 它指向一个模型实例.
在创建模型表单时, 如果你传递了一个模型实例作为参数(例如, 在编辑现有对象时),
那么这个实例会被存储在表单的instance属性中.
用途: 主要用于在表单保存时确定应该更新哪个数据库对象.
如果表单是通过一个已经存在的对象实例化的, 那么当表单验证通过并调用save()方法时,
Django会默认更新这个instance而不是创建一个新的对象.
数据来源: 来自Django模型的一个实例.
注意事项: 在表单验证失败并重新渲染时, instance仍然指向原来的模型实例, 因此你可以用它来在模板中显示原始数据.
* 2. cleaned_data 字典: 包含了表单字段经过验证和清洗后的数据.
在表单验证通过后, 可以通过cleaned_data字典访问表单字段的值.
用途: 主要用于访问用户输入的数据.
在表单验证通过后, 可以从cleaned_data中获取字段值, 并在视图中进行进一步的处理, 如保存到数据库或执行其他逻辑.
数据来源: 来自用户提交的表单数据, 经过Django表单的验证和清洗过程.
注意事项: 如果表单验证失败, cleaned_data将是空的(或只包含部分经过验证的字段).
在Django中, 通过删除instance属性所引用的模型实例(即调用instance.delete())来删除数据,
是因为Django的ORM(对象关系映射)系统提供了一种简洁, 直观的方式来与数据库进行交互.
具体来说, 当有一个Django模型实例时, 可以直接调用该实例的delete()方法来从数据库中删除相应的记录.
启动项目, 访问: 127.0.0.1:8000 查询数据:

删除单选框, 默认显示为英文.
可以设置适当的语言环境和国际化支持, 在settings.py中设置LANGUAGE_CODE和USE_I18N:
LANGUAGE_CODE = 'zh-hans'
USE_I18N = True

再次访问: 127.0.0.1:8000 , 删除单选框显示为中文.

勾选删除单选框, 点击提交数据, 完成删除数据.

8.9 多文件批量上存
如果网页表单是由Django的表单类创建, 并且表单设有文件上存功能, 那么可以在表单类中设置表单字段为forms.FileField类型;
或者在模型中设置模型字段models.FileField, 通过模型映射到表单类ModelForm, 从而生成文件上存的功能控件.
每次执行文件上存, Django只能上存一个文件.
如果要实现多个文件批量上存, 需要设置表单字段FileField的参数widget, 使网页表单支持多个文件上存.
以人员信息为例, 每个人会有多张证件信息, 因此定义模型PersonInfo和CertificateInfo, 分别代表人员基本信息和证件信息.
以MyDjango为例, 在项目目录MyDjango下创建media文件夹, 并在media文件夹中再创建images文件夹, 目录结构如图8-16所示.

图8-16 目录结构
将项目目录Django的media媒体文件夹路径注册Django中.
这样做是为了告诉Django, 媒体文件(如用户上传的图片, 文档等)存储在文件系统中的哪个位置.
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

下一步在项目应用index的models.py定义模型PersonInfo, CertificateInfo以及重写Django内置文件系统类FileSystemStorage,
实现代码如下:
from django.db import models
import os
from django.core.files.storage import FileSystemStorage
from django.conf import settings
class PersonInfo(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=20)
age = models.IntegerField()
def __str__(self):
return str(self.name)
class Meta:
verbose_name = '人员信息'
class MyStorage(FileSystemStorage):
def get_available_name(self, name, max_length=None):
if self.exists(name):
os.remove(os.path.join(settings.MEDIA_ROOT, name))
return name
class CertificateInfo(models.Model):
id = models.AutoField(primary_key=True)
certificate = models.FileField(blank=True,
upload_to='images/', storage=MyStorage())
person_info = models.ForeignKey(PersonInfo, blank=True, null=True, on_delete=models.CASCADE)
def __str__(self):
return self.person_info
class Meta:
verbose_name = '证件信息'

python manage.py makemigrations
python manage.py migrate

默认情况下, Django内置的文件系统类FileSystemStorage不会覆盖相同命名的文件,
如果上存文件与系统已有的文件命名重复, Django自动为上存文件重新命名.
上述代码中, 重写了内置文件系统类FileSystemStorage的get_available_name(),
判断当前上存文件与系统已有的文件是否命名重复, 若存在则删除系统的原文件, 使上存的文件能保存在系统中.
模型PersonInfo和CertificateInfo之间通过外键person_info进行关联, 实现数据的一对多关系, 使得一个人员能上存多张证件信息.
模型定义后需要执行数据迁移, 在数据库中创建相应的数据表.
下一步在项目应用index的form.py定义表单类PersonInfoForm,
它将模型PersonInfo的字段映射为表单字段, 并添加表单字段certificate, 该字段生成文件上存的功能控件, 表单类的定义过程如下:
from django import forms
from .models import *
class PersonInfoForm(forms.ModelForm):
certificate = forms.FileField(
label='证件',
allow_empty_file=True,
widget=forms.ClearableFileInput(
attrs={'multiple': True}
))
class Meta:
model = PersonInfo
fields = '__all__'
labels = {
'name': '姓名',
'age': '年龄',
}

然后在MyDjango的urls.py和项目应用index的urls.py定义路由信息, 分别定义路由index和路由media.
路由index是生成网页表单的地址链接, 路由media是一个指向媒体文件的路由,
其主要作用是为了在开发环境中提供一种方便的方式来服务媒体文件, 以便开发者在本地开发时能够访问到这些文件.
切勿忘记在配置文件settings.py中配置media文件夹的路径信息, 详细代码如下:
from django.urls import path, include, re_path
from django.views.static import serve
from django.conf import settings
urlpatterns = [
path('', include(('index.urls', 'index'), namespace='index')),
re_path('media/(?P<path>.*)', serve,
{'document_root': settings.MEDIA_ROOT}, name='media'),
]

解释一下为什么from django.conf import settings能从django.conf中导入配置:
在Django中, django.conf是一个包, 它包含了一系列与配置相关的模块和工具.
当执行from django.conf import settings时, 并不是在导入django.conf包中的一个静态定义的模块或对象.
相反, 这个settings对象是由Django框架在运行时动态创建和配置的.
以下是这个过程的简要概述:
* 1. 环境变量: 首先, Django依赖于一个名为DJANGO_SETTINGS_MODULE的环境变量来知道哪个模块包含了你的项目设置.
这个环境变量通常设置为项目的settings.py文件的Python路径, 比如MyDjango.settings.
* 2. 项目启动: 当启动一个Django项目(比如通过manage.py命令)时, Django会检查DJANGO_SETTINGS_MODULE环境变量.
* 3. 动态导入: Django会根据DJANGO_SETTINGS_MODULE的值动态地导入项目设置模块.
这个导入过程不是通过Python标准的导入机制完成的, 而是Django内部使用的一个特殊机制.
* 4. 创建settings对象: 一旦项目设置模块被导入, Django会读取该模块中的配置信息, 并创建一个settings对象来存储这些信息.
这个settings对象是一个特殊的对象, 它允许通过属性访问配置信息(比如settings.DEBUG或settings.DATABASES).
* 5. 全局可用: 最后, Django将这个settings对象存储在django.conf包中, 以便在你的Django项目中全局可用.
这就是为什么你可以通过from django.conf import settings来访问它.
from django.urls import path
from .views import *
urlpatterns = [
path('', index_view, name='index'),
]

最后在项目应用index的views.py和模板文件index.html中分别编写视图函数index_view()和表单的模板语法, 详细代码如下:
from django.shortcuts import render
from django.http import HttpResponse
from .form import PersonInfoForm
from .models import PersonInfo, CertificateInfo
from django.conf import settings
import os
def index_view(request):
if request.method == 'GET':
pk = request.GET.get('id', '')
if pk:
i = PersonInfo.objects.filter(id=pk).first()
p = PersonInfoForm(initial=i)
else:
p = PersonInfoForm()
return render(request, 'index.html', locals())
else:
p = PersonInfoForm(data=request.POST, files=request.FILES)
if p.is_valid():
name = p.cleaned_data['name']
result = PersonInfo.objects.filter(name=name)
if not result:
p = p.save()
pk = p.id
for f in request.FILES.getlist('certificate'):
f.name = f'{pk}.'.join(f.name.split('.'))
d = dict(person_info_id=pk, certificate=f)
CertificateInfo.objects.create(**d)
return HttpResponse('新增成功!')
else:
age = p.cleaned_data['age']
d = dict(name=name, age=age)
result.update(**d)
pk = result.first().id
for c in CertificateInfo.objects.filter(person_info_id=pk):
fn = c.certificate.name
os.remove(os.path.join(settings.MEDIA_ROOT, fn))
c.delete()
for f in request.FILES.getlist('certificate'):
f.name = f'{pk}.'.join(f.name.split('.'))
d = dict(person_info_id=pk, certificate=f)
CertificateInfo.objects.create(**d)
return HttpResponse('修改成功!')
else:
error_msg = p.errors.as_json()
print(error_msg)
return render(request, 'index.html', locals())

如果表单包含文件上传字段(如<input type="file">), 则这些文件将不会包含在request.POST中, 而是包含在request.FILES中.
request.FILES是一个类似于字典的对象, 其键是文件字段的名称, 值是上传文件的文件对象.
将request.FILES传递给PersonInfoForm的files参数意味着我们也在告诉表单类: '使用这些文件来填充和验证文件上传字段(如果有的话).'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户信息</title>
</head>
<body>
{# 校验的字段如果不通过会将错误信息存放在errors属性中 #}
{% if p.errors %}
<p>数据出错啦, 错误信息: {{ p.errors }}</p>
{% else %}
<form action="" method="post", enctype="multipart/form-data">
{% csrf_token %}
<ul>
<li>姓名: {{ p.name }}</li>
<li>年龄: {{ p.age }}</li>
<li>证件: {{ p.certificate }}</li>
</ul>
<input type="submit" name="add" value="提交">
</form>
{% endif %}
</body>
</html>

视图函数indexView()根据GET和POST请求编写了不同的处理过程, 整个函数的业务逻辑说明如下:
(1) 当用户以GET请求访问路由index时, 相当于在浏览器中访问路由index,
视图函数indexView()判断当前是否有请求参数id, 如果存在请求参数id,
则在模型PersonInfo中查询对应的用户数据, 并将数据初始化到表单类PersonInfoForm, 网页上就会显示当前用户的信息;
如果没有请求参数id, 浏览器就会显示空白的网页表单.
(2) 当用户在网页表单上填写数据后并单击'提交'按钮, 浏览器向Django发送POST请求, 由视图函数indexView()处理.
表单数据由表单类PersonInfoForm接收, 根据表单字段name查询模型PersonInfo,
如果数据不存在, 则将数据保存在模型PersonInfo所对应的数据表,
然后从request.FILES.getlist('certificate')中获取用户上存的所有文件并执行遍历操作,
每次遍历是把每个文件信息分别保存到模型CertificateInfo, 模型字段certificate负责记录文件路径和保存文件.
(3) 视图函数indexView()在处理POST请求中, 如果表单数据已存在数据表中, 程序修改模型PersonInfo的数据,
然后从模型CertificateInfo中删除当前用户的数据, 最后从request.FILES.getlist('certificate')中获取用户上存的所有文件,
写入模型CertificateInfo和保存文件.
模板文件index.html在编写表单的时候, 必须将表单的属性enctype设为multipart/form-data, 该属性值是设置文件上存功能,
而多文件批量上存是由表单字段certificate的属性widget设置(即attrs={'multiple': True}).
运行MyDjango, 在浏览器上访问路由index, 可以看到网页表单生成了3个表单字段, 单击'选择文件'按钮,
允许选择多个文件上存, 如图8-17所示.

图8-17 上存文件
综合上述, 使用Django实现多文件批量上存的过程如下:
(1) 定义模型, 模型的某个字段为FileField类型, 用于记录文件上存的信息, 如有需要还可以重写内置文件系统类FileSystemStorage.
(2) 根据模型定义表单类, 文件上存的模型字段映射的表单字段也是FileField类型, 并且设置表单字段的属性widget,
属性值为forms.ClearableFileInput(attrs={'multiple': True})), 其中attrs={'multiple': True}是支持多文件上存功能;
如有需要还可以设置属性allow_empty_file等于True, 该属性支持上存空白内容的文件.
(3) 在视图函数中, 使用已定义的表单类创建表单对象, 当收到POST请求后, 网页表单的数据由表单类接收,
接收方式必须在表单类中设置参数data和files, 参数data是除文件上存之外的表单字段, 参数files代表用户上存的所有文件.
(4) 在模板文件中编写网页表单, 必须将表单的enctype属性设为multipart/form-data, 否则文件无法实现上存功能.
(5) 一个网页表单可以创建多个文件上存控件, 视图函数的request.FILES是获取所有文件上存控件的所有文件信息.
如果要获取某一个文件上存控件的文件信息, 可以调用getlist()方法获取, 比如网页中有两个文件上存控件,
分别为certificate和portrait, 如果只获取属性name=certificate的所有文件信息,
实现代码为request.FILES.getlist('certificate').
访问用户证件信息:
http://127.0.0.1:8000/media/images/avatar1.png
http://127.0.0.1:8000/media/images/id1.png

8.10 本章小结
表单是搜集用户数据信息的各种表单元素的集合, 其作用是实现网页上的数据交互,
比如用户在网站输入数据信息, 然后提交到网站服务器端进行处理(如数据录入和用户登录注册等).
网页表单是Web开发的一项基本功能, Django的表单功能由Form类实现, 主要分为两种: django.forms.Form和django.forms.ModelForm.
前者是一个基础的表单功能, 后者是在前者的基础上结合模型所生成的数据表单.
一个完整的表单主要由4部分组成: 提交地址, 请求方式, 元素控件和提交按钮, 分别说明如下:
● 提交地址(form标签的action属性)用于设置用户提交的表单数据应由哪个路由接收和处理.
当用户向服务器提交数据时, 若属性action为空, 则提交的数据应由当前的路由来接收和处理,
否则网页会跳转到属性action所指向的路由地址.
● 请求方式用于设置表单的提交方式, 通常是GET请求或POST请求, 由form标签的属性method决定.
● 元素控件是供用户输入数据信息的输入框, 由HTML的<input>控件实现,
控件属性type用于设置输入框的类型, 常用的输入框类型有文本框, 下拉框和复选框等.
● 提交按钮供用户提交数据到服务器, 该按钮也是由HTML的<input>控件实现的.
但该按钮具有一定的特殊性, 因此不归纳到元素控件的范围内.
表单类Form和模型实现数据交互需要注意以下事项:
● 表单字段最好与模型字段相同, 否则两者在进行数据交互时, 必须将两者的字段进行转化.
● 使用同一个表单并且需要多次实例化表单时, 除了参数initial和data的数据不同之外,
其他参数设置必须相同, 否则无法接收上一个表单对象所传递的数据信息.
● 参数initial是表单实例化的初始化数据, 它只适用于模型数据传递给表单, 再由表单显示在网页上;
参数data是在表单实例化之后, 再将数据传递给实例化对象, 只适用于表单接收HTTP请求的请求参数.
● 参数prefix设置表单的控件属性name和id的值, 若在一个网页里使用同一个表单类生成多个不同的网页表单,
参数prefix可区分每个网页表单, 则在接收或设置某个表单数据时不会与其他的表单数据混淆.
模型表单ModelForm实现数据保存只有save()和save_m2m()两种方法.
使用save()保存数据时, 参数commit的值会影响数据的保存方式.
如果参数commit为True, 就直接将表单数据保存到数据库;
如果参数commit为False, 就会生成一个数据库对象, 然后可以对该对象进行增, 删, 改, 查等数据操作, 再将修改后的数据保存到数据库.
值得注意的是, save()只适合将数据保存在非多对多关系的数据表中, 而save_m2m()只适合将数据保存在多对多关系的数据表中.
如果网页表单是由Django生成, 并且同一个表单类实例化多个表单对象,
在实例化过程中可以设置参数prefix,该参数是对每个表单对象进行命名, Django通过表单对象的命名进行区分和管理.
如果表单所有功能都是由Django实现(排除Ajax异步请求这类实现方式), 那么多个按钮所触发的功能应在同一个视图函数中实现,
视图函数可以根据当前请求进行判断, 分辨出当前请求是由哪一个按钮触发.
为了减少'提交'按钮的单击次数, 可以将多条数据在一个表单中填写, 只要表单能生成多个相同的表单字段,
当单击'提交'按钮时, 系统将会对表单数据执行批量操作, 在数据库中保存多条数据.
使用Django实现数据的批量处理, 可以在表单类实例化的时候重新定义表单的工厂函数,
表单的实例化对象在生成网页表单的时候, Django自动为每个表单字段创建多个网页元素.
如果网页表单是由Django的表单类创建, 并且表单设有文件上存功能, 那么可以在表单类中设置表单字段为forms.FileField类型;
或者在模型中设置模型字段models.FileField, 通过模型映射到表单类ModelForm, 从而生成文件上存的功能控件.