Django forms组件

【一】引入

【1】实现登陆验证功能

(1)需求分析

  • 登陆验证需要前后端交互,采用form表单提交数据
  • 对数据进行校验
    • 用户名必须以英文大写字母开头
    • 密码必须大于三位
  • 反馈给用户错误的信息
    • 除了反馈错误的信息
    • 还有保留原始输入内容

(2)后端代码

  • 使用user_info_dict字典
    • 每次刷新存储
    • 存储前端发送的信息
    • 存储后端进行验证的信息
def home(request):
    # 每次后刷新这个信息字典
    user_info_dict = dict()
    if request.method == "POST":
        # 获取并保存信息
        username = request.POST.get("username")
        password = request.POST.get("password")
        user_info_dict["username"] = username
        user_info_dict["password"] = password
        # 信息验证
        if not username[0].isupper():
            user_info_dict["name_message"] = "用户名必须以英文大写字母开头"
        if len(password) <= 3:
            user_info_dict["pwd_message"] = "密码必须大于三位数"
    return render(request, 'home.html', locals())

(3)前端代码

  • 通过后端发送的user_info_dict
    • 可以知道上次输入的信息
    • 可以知道后端处理的结果
  • 使用行内标签span
    • 没有错误不显示
    • 有错误显示错误信息
<form action="" method="post">
    <p><lable>用户名:
        <input type="text" name="username"
               value="{{ user_info_dict.username }}">lable>
        <span style="color: red">{{ user_info_dict.name_message }}span>
    p>
        <p><lable>密码:
        <input type="password" name="password"
               value="{{ user_info_dict.password }}">lable>
        <span style="color: red">{{ user_info_dict.pwd_message }}span>
    p>
    <input type="submit" class="btn btn-success">
form>

【2】功能小结

  • 我们需要的技术点
    • 创建布局前端、渲染HTML页面
      • 主要是输入框和错误信息的展示布局
    • 后端进行内容校验
      • 后端进行逻辑处理
    • 前端还需要显示上次的输入内容
      • 使用user_dict_info保存所有信息
  • 接下来的form组件姐可以更好的处理上述内容

【二】后端部分

【1】基础使用

(1)简单示例

  • 创建表单类
    • 虽然和模型层的数据库表很像
    • 但是他是创建在视图层的
from django import forms


class MyForm(forms.Form):
    # 字符串类型 最长10位,最短4位
    username = forms.CharField(max_length=10, min_length=4)
    # 必须满足邮箱格式 [email protected]
    email = forms.EmailField()

(2)测试环境

  • 在模型层的学习中

    • 我们在每个应用的下面都可以开一个tests.py文件进行测试
  • 实际上还可以在

    • pycharm底部的Python Console中进行测试
    • 在这不需要我们搭建测试环境
  • 导入视图层文件

from app01 import views
  • 创建表单组件实例
    • 需要传递字典类型
form_obj = views.MyForm({"username":"bruce", "email":"bruce"})
  • 检验是否合法is_valid()
    • 显然不合理,输入的邮箱不满足邮箱格式
from_obj.is_valid()
# False
  • 组件检查的字段changed_data
form_obj.changed_data
# ['username', 'email']
  • 查看不合法的数据errors
from_obj.errors
# {'email': ['Enter a valid email address.']}
  • 多传值是否报错
    • 不会报错
    • 说明是根据创建的表单类的字段进行检查
    • 没有在表单中的属性不会进行检查
form_data = views.MyForm({"username":"Bruce", "email":"[email protected]", "age":18})
form_data.is_valid()
# True
form_data.errors
# {}
  • 少传值是否报错
    • 会报错说明默认情况下
    • 每个类的属性值都不能为空
form_data = views.MyForm({"username":"bruce"})
form_data.is_valid()
# False
form_data.errors
# {'email': ['This field is required.']}

【2】常用表单组件的属性

(1)长度要求length

class MyFrom(forms.Form):
    username = forms.CharField(max_length=10, min_length=4)

(2)初始值initial

  • 和前端表单中的value一样
class MyFrom(forms.Form):
    # 初始值是Bruce
    username = forms.CharField(initial="Bruce")

