Django(7)之表单与模型

Django(7)之表单与模型

微信搜索 zze_coding 或扫描 👉 二维码关注我的微信公众号获取更多资源推送:

用户表单是 Web 开发的一项基本功能,Django 的表单功能由 Form 类实现,主要分为两种:django.forms.Formdjango.forms.ModelForm。前者是一个基础的表单功能,后者是在前者的基础上结合模型所生成的数据表单。

初识表单

传统的表单生成方式是在模板文件中编写 HTML 代码实现,在 HTML 语言中,表单由 <form> 标签实现。一个标准的表单格式大致如下:

<!DOCTYPE html>
<html lang="en">
<body>
<form action="" method="post">
    <table>
        <tr>
            <td>First name</td>
            <td><input type="text" name="firstname" value="Mickey"></td>
        </tr>
        <tr>
            <td>Last name</td>
            <td><input type="text" name="lastname" value="Mouse"></td>
        </tr>
        <tr>
            <td colspan="2" style="text-align: center"><input type="submit" value="Submit"></td>
        </tr>
    </table>
</form>
</body>
</html>

一个完整的表单主要有 4 个组成部分:提交地址、请求方式、元素控件和提交按钮。其说明如下:

  • 提交地址用于设置用户提交的表单数据应由哪个 URL 接收和处理,由控件 <form> 的属性 action 决定。当用户向服务器提交数据时,若属性 action 为空,则提交的数据应由当前的 URL 来接收和处理,否则网页会跳转到属性 action 所指向的 URL 地址。
  • 请求方式用于设置表单的提交方式,通常是 GET 请求或 POST 请求,由控件 <form> 的属性 method 决定。
  • 元素控件是供用户输入数据信息的输入框。由 HTML 的 <input> 控件实现,其控件属性 type 用于设置输入框的类型,常用的输入框类型有文本框、下拉框和复选框等。
  • 提交按钮供用户提交数据到服务器,该按钮也是由 HTML 的 <input> 控件实现的。但该按钮具有一定的特殊性,因此不归纳到元素控件的范围内。

在模板文件中,直接编写表单是一种较为简单的实现方式,如果表单元素较多,会在无形之中增加模板的代码量,这样对日后的维护和更新造成极大的不便。为了简化表单的实现过程和提高表单的灵活性,Django 提供了完善的表单功能。在讲解表单使用方法之前,我们对 test01 的目录做了细微的调整,如下图所示。

image.png

test01index 中添加了空白文件 form.py,该文件主要用于编写表单的实现功能,文件夹可自行命名;同时在文件夹 templates 中添加模板文件 data_form.html,该文件用于将表单的数据显示到网页上。最后在文件 form.pyviews.pydata_form.html 中分别添加以下代码:

# form.py 定义 ProductForm 表单对象
from django import forms
from .models import *


class ProductForm(forms.Form):
    name = forms.CharField(max_length=20, label='名字')
    weight = forms.CharField(max_length=50, label='重量')
    size = forms.CharField(max_length=50, label='尺寸')
    # 设置下拉框的值
    choices_list = [(i + 1, v['type_name']) for i, v in enumerate(Type.objects.values('type_name'))]
    type = forms.ChoiceField(choices=choices_list, label='产品类型')
# views.py 代码
from django.http import HttpResponse
from django.shortcuts import render
from .form import *


def index(request):
    product = ProductForm()
    return render(request, 'data_form.html', locals())
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
{% if product.errors %}
    <p>数据出错啦,错误信息:{{ product.errors }}</p>
{% else %}
    <form action="" method="post">
        {% csrf_token %}
        <table>
            {{ product.as_table }}
        </table>
        <input type="submit" value="提交">
    </form>
{% endif %}
</body>
</html>