(3)标签内容label

  • 前端页面中的label中的内容
    • 就是输入框前面的内容
  • label_suffix
    • label参数后缀默认是:冒号
    • 这个可以改成执行的字符
class MyFrom(forms.Form):
    # label标签内容
    username = forms.CharField(initial="用户名:")
    captcha_answer = forms.IntegerField(label='2 + 2', label_suffix=' =')

(4)必填字段required

  • 和模型层中的null=True同理
    • 在这里默认是True,即不能为空值
class MyFrom(forms.Form):
    # 不能为空
    username = forms.CharField(required=True)

(5)提示信息help_text

  • 前端需要.help_text才可以显示
class MyForm(forms.Form):
    username = forms.CharField(
        help_text="请输入3-8个字符的用户名",
    )

(5)错误信息error_message

  • 和上述例子中的span标签内的类型一样
  • 是一个字典形式,不同的错误对应不同的提示信息
class MyFrom(forms.Form):
    username = forms.CharField(
        max_length=10, 
        min_length=4,
        error_message ={
            "required":"输入框不能为空",
            "max_length":"输入内容不能超过10个字符",
            "min_length":"输入内容不能少于4个字符"
        }
    )

(6)自定义验证器validators

  • 自定义一个手机号码检查验证器
import re
from django import forms
from django.core.exceptions import ValidationError


def mobile_validate(value):
    mobile_re = re.compile(r"^0?(13|14|15|17|18|19)[0-9]{9}$")
    if not mobile_re.match(value):
        raise ValidationError("手机号格式错误")


class MyForm(forms.Form):
    phone = forms.CharField(
        validators=[mobile_validate, ],
        error_messages={"required": "手机号不能为空"}
    )
# 另一个方式
from django import forms
from django.core.validators import RegexValidator

class MyForm(forms.Form):
    phone = forms.CharField(
        validators=[
            RegexValidator(r"^[0-9]+$", "只能输入数字"),
            RegexValidator(r"^123[0-9]+$", "只接受123开头的电话")
        ]
    )

(7)其他

  • disbaled:是否禁用
  • readonly:是否只读

【3】小插件widget

(1)文本和密码

  • input标签的type属性
    • TextInput
    • PasswordInput
      • 密码框无法指定默认值
class MyForm(forms.Form):
    username = forms.CharField(
        widget=forms.TextInput()
    )
    password = forms.CharField(
        widget=forms.PasswordInput()
    )

(2)属性添加attrs

  • 这个就是后端指定前端的属性
    • **attrs**按照字典的格式传递
    • class的多个属性之间还是以空格隔开
    • style格式还是按照**分号;**隔开
class MyForm(forms.Form):
    username = forms.CharField(
        widget=forms.TextInput(attrs={
            "class": "btn btn-success",
            "style": "color: red; font-size: 10px"
        })
    )

(3)单选

  • RadioSelect:按钮形式单选
  • Select:下拉菜单单选
class MyForm(forms.Form):
    gender_choices = ((1, "男"), (2, "女"), (3, "保密"))
    radioselect = forms.ChoiceField(
        choices=gender_choices,
        widget=forms.RadioSelect
    )
    select = forms.ChoiceField(
        choices=gender_choices,
        widget=forms.Select
    )

(4)复选

  • SelectMultiple:按钮形式复选
  • CheckboxSelectMultiple:下拉菜单复选
class MyForm(forms.Form):
    hobby_choices = (("s", "swimming"), ("d", "dancing"), ("r","running"))
    select_multiple = forms.MultipleChoiceField(
        choices=hobby_choices,
        widget=forms.SelectMultiple
    )
    checkbox_select_multiple = forms.MultipleChoiceField(
        choices=hobby_choices,
        widget=forms.CheckboxSelectMultiple
    )

(5)单个复选框

  • CheckboxInput
    • 将选项以单个复选框的形式呈现
    • 用户可以选择或者取消
class LoginForm(forms.Form):
    keep = forms.ChoiceField(
        label="是否记住密码",
        initial="checked",
        widget=forms.widgets.CheckboxInput()
    )