上述代码演示了 Django 内置表单功能的使用方法,主要由 form.pyviews.pydata_form.html 共同实现,实现说明如下:

  1. form.py 中定义表单 ProductForm,表单以类的形式表示。在表单中定义了不同类型的类属性,这些属性在表单中称为表单字段,每个表单字段代表 HTML 里的一个控件,这是表单的基本组成单位。
  2. views.py 中导入 form.py 所定义的 ProductForm 类,在视图函数 index 中对 ProductForm 实例化生成对象 product,再将对象 product 传递给模板 data_form.html
  3. 模板 data_form.html 将对象 product 以 HTML 的 <table> 的形式展现在网页上,如下图所示。

image.png

表单的定义

从上节的例子发现,Django 的表单功能主要是通过定义表单类,再由类的实例化生成 HTML 的表单元素控件,这样可以在模板中减少 HTML 的硬编码。每个 HTML 的表单元素控件由表单字段来决定。

以表单类 ProductForm 的字段 name 为例:

name = forms.CharField(max_length=20, label='名字')

其生成的 HTML 元素控件如下:

<tr><th><label for="id_name">名字:</label></th><td><input type="text" name="name" maxlength="20" required id="id_name" /></td></tr>

从表单字段转换 HTML 元素控件可以发现:

  • 字段 name 的参数 label 将转换成HTML 的标签
  • 字段 nameforms.CharField 类型转换成 HTML 的 <input type="text"> 控件,标签 <input> 是一个输入框控件,type="text" 代表当前输入框为文本输入框,参数 type 用于设置输入框的类型。
  • 字段 name 的命名转换成 <input> 控件的参数 name,表单字段的参数 max_length 将转换成 <input> 控件的参数 required maxlength

除了上述表单字段外,Django 还提供多种内置的表单字段,如下表所示:

字段说明
BooleanField复选框,如果字段带有 required=True,复选框被勾选上
CharField文本框,参数 max_lengthmin_length 分别设置输入长度
ChoiceField下拉框,参数 choices 设置数据内容
TypedChoiceFieldChoiceField 相似,但比 ChoiceField 多出参数 coerceempty_value,分别代表强制转换数据类型和用于表示空值,默认为空字符串
DateField文本框,具有验证日期格式的功能,参数 input_formats 设置日期格式
EmailField文本框,验证输入数据是否为合法的邮箱地址。可选参数为 max_lengthmin_length
FileField文件上传功能,参数 max_lengthallow_empty_file 分别用于设置文件名的最大长度和文件内容是否为空
FilePathField在特定的目录选择并上传文件,参数 path 是必需参数,参数 recursivematchallow_filesallow_folders 为可选参数
FloatField验证数据是否为浮点数
ImageField验证文件是否为 Pillow 库可识别的图像格式
IntegerField验证数据是否为整型
GenericIPAddressField验证数据是否为有效数值
SlugField验证数据是否只包括字母、数字、下画线及连字符
TimeField验证数据是否为 datetime.time 或指定特定时间格式的字符串
URLField验证数据是否为有效的 URL 地址

从上表可以看出,表单字段除了转换 HTML 控件之外,还具有一定的数据格式规范,数据格式规范主要由字段类型和字段参数共同实现。每个不同类型的表单字段都有一些自己特殊的参数,但每个表单字段都具有下表所示的共同参数。

参数说明
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,是否可以编辑
label_suffixLabel 内容后缀,在 Label 后添加内容

根据上表的参数说明,我们对 form.py 的表单 ProductForm 的字段进行优化,代码如下:

# form.py 定义 ProductForm 表单对象
from django import forms
from .models import *
from django.core.exceptions import ValidationError


# 自定义数据验证函数
def weight_validate(value):
    if not str(value).isdigit():
        raise ValidationError('请输入正确的重量')


# 表单
class ProductForm(forms.Form):
    # 设置错误信息并设置样式
    name = forms.CharField(max_length=20, label='名字', widget=forms.widgets.TextInput(attrs={'class': 'c1'}),
                           error_messages={'required': '名字不能为空'})
    # 使用自定义数据验证函数
    weight = forms.CharField(max_length=50, label='重量', validators=[weight_validate])
    size = forms.CharField(max_length=50, label='尺寸')
    # 获取数据库数据来填充下拉框
    choices_list = [(i + 1, v['type_name']) for i, v in enumerate(Type.objects.values('type_name'))]
    # 设置 CSS 样式
    type = forms.ChoiceField(widget=forms.widgets.Select(attrs={'class': 'type', 'size': 4}), choices=choices_list,
                             label='产品类型')

优化的代码分别使用了参数 widgetlabelerror_messagesvalidators,这 4 个参数是实际开发中常用的参数,参数说明如下:

  • 参数 widget 是一个 forms.widgets 对象,其作用是设置表单字段的 CSS 样式。widget 的对象类型应与表单字段类型相符合,如果字段类型为 CharFieldwidget 的对象类型为 forms.widgets.TextInput ,这两者的含义与作用是一致的,都代表文本输入框;如果字段类型为 ChoiceFieldwidget 的对象类型为 forms.widgets.TextInput 相组合,前者是下拉选择框,后者是文本输入框,那么在网页上就会优先显示为文本输入框。
  • 参数 label 会转换成 HTML 的标签 <label>,作用是对控件的描述和命名,方便用户理解控件的作用与含义。
  • 参数 error_messages 用于设置数据验证失败后的错误信息,参数值以字典的形式表示,字典的键为表单字段的参数名,字典的值为错误信息。
  • 参数 validators 用于自定义数据验证函数,当用户提交表单数据后,首先执行自定义的验证函数,当数据验证失败后,会抛出自定义的异常信息。所以,字段中设置了参数 validators,就无须设置参数 error_messages,因为数据验证已由参数 validators 优先处理。

为了进一步验证优化后的表单是否正确运行,我们对 views.py 的视图函数 index 代码进行优化,代码如下:

# views.py 代码
from django.http import HttpResponse
from django.shortcuts import render
from .form import *


def index(request):
    # GET 请求
    if request.method == 'GET':
        product = ProductForm()
        return render(request, 'data_form.html', locals())
    # POST 请求
    else:
        product = ProductForm(request.POST)
        if product.is_valid():
            # 获取网页控件 name 的数据
            # 方法一
            name = product['name']
            # 方法二
            # cleaned_data 将控件 name 的数据进行清洗,转换成 Python 数据类型
            cname = product.cleaned_data['name']
            return HttpResponse('提交成功')
        else:
            # 将错误信息输出,error_msg 是将错误信息以 json 格式输出
            error_msg = product.errors.as_json()
            print(error_msg)
            return render(request, 'data_form.html', locals())

上述代码是 views.py 的视图函数 index 进行优化,优化说明如下:

  • 首先判断用户的请求方式,不同的请求方式执行不同的程序处理。函数 index 分别对 GET 和 POST 请求做了不同的响应处理。
  • 用户在浏览器中访问 http://127.0.0.1:8000/,等同于向 test01 发送一个 GET 请求,函数 index 将表单 ProductForm 实例化并传递给模板,由模板引擎生成 HTML 表单返回给用户。
  • 当用户在网页上输入相关信息后单击“提交”按钮,等同于向 test01 发送一个 POST 请求,函数 index 首先获取表单数据对象 product,然后由 is_valid()对数据对象 product 进行数据验证。
  • 如果验证成功,可以使用 product['name']product.cleaned_data['name'] 方法来获取用户在某个控件上的输入值。只要将获取到的输入值和模型相互使用,就可以实现表单与模型的信息交互。
  • 如果验证失败,使用 errors.as_json() 方法获取验证失败的信息,然后将验证失败的信息通过模板返回给用户。

从上述例子发现,模板 data_form.html 的表单是使用 HTML 的 <table> 标签展现在网页上,除此之外,表单还可以使用其他 HTML 标签展现,只需将模板 data_form.html 的对象 product 使用以下方法即可生成其他 HTML 标签:

# 将表单生成 HTML 的 ul 标签
{{ product.as_ul }}
# 将表单生成 HTML 的 p 标签
{{ product.as_p }}
# 生成单个 HTML 元素控件
{{ product.type }}
# 获取表单字段的参数 label 的属性值
{{ product.type.label }}