(6)choices实时刷新

  • 在前面的实例中
    • 我们定死了choices参数
    • 他是静态的了
    • 不会随着内容的添加而更新
  • 所以需要新办法
    • 方式一:重写构造方法
    • 方式二:使用
      • ModelChoiceField单选
      • ModelMultipleChoiceField多选
# 重写构造方法
from django import forms
from models import Gender

class MyForm(forms.Form):
    gender = forms.ChoiceField(initial=3,
                               widget=forms.Select)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['gender'].choices = Gender.objects.all().values_list('id', 'value')
# 使用新的方法
from django import forms
from models import Gender

class MyForm(forms.Form):
    gender = forms.ModelChoiceField(
        initial=2,
        widget=forms.Select,
        queryset=models.Gender.objects.all()
    )
    
# 模型层
from django.db import models
class Gender(models.Model):
    value = models.CharField(max_length=10)

    def __str__(self):
        return self.value

(7)小结

  • forms.TextInput:默认文本输入框
  • forms.Textarea:多行文本输入框
  • forms.CheckboxInput::单个复选框
  • forms.Select:下拉单选
  • forms.RaidoSelect:按钮单选
  • forms.SelectMultiple:按钮形式复选
  • forms.CheckboxSelectMultiple:下拉菜单复选
  • forms.FileInput:文件上传
  • forms.HiddenInput:隐藏输入框
  • ModelChoiceField:实时更新单选
  • ModelMultipleChoiceField:实时更新多选

【4】form组件所有字段