模型与表单

我们知道 Django 的表单分为两种:django.forms.Formdjango.forms.ModelForm。前者是一个基础的表单功能,后者是在前者的基础上结合模型所生成的数据表单。数据表单是将模型的字段转换成表单的字段,再从表单的字段生成 HTML 的元素控件,这是日常开发中常用的表单之一。本节通过讲解表单功能模块 ModelForm 实现表单数据与模型数据之间的交互开发。
首先在文件 form.py 中定义表单 ProductModelForm,该表单继承自父类 forms.ModelForm。其代码如下:

# form.py 定义 ProductModelForm 表单对象
from django import forms
from .models import *
from django.core.exceptions import ValidationError


# 数据库表单
class ProductModelForm(forms.ModelForm):
    # 添加模型外的表单字段
    productId = forms.CharField(max_length=20, label='产品序号')

    # 模型与表单设置
    class Meta:
        # 绑定模型
        model = Product
        # fields 属性用于设置转换字段,'__all__' 是将全部模型字段转换成表单字段
        # fields = '__all__'
        fields = ['name', 'weight', 'size', 'type']
        # exclude 用于禁止模型字段转换表单字段
        exclude = []
        # labels 设置 HTML 元素控件的 label 标签
        labels = {
            'name': '产品名称',
            'weight': '重量',
            'size': '尺寸',
            'type': '产品类型'
        }
        # 定义 widgets,设置表单字段的 CSS 样式
        widgets = {
            'name': forms.widgets.TextInput(attrs={'class': 'c1'})
        }
        # 定义字段的类型,一般情况下模型的字段会自动转换成表单字段
        field_classes = {
            'name': forms.CharField
        }
        # 帮助提示信息
        help_texts = {
            'name': ''
        }
        # 自定义错误信息
        error_messages = {
            # __all__ 设置全部错误信息
            '__all__': {'required': '请输入内容', 'invalid': '请检查输入内容'},
            # 设置某个字段的错误信息
            'weight': {'required': '请输出重量数值', 'invalid': '请检查数值是否正确'}
        }

    # 自定义表单字段 weight 的数据清洗
    def clean_weight(self):
        # 获取字段 weight 的值
        data = self.cleaned_data['weight']
        return data + 'g'

上述代码中,表单类 ProductModelForm 可分为三大部分:添加模型外的表单字段、模型与表单设置和自定义表单字段 weight 的数据清洗函数,说明如下:

  • 添加模型外的表单字段是在模型已有的字段下添加额外的表单字段。
  • 模型与表单设置是将模型的字段转换成表单字段,由类 Meta 的属性实现两者的字段转换。
  • 自定义表单字段 weight 的数据清洗函数只适用于字段 weight 的数据清洗。

综上所述,模型字段转换成表单字段主要在类 Meta 中实现。在类 Meta 中,其属性说明如下表所示:

属性说明
Model必需属性,用于绑定 Model 对象
Fields必需属性,设置模型内哪些字段转换成表单字段。属性值为 _all_ 代表整个模型的字段,若设置一个或多个,使用列表或元组的数据格式表示, 列表或元组里的元素是模型的字段名
Exclude可选属性,与 fields 相反,禁止模型内哪些字段转换成表单字段。属性值以列表或元组表示,若设置了该属性,则属性 fields 可以不用设置
Labels可选属性,设置表单字段里的参数 label。属性值以字典表示,字典里的键是模型的字段
Widgets可选属性,设置表单字段里的参数 widget
field_classes可选属性,将模型的字段类型重新定义为表单字段类型,默认情况下,模型字段类型会自动转换为表单字段类型
help_texts可选属性,设置表单字段里的参数 help_text
error_messages可选属性,设置表单字段里的参数 error_messages

值得注意的是,一些较为特殊的模型字段在转换表单时会有不同的处理方式。例如模型字段的类型为 AutoField,该字段在表单中不存在对应的表单字段;模型字段类型为 ForeignKeyManyToManyField,在表单中对应的表单字段为 ModelChoiceFieldModelMultipleChoiceField
自定义表单字段 weight 的数据清洗函数是在视图函数中使用 cleaned_data 方法时,首先判断当前清洗的表单字段是否已定义数据清洗函数。例如上述的 clean_weight
数,在清洗表单字段 weight 的数据时会自动执行该自定义函数。在自定义数据清洗函数时,必须以 clean_字段名 的格式作为函数名,而且函数必须有 return 返回值。如果在函数中设置 ValidationError 了异常抛出,那么该函数可视为带有数据验证的清洗函数。

数据表单的使用

上节通过定义表单类 ProductModelForm 将模型 Product 与表单相互结合起来,本节将通过表单类 ProductModelForm 在网页上生成 HTML 表单。我们沿用前面的模板 data_form.html,在 test01urls.pyviews.py 中分别定义新的 URL 地址和视图函数,代码如下:

# views.py 代码
from django.http import HttpResponse
from django.shortcuts import render
from .form import *


# views.py 的视图函数 model_index
def model_index(request, id):
    if request.method == 'GET':
        instance = Product.objects.filter(id=id)
        # 判断是数据是否存在
        if instance:
            product = ProductModelForm(instance=instance[0])
        else:
            product = ProductModelForm()
        return render(request, 'data_form.html', locals())
    else:
        product = ProductModelForm(request.POST)
        if product.is_valid():
            # 获取 weight 的数据,并通过 clean_weight 进行清洗,转换成 Python 数据类型
            weight = product.cleaned_data['weight']
            # 数据保存方法一
            # 直接将数据保存到数据库
            # product.save()
            # 数据保存方法二
            # save 方法设置 commit=False,将生成数据库对象 product_db,然后对该对象的属性值修改并保存
            product_db = product.save(commit=False)
            product_db.name = 'iPhone 12'
            product_db.save()
            # 数据保存方法三
            # save_m2m() 方法用于保存 ManyToMany 的数据类型
            # product.save_m2m()
            return HttpResponse('提交成功!weight 清洗后的数据为:' + weight)
        else:
            # 将错误信息输出,error_msg 是将错误信息以 json 格式输出
            error_msg = product.errors.as_json()
            print(error_msg)
            return render(request, 'data_form.html', locals())


def index(request):
    # GET 请求
    if request.method == 'GET':
        product = ProductForm()
        return render(request, 'data_form.html', locals())
    # POST 请求
    else:
        product = ProductForm(request.POST)
        if product.is_valid():
            # 获取网页控件 name 的数据
            # 方法一
            name = product['name']
            # 方法二
            # cleaned_data 将控件 name 的数据进行清洗,转换成 Python 数据类型
            cname = product.cleaned_data['name']
            return HttpResponse('提交成功')
        else:
            # 将错误信息输出,error_msg 是将错误信息以 json 格式输出
            error_msg = product.errors.as_json()
            print(error_msg)
            return render(request, 'data_form.html', locals())

函数 model_index 的处理逻辑说明如下:

  • 首先判断用户的请求方式,不同的请求方式执行不同的处理程序。代码分别对 GET 和 POST 请求做了不同的响应处理。
  • 若当前请求为 GET 请求,函数根据 URL 传递的变量 id 来查找模型 Product 的数据,如果数据存在,模型的数据以参数的形式传递给表单 ProductModelForm 的参数 instance,在生成网页时,模型数据会填充到对应的元素控件上,如下图所示。
  • 若当前请求为 POST 请求,函数首先对表单数据进行验证,若验证失败,则返回失败信息;若验证成功,则使用 cleaned_data 方法对字段 weight 进行清洗,字段 weight 清洗由自定义函数 clean_weight 完成,最后将表单数据保存到数据库,保存数据有三种方式,具体说明可看代码注释,运行结束如下图所示。

image.png