python ------------------------------------------------------------------------------ (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', )} >>> form = ImageForm({}, file_data) # Pillow closes the underlying file descriptor. >>> form.is_valid() True >>> image_field = form.cleaned_data['img'] >>> image_field.image

【三】前端部分

【1】渲染HTML

  • 视图层
class MyForm(forms.Form):
    name = forms.CharField(
        label="用户名:",
        error_messages={"required": "用户名不能为空"}
    )
    password = forms.CharField(
        max_length=10,
        min_length=4,
        label="密码:",
        error_messages={"max_length": "密码最长10位", "min_length": "密码最短4位"},
        widget=forms.PasswordInput(),
    )
    phone = forms.CharField(
        label="手机号",
        validators=[mobile_validate, ],
        error_messages={"required": "手机号不能为空"}
    )
  • 视图层重点
def home(request):
    # 每次访问都产生一个空对象
    form_obj = MyForm()
    # 对post请求进行特殊处理
    if request.method == "POST":
        # 可以将request.POST看成一个字典对象
        # 再次创建有值的对象保存信息
        form_obj = MyForm(request.POST)
        if form_obj.is_valid():
            # 数据合法操作
            return HttpResponse("OK")
        else:
            # 数据不合法处理
            pass
        	
    return render(request, 'home.html', locals())

(1)方式一

  • 直接输出form_obj
{{ form_obj }}
  • 封装成都过高
    • 奇丑无比
    • 没有提交按钮
    • 没法设置post请求

(2)方式二

  • .as_p:当作p标签显示
  • as_table:当作table标签显示
  • as_ul:当作ul标签显示
<form action="" method="post">
    {{ form_obj.as_p }}
    <input type="submit" class="btn btn-success">
form>
  • 好看了好多
    • 可扩展性也很好,可以更改method方法,添加按钮等
    • 并且目前的样子已经实现的引入需求中的功能
    • 但是显示复杂的样式还是不够的

(3)方式三

<form action="" method="post">
    {% for form in form_obj %}
    	<p>{{ form.label }}{{ form }}p>
    {% endfor %}
    <input type="submit" class="btn btn-success">
form>
  • 可扩展性更好
  • 推荐使用

【2】渲染错误信息

  • 在前面的渲染HTML中
    • 当我们输入不对信息
    • 可以看到form组件给我们的提示信息
    • 这个看着挺好,但是我们自己写的错误信息却没用上
    • 扩展性还是不够,需要关系自带的校验
  • 禁用浏览器表单验证novalidate
    • 实际还有个原因就是
    • 前端的验证时极其松懈
<form action="" method="post" novalidate>
  • 前端禁用验证,用自定义的错误信息
    • 错误信息需要.0出来
    • 要不然错误信息将变成li标签
<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" class="btn btn-success">
form>

【四】钩子函数(HOOK)

【1】介绍

(1)说明

  • 钩子函数
    • 就是一种在特定事件或者条件发生时,
    • 被调用的函数
  • 提供了一种创建交互性动态性行为的方法
  • 核心思想
    • 哪里钓出来的数据还需要放回在哪里中

(2)自定义钩子函数

  • 全局钩子函数clean
    • 在应用程序的某个阶段或特定条件下进行数据清理或资源释放的操作
    • 全局钩子函数通常用于应用程序的生命周期管理,以确保在适当的时机执行清理操作,避免资源泄漏或不必要的数据残留
  • 局部钩子函数clean_字段值
    • 在特定字段值发生变化时执行清理操作的钩子函数
    • 这种钩子函数通常与表单处理、输入验证或数据处理相关。当特定字段的值发生变化时,可以使用该钩子函数来执行清理、数据转换、验证或其他相关操作。

【2】示例

(1)局部钩

子函数

  • clean_字段值
  • 对邮箱字段进行额外的验证
    • 不可以使用QQ邮箱
from django import forms

class MyForm(forms.Form):
    email = forms.EmailField(
        label="邮箱地址",
        error_messages={
            "invalid": "邮箱格式不正确",
            "required": "邮箱地址不能为空",
        }
    )

    def clean_email(self):
        # 勾出数据
        email = self.cleaned_data.get("email")
        # 逻辑判断
        if "qq.com" in email:
            # 添加报错信息
            self.add_error("email", "不可以使用QQ邮箱")
        # 放回到原来的位置中
        return email
# 自定义验证器版本
from django import forms
from django.core.exceptions import ValidationError

def validate_email(value):
    if "qq.com" in value:
        raise ValidationError("不可以使用QQ邮箱")

class MyForm(forms.Form):
    email = forms.EmailField(
        label="邮箱地址",
        error_messages={
            "invalid": "邮箱格式不正确",
            "required": "邮箱地址不能为空",
        },
        validators=[validate_email]
    )

(2)全局钩子函数

  • clean

  • 对密码进行二次检验

    • 需要两次密码输入的一样
class MyForm(forms.Form):
    password = forms.CharField(
        label="密码",
        min_length=6,
        widget=forms.PasswordInput,
        error_messages={
            "required": "密码不能为空",
            "min_length": "密码至少含有6位",
        }
    )
    confirm_pwd = forms.CharField(
        label="确认密码",
        min_length=6,
        widget=forms.PasswordInput,
        error_messages={
            "required": "密码不能为空",
            "min_length": "密码至少含有6位",
        }
    )

    def clean(self):
        # 勾出想要的数据
        password = self.cleaned_data.get("password")
        confirm_pwd = self.cleaned_data.get("confirm_pwd")
        # 逻辑判断
        if password != confirm_pwd:
            # 添加报错信息
            self.add_error("confirm_pwd", "两次输入的密码不一致")
        # 勾出的数据放回
        return self.cleaned_data

【五】forms组件源码

【1】入手.is_valid()

  • 视图层的CBV源码是通过as_view
  • forms组件通过.is_valid()入手份分析

【2】简单分析

(1)is_valid

def is_valid(self):
	return self.is_bound and not self.errors
  • 要想检验通过,那么最终的返回结果是True,所以
    • self.is_bound是True
    • not self.errors是True
      • self.errors是False

(2)is_bound

def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
                 initial=None, error_class=ErrorList, label_suffix=None,
                 empty_permitted=False, field_order=None, use_required_attribute=None, renderer=None):
	self.is_bound = data is not None or files is not None
  • is_bound需要是True
    • 那么data或者file必须要有内容
    • __init__参数中的data和file
    • 其中data是第一个形参
      • 即MyForm(request.POST)中的request.POST
      • 只要我们传值了就一定是True

(3)errors

  • is_bound只要传值就是True了,接下来讨论errors
@property
def errors(self):
	if self._errors is None:
		self.full_clean()
	return self._errors
  • 使用property装饰器将errors伪装为属性了
    • 外部可以直接.属性访问
  • 这个方法返回的是_errors
self._errors = None
  • 这个参数默认是None
    • 所以在在没修改这个参数情况下
    • 一定会返回False
    • full_clean()方法一定会执行

(4)full_clean()

    def full_clean(self):
        self._errors = ErrorDict()
        if not self.is_bound: 
            return
        self.cleaned_data = {}
        
        if self.empty_permitted and not self.has_changed():
            return

        self._clean_fields()
        self._clean_form()
        self._post_clean()
  • self._errors = ErrorDict()

    • 创建了一个空字典
  • if not self.is_bound:

    • 前面提到的参数
    • 只要传值了is_bound就不是False
    • 这里一定不会走
  • self.cleaned_data = {}

    • 再次定义空子典
    • 看名字说明是检查的字段
    • 即接下来将检查通过的字段放里面
  • if self.empty_permitted and not self.has_changed():

    • 第一个参数empty_permitted 默认是False
    • 我们又没有对其传值,所以一定是False
    • 即一定不会执行下面的return
    • 第二个参数有点复杂,但是第一个已经是False了
    • 所以不讨论
  • self._clean_fields() self._clean_form()
    self._post_clean()

(5)self._clean_fields()

  • 总的代码,详细分解在下面
    • for name, field in self.fields.items():
    • 遍历表单中的所有字段
    • 接下来的所有部分都是在这之下的
def _clean_fields(self):
    for name, field in self.fields.items():
        if field.disabled:
            value = self.get_initial_for_field(field, name)
        else:
            value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
        try:
            if isinstance(field, FileField):
                initial = self.get_initial_for_field(field, name)
                value = field.clean(value, initial)
            else:
                value = field.clean(value)
            self.cleaned_data[name] = value
            if hasattr(self, 'clean_%s' % name):
                value = getattr(self, 'clean_%s' % name)()
                self.cleaned_data[name] = value
        except ValidationError as e:
            self.add_error(name, e)
  • 第一部分
    • 对字段的disabled禁用属性进行判断,最终获取value
    • 是的话,使用get_initial_for_field
    • 不是的话,使用value_from_datadict处理
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:
	pass
	# 数据清洗
except ValidationError as e:
	self.add_error(name, e)
  • 第二部分:第1块
    • 判断类型是不是文件类型
    • 是的话
      • 使用get_initial_for_field获取初始值
      • 使用clean方法,清洗数据报包括初始值
    • 不是的话,使用clean方法,清洗数据
    • 最终将清洗的数据保存在之前创建的空子典cleaned_data
if isinstance(field, FileField):
initial = self.get_initial_for_field(field, name)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value
  • 第二部分:第2块
    • 利用反射获取局部钩子方法
    • 并执行拿到数据value
    • 保存在清洗字典cleaned_data
    • 这里的value就是局部钩子的返回值
      • 即局部钩子需要将钓出数据返回
if hasattr(self, 'clean_%s' % name):
	value = getattr(self, 'clean_%s' % name)()
	self.cleaned_data[name] = value
  • 所以总的来说就是
    • 对所有的自定义字段进行清洗
    • 对所有的局部钩子函数进行调用,清洗数据

(5)self._clean_form()

def _clean_form(self):
    try:
        cleaned_data = self.clean()
    except ValidationError as e:
        self.add_error(None, e)
    else:
        if cleaned_data is not None:
            self.cleaned_data = cleaned_data
  • 进行异常捕获
    • 如果发生异常
    • 就将异常内容添加到异常部分中
    • 否则将cleaned_data
  • 可能发生的异常代码cleaned_data = self.clean()
    • 首先clean()函数内是什么都没有的只返回了cleaned_data
    • 然而我们之前又有写过全局钩子函数
    • 所以这里是全局钩子函数的调用
    • 接收值是cleaned_data
      • 所以全局钩子函数中
      • 一定要返回这个cleaned_data

【六】ModelForm

【1】介绍

  • Django 的 ModelForm 是一个非常强大的功能,它允许直接模型定义中创建表单
  • 这意味着你无需手动定义表单字段,因为 ModelForm 会自动为你做这个工作。
  • 这对于创建更新模型实例非常有用