我们在 views.py 中实现了表单 ProductModelForm 的使用。在实现过程中,你可能会产生以下疑问:

  • 当请求方式 GET 的时候,设置表单 ProductModelForm 的参数 instance 相当于为表单进行初始化,那么表单的初始化有哪些方法?
  • 上图的下拉框数据是一个模型 Type 对象,如何将模型 Type 的字段 type_name 的数据在下拉框中展示呢?
  • 将表单数据保存到数据库中,三种保存方式有什么区别?

针对疑问一,表单的初始化有 4 种方法,每一种方法都有自己的适用范围:

  1. 在视图函数中对表单类进行实例化时,可以设置实例化对象的参数 initial。例如 ProductModelForm(initial={'name':value}),参数值以字典的格式表示,字典的键为表单的字段名,这种方法适用于所有表单类。
  2. 在表单类中进行实例化时,如果初始化的数据是一个模型对象的数据,可以设置参数 instance,这种方法只适用于 ModelForm,如 ProductModelForm(instance=instance)
  3. 定义表单字段时,可以对表单字段设置初始化参数 initial,此方法不适用于 ModelForm,如 name=forms.CharField(initial=value)
  4. 重写表单类的初始化函数 __init__(),适用于所有表单类,如在初始化函数 __init_() 中设置 self.fields[ 'name ' ].initial=value

上述四种初始化方法中,我们以方法 3 和方法 4 为例,在定义表单类时设置表单的初始化,代码如下:

# 数据库表单
class ProductModelForm(forms.ModelForm):
    # 方法四:重写 ProductModelForm 类的初始函数 __init__
    def __init__(self, *args, **kwargs):
        super(ProductModelForm, self).__init__(*args, **kwargs)
        self.fields['name'].initial = '我的手机'

    # 方法三:定义表单字段时,设置参数 initial
    productId = forms.CharField(max_length=20, label='产品序号', initial='NO1')

重启 test01 项目,在浏览器上访问 http://127.0.0.1:8000/111.html,运行结果如下所示:

image.png

解决疑问一的表单初始化问题后,我们接着分析疑问二可以发现,下拉框的数据是一个模型 Type 对象,而下拉框是由模型 Product 的外键 type 所生成的,外键 type 指向模型 Type。因此,要解决下拉框的数据问题,可以从定义模型或者定义表单这两方面解决。
定义模型是在定义模型 Type 时,设置该模型的返回值。当有外键指向模型 Type 时,模型 Type 会将返回值返回给外键。在模型中通过重写 __str__ 函数可以设置模型的返回值,代码如下:

# models.py
class Type(models.Model):
    id = models.AutoField(primary_key=True)
    type_name = models.CharField(max_length=20)

    # 设置返回值,若不设置,则默认返回 Type 对象
    def __str__(self):
        return self.type_name

如果存在多个下拉框,而且每个下拉框的数据分别取同一个模型的不同字段,那么重写 __str__ 函数可能不太可行。遇到这种情况,可以在定义表单类的时候重写初始化函数 __init_(),代码如下:

class ProductModelForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(ProductModelForm, self).__init__(*args, **kwargs)
        # 设置下拉框的数据
        type_obj = Type.objects.values('type_name')
        choices_list = [(i + 1, v['type_name']) for i, v in enumerate(type_obj)]
        self.fields['type'].choices = choices_list
        # 初始化字段 name
        self.fields['name'].initial = '我的手机'

最后对于疑问三所提及的数据保存,实质上数据保存只有 save()save_m2m() 方法实现,在上述代码中所演示的三种保存方式,前两者是 save() 的参数 commit 的不同而导致保存方式有所不同。如果参数 commitTrue,直接将表单数据保存到数据库;如果参数 commitFalse,这时将生成一个数据库对象,然后可以对该对象进行增删改查等数据操作,再将修改后的数据保存到数据库中。
值得注意的是 save() 只适合将数据保存在非多对多数据关系的数据表,而 save_m2m() 只适合将数据保存在多对多数据关系的数据表。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://www.zze.xyz/archives/django7.html

Buy me a cup of coffee ☕.