from django import forms
from .models import YourModel
# 创建表单类
class YourModelForm(forms.ModelForm):
    class Meta:
        model = YourModel
        fields = '__all__'  # Use all fields from the model
        
# 创建表单
form = YourModelForm()

# 根据已有模型创建表单
your_model = YourModel().object.get(k=1)
form = YourModelForm(instance=your_model)

【2】字段类型

模型字段 表单字段
AutoField 在Form类中无法使用
BigAutoField 在Form类中无法使用
BigIntegerField IntegerField,最小-9223372036854775808,最大9223372036854775807.
BooleanField BooleanField
CharField CharField,同样的最大长度限制。如果model设置了null=True,Form将使用empty_value
CommaSeparatedIntegerField CharField
DateField DateField
DateTimeField DateTimeField
DecimalField DecimalField
EmailField EmailField
FileField FileField
FilePathField FilePathField
FloatField FloatField
ForeignKey ModelChoiceField
ImageField ImageField
IntegerField IntegerField
IPAddressField IPAddressField
GenericIPAddressField GenericIPAddressField
ManyToManyField ModelMultipleChoiceField
NullBooleanField NullBooleanField
PositiveIntegerField IntegerField
PositiveSmallIntegerField IntegerField
SlugField SlugField
SmallIntegerField IntegerField
TextField CharField,并带有widget=forms.Textarea参数
TimeField TimeField
URLField URLField

(1)两个特殊字段

  • model字段和表单字段存在大量的相似和重复之处

  • ManyToManyFieldForeignKey字段类型属于特殊情况

  • ForeignKey

    • ForeignKey 在模型中表示一对多关系。
    • ModelForm 中,ForeignKey 会被转化为 django.forms.ModelChoiceField
    • 这个字段会渲染为一个下拉菜单,用户可以从中选择一个选项。
  • ManyToManyField

    • ManyToManyField 在模型中表示多对多关系。
    • ModelForm 中,ManyToManyField 会被转化为 django.forms.ModelMultipleChoiceField
    • 这个字段会渲染为一个多选框,用户可以选择多个选项。
    • 注意模型类中需要有__str__方法,要不然前端显示的是对象

(2)属性对照

  • blank=True

    • 表示该字段在数据库中可以为空
    • 对应modelform的require=False
  • null=True

    • 表示该字段在数据库中可以为Null
    • 没有对应的modelform属性
  • default

    • 在数据库中的默认值
    • 对应modelform的initial属性,初始值
  • choices

    • 给数据库提供可选择值的列表或元组
    • 对应modelform的ChoiceField或者MultipleChoiceField
  • help_text:

    • 数据库字段的额外描述信息
    • 对应modelform的help_text
  • verbose_name

    • 数据库字段的人类刻度名称
    • 对应modelform的label属性

【3】字段选择

(1)直接添加

  • 通常推荐ModelFormMeta 类中使用 fields 属性来明确指出哪些字段应该被包含在表单中。
  • 这样做可以确保你对表单的完全控制,避免不必要的数据暴露或错误。

(2)__all__

  • 如果模型有很多字段,或者想要包含模型中的所有字段,那么一一列出所有字段可能会很麻烦。
  • 在这种情况下,你可以使用特殊的字符串 '__all__' 来表示所有字段
  • 注意
    • 这样做可能会导致一些安全问题,因为它可能会暴露一些不希望用户直接访问的字段。
    • 因此,除非你完全确定你想要包含所有字段,否则最好还是明确指定 fields
  • 注意
    • 如果在某个模型字段中定义了 editable=False , 则任何使用 ModelForm 给该模型创建的表单都不会包含这个字段。

(3)exclude

  • exclude可以将除了指定的字段以外,将其他字段全部添加
  • 使用 exclude 也需要谨慎,因为它可能会意外地包含一些不希望包含的字段。

(4)示例

class YourModelForm(forms.ModelForm):
    class Meta:
        model = YourModel
        # 添加全部
        fields = '__all__'
        # 指定添加
        fields = ['field1', 'field3']
        # fields和exclude不能混用
        # 除了指定之外的全部添加
        exclude = ['field2']

【4】ModelForm的验证

(1)表单本身的验证

  • Django 的 ModelForm 会自动为每个字段生成相应的验证。
  • 例如:
    • 如果模型有一个 CharField,那么生成的表单会包含一个必填的文本输入字段。
    • 如果用户尝试提交一个空的文本输入字段,那么表单验证将失败,错误信息将显示在表单的错误列表中。

(2)验证模型实例

  • 在表单验证之后,ModelForm 还会验证模型实例。
  • 这包括检查模型的 clean() 方法,以及所有的 uniqueunique_together 约束。
  • 如果模型验证失败,错误信息将被添加到表单的错误列表中。
from django import forms
from django.core.exceptions import ValidationError


class YourModelForm(forms.ModelForm):
    class Meta:
        model = YourModel

    def clean(self):
        cleaned_data = super().clean()
        if condition:
            raise ValidationError({"field": "some message"})
        return cleaned_data

【5】save方法

  • 假设有一个 Article 模型和对应的 ArticleForm
from django import forms
from myapp.models import Article

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ['title', 'content']

(1)instance参数

  • ModelFormsave() 方法可以根据表单中的数据创建并保存一个数据库对象

  • instance参数

    • 可以在调用 save() 方法时传入一个已经存在的模型实例作为 instance 参数,这样 save() 方法将会更新这个实例。

    • 如果没有传入 instance 参数,save() 方法将会创建一个新的模型实例。

    • 在调用 save() 方法时,如果表单还没有进行验证,save() 方法将会先进行验证。如果验证失败,将会抛出 ValueError

form = ArticleForm(request.POST)
if form.is_valid():
    # 保存新的 Article 实例
    new_article = form.save()  
# 获取已存在的 Article 实例
article = Article.objects.get(pk=1)  
form = ArticleForm(request.POST, instance=article)
if form.is_valid():
    # 更新已存在的 Article 实例
    form.save()  

(2)commit 参数

  • save() 方法接受一个可选的 commit 参数,默认为 True

  • 如果传入 commit=Falsesave() 方法将返回一个还没有保存到数据库的模型实例

  • 可以在这个实例上进行任何需要的修改,然后再次手动调用模型实例的 save() 方法将其保存到数据库。

  • 如果模型有多对多关系并且在调用 save() 方法时传入了 commit=False,需要在手动保存模型实例后调用表单的 save_m2m() 方法来保存多对多数据。

  • 除了 save()save_m2m() 方法,ModelForm 的其他使用方式与普通的 Form 完全一样。

    • 可以使用 is_valid() 方法来验证表单
    • 使用 is_multipart() 方法来检查表单是否需要多部分文件上传,等等。
form = ArticleForm(request.POST)
if form.is_valid():
    # 创建但不保存新的 Article 实例
    new_article = form.save(commit=False)  
    # 修改实例的某个属性
    new_article.title = 'New Title'  
    # 手动保存实例到数据库
    new_article.save()  
form = AuthorForm(request.POST)
if form.is_valid():
    # 创建但不保存新的 Author 实例
    new_author = form.save(commit=False)  
    # 修改实例的某个属性
    new_author.name = 'New Name'  
     # 手动保存实例到数据库
    new_author.save() 
    # 保存多对多数据
    form.save_m2m()  

【6】重新定义字段

(1)介绍

  • ModelForm可以自动从模型定义中生成表单字段
  • 但有时希望可以对生成的表单字段进行一些定制
  • 这是就需要modelformMeta类中使用一些特殊的属性来实现

(2)示例

  • widgets 属性来改变 name 字段的 widget
    • 让它使用 Textarea widget,并添加了一个 CSS 类 “special”
  • error_messages 属性来为 name 字段自定义错误信息
    • name 字段的长度超过最大长度时,会提示指定信息
  • help_texts 属性为 name 字段添加了一个帮助文本
    • 这个文本会在表单中显示,以帮助用户知道应该如何填写这个字段。
  • labels 属性来为 name 字段设置一个自定义标签 作者名称
  • 显式定义了 name 字段,并为它设置了一个最小长度验证器 MinLengthValidator(3)
    • 。这将覆盖 Meta 类中的设置
from django import forms
from django.core.validators import MinLengthValidator
from .models import Author


class AuthorForm(forms.ModelForm):
    class Meta:
        model = Author
        fields = ['name', 'title', 'birth_date']
        widgets = {
            'name': forms.TextInput(attrs={'class': "special"})
        }
        error_messages = {
            'name': {
                'max_length': "名字太长了"
            },
        }
        help_texts = {
            'name':"作者的名字字段"
        }
        labels = {
            'name':"作者名称"
        }
		name = forms.CharField(validators=[MinLengthValidator(3),])

【7】数据本地化

(1)什么是数据本地化

  • 本地化是指根据特定的地域设置(如日期和数字的格式)来格式化数据,和配置文件有关,我们可以将
    • TIME_ZONE 设置为 'Asia/Shanghai'
    • USE_TZ 设置为 True
  • 例如
    • 美国和欧洲的日期格式是不同的,美国通常使用月/日/年,而欧洲使用日/月/年。
    • 通过启用本地化,Django 可以根据用户的地理位置或语言偏好来自动选择正确的格式。

(2)示例

from django.forms import ModelForm
from .models import Author

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        localized_fields = ('birth_date',)
        # 所有字段本地化
        localized_fields = '__all__'

【8】初始值

(1)模型表默认值

  • 假设模型表默认值default是张三
class Person(models.Model):
    name = models.CharField(max_length=100, default='张三')
  • 创建表单,没有修改默认值
class PersonForm(forms.ModelForm):
    class Meta:
        model = Person
        fields = ['name']
  • 创建实例
# 创建实例
person = Person.objects.create()
# 创建表单实例
form = PersonForm(instance=person)
  • 结果:
    • 在表单中,name 字段的值将会是 '张三'
    • 因为表单字段的初始值来自模型实例的当前值

(2)modelform的initial

  • 创建新的实例,初始为李四
form = PersonForm(initital={'name': '李四'}, instance=person)
  • 结果

    • 那么在表单中,name 字段的值将会是 '李四'
    • 因为 initial 参数提供的初始值覆盖了模型实例的当前值。
  • 注意:

    • 这个操作不会改变Perosn实例的name字段的值
    • 除非显示地改变它或保存表单

【9】表单的继承

  • 创建的类继承了ModelForm类
  • 那么同样的也可以有子类继承我们创建的类

(1)示例一

  • django自带有模型user和表单UserCreationForm

  • 继承表单UserCreationForm使用模型user重写注册表单

from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django import forms

class CustomUserCreationForm(UserCreationForm):
    # 重写 user模型有
    email = forms.EmailField(required=True)
    # 重写 UserCreationForm表单有
    password1 = forms.CharField(label='密码', widget=forms.PasswordInput)
    password2 = forms.CharField(label='确认密码', widget=forms.PasswordInput)

    class Meta:
        # 模型
        model = User
        # 字段
        fields = ('username', 'email', 'password1', 'password2')

    # 实际父表单有 重写
    def clean(self):
        password = self.cleaned_data.get('password')
        password2 = self.cleaned_data.get('password2')
        if not password or not password2 or password != password2:
            self.add_error("password2", "两次密码不一致")
        return self.cleaned_data
    
    # 重写save方法 保存邮箱
    def save(self, commit=True):
        user = super().save(commit=False)
        user.email = self.cleaned_data['email']
        if commit:
            user.save()
        return user
    

(2)示例二

  • 也可以在表单中定义一些不在模型中的字段
  • 例如
    • 可以在 UserCreationForm 中添加一个 terms_of_service 字段
    • 用于验证用户是否接受服务条款
class CustomUserCreationForm(UserCreationForm):
    # ...
    terms_of_service = forms.BooleanField()

    def clean_terms_of_service(self):
        accepted = self.cleaned_data.get('terms_of_service')
        if not accepted:
            raise forms.ValidationError('你必须接受服务条款。')
        return accepted
  • 在这个例子中,terms_of_service 字段并不对应到用户模型的任何一个字段,而是用来获取用户是否接受服务条款的信息。
  • 这个字段的值可以在视图或表单的 save 方法中进行处理。

你可能感兴趣的:(Django,django,python,后端)