10 forms组件

一. 前戏

1. 视图层views.py

def my_ab_form(request):
    """
    无论是post请求还是get请求
    页面都能够获取到字典 只不过get请求来的时候 字典值都是空的
    而post请求来之后 字典可能有值
    """
    dic = {'username': '', 'password': ''}
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        if not all([username, password]):
            dic['username'] = '用户账户输入不能为空!'
            dic['password'] = '用户密码输入不能为空!'
    return render(request, 'my_ab_form.html', locals())

2. 模板层my_ab_form.html

<form action="" method="post">
    <p>
        用户账户: <input type="text" name="username">
        <span style="color: red">{{ dic.username }}</span>
    </p>
    <p>
        用户密码: <input type="text" name="password">
        <span style="color: red">{{ dic.password }}</span>
    </p>
    <input type="submit" class="btn btn-success">
</form>

3. 总结

# 以上内容分成三部分
"""
1. 手动书写前端获取用户数据的html代码     渲染html代码
2. 后端对用户数据进行校验                校验数据
3. 对不符合要求的数据进行前端提示         展示提示信息
"""

# 引入: forms组件能够完成的事情
"""
1.渲染html代码
2.校验数据
3.展示提示信息
"""

# 疑问: 为什么数据校验非要去后端 不能在前端利用js直接完成呢?
"""
数据校验前端可有可无
但是后端必须要有!!!
    因为前端的校验是弱不禁风的 你可以直接修改
    或者利用爬虫程序绕过前端页面直接朝后端提交数据

例如: 购物网站
    选取了货物之后 会计算一个价格发送给后端 如果后端不做价格的校验
    实际是获取到用户选择的所有商品的主键值
    然后在后端查询出所有商品的价格 再次计算一遍
    如果跟前端一致 那么完成支付如果不一致直接拒绝
"""

二. 基本使用

from django import forms

class MySelfForm(forms.Form):
    # username字符串类型最小3位最大8位
    username = forms.CharField(min_length=3, max_length=8)
    # password字符串类型最小3位最大8位
    password = forms.CharField(min_length=3, max_length=8)
    # email字段必须符合邮箱格式  xxx@xx.com
    email = forms.EmailField()

三. 校验数据

'''
1. 测试环境的准备 可以自己拷贝代码准备
2. 其实在pycharm里面已经帮你准备一个测试环境 -> python console
校验数据的1个方法. 2个属性
    form_obj.is_valid()
    form_obj.cleaned_data
    form_obj.errors
'''

from app01 import views

# 1. 将带校验的数据组织成字典的形式传入即可
form_obj = views.MySelfForm({'username': 'egon', 'password': '123', 'email': '123'})

# 2. 判断数据是否合法		注意该方法只有在所有的数据全部合法的情况下才会返回True
form_obj.is_valid()    # False

# 3. 查看所有校验通过的数据
form_obj.cleaned_data  # {'username': 'egon', 'password': '123'}

# 4. 查看所有不符合校验规则以及不符合的原因
form_obj.errors        # {'email': ['Enter a valid email address.']}


# 5. 校验数据只校验类中出现的字段 多传不影响 多传的字段直接忽略
form_obj = views.MySelfForm({'username': 'egon', 'password': '123', 'email': 'xxx@qq.com', 'hobby': 'play'})
form_obj.is_valid()    # True
form_obj.cleaned_data  # {'username': 'egon', 'password': '123', 'email': 'xxx@qq.com'}
form_obj.errors        # {}


# 6. 校验数据 默认情况下 类里面所有的字段都必须传值. 结合5发现: 也就意味着校验数据的时候 默认情况下数据可以多传但是绝不可能少传
form_obj = views.MySelfForm({'username': 'egon', 'password': '123'})
form_obj.is_valid()    # False
form_obj.cleaned_data  # {'username': 'egon', 'password': '123'}
form_obj.errors        # {'email': ['This field is required.']}

注意事项:

1. is_valid()方法是效验数据的整个核心. 

2. is_valid()必须要先执行. 才能获取效验成功时对应的cleaned_data数据 
    因为is_valid()内部源码执行过程中才对cleaned_data属性进行的赋值操作. 
    如果is_valid()没有效验之前是没有cleaned_data属性的. 会抛出这个属性不存在的错误. 如下所示:
    form_obj = LoginForms({'username': 'alex', 'password': 'qwe123qwe'})
    print(form_obj.cleaned_data)   # AttributeError: 'LoginForms' object has no attribute 'cleaned_data'
    
3. 争对errors没有这个限制, 所继承的Form类的父类BaseForm中则定义了这个方法. 

四. 渲染标签的三种方式

1. 视图层views.py

def my_index(request):
    # 1. 先产生一个空对象
    form_obj = MySelfForm()
    # 2. 直接将该空对象传递给html页面
    return render(request, 'my_index.html', locals())

2. 模板层my_index.py

<form action="" method="post">
    <h3>第一种渲染方式: 代码书写极少,封装程度太高 不便于后续的扩展 一般情况下只在本地测试使用</h3>
    {{ form_obj.as_p }}
    {{ form_obj.as_ul }}
    {{ form_obj.as_table }}

    <h3>第二种渲染方式: 可扩展性很强 但是需要书写的代码太多 一般情况下不用</h3>
    <p>{{ form_obj.username.label }}: {{ form_obj.username }}</p>
    <p>{{ form_obj.password.label }}: {{ form_obj.password }}</p>
    <p>{{ form_obj.email.label }}: {{ form_obj.email }}</p>

    <h3>第三种渲染方式: 代码书写简单 并且扩展性也高 (推荐使用)</h3>
    {% for form in form_obj %}
        <p>{{ form.label }}: {{ form }}</p>   {# 这里的form等同于form_obj.username #}
    {% endfor %}
</form>

3. 总结

'''
注意: forms组件只会自动帮你渲染获取用户输入的标签, 不能帮你渲染提交按钮. 如: input select radio checkbox...等

渲染标签的三种方式: (提示: 原生label属性默认展示的是类中定义的字段首字母大写的形式, 也可以自己修改 直接给字段对象加label属性即可)
    修改label: username = forms.CharField(min_length=3,max_length=8,label='用户名')
    第一种: 用于测试  label 和 框联体
        {{ form_obj.as_p }}
        {{ form_obj.as_ul }}
        {{ form_obj.as_table }}
    第二种: label 和 框分离
        {{ form_obj.字段.label }}: {{ form_obj.字段 }}
    第三种: form 等同于 form_obj.字段   label 和 框分离
        {% for form in form_obj %}
            {{ form.label }}: {{ form }}
        {% endfor %}
'''

五. 展示提示信息

1. 视图层views.py

def my_index(request):
    """
    forms组件精髓: forms组件当你的数据不合法的情况下 会保存你上次的数据 让你基于之前的结果进行修改更加的人性化
    你会发现form_obj = MySelfForm() 和 form_obj = MySelfForm(request.POST)都是用的同一个变量名. 这是为什么呢?
    我们先了解下网页请求步骤:
        第一步: 浏览器输入url, 服务端反馈form页面. get请求
        第二步: 用户输入提交输入数据. post请求
    解析:
        get以后form_obj拿到的是一个空对象. 默认先对input框value=''占位
        post以后form_obj获取到是用户输入的内容.
            这里右从新赋值了有值的form_obj.
            通过模板语法传递到了HTML页面又render返回给了页面中.
            这时原来占位的就是之前用户输入的内容.
    用之前得代码分析:
        def my_ab_form(request):
            success_dic = {'username': '', 'password': ''}
            error_dic = {'username': '', 'password': ''}
            if request.method == 'POST':
                username = request.POST.get('username')
                password = request.POST.get('password')
                if not all([username, password]):
                    error_dic['username'] = '用户账户输入不能为空!'
                    error_dic['password'] = '用户密码输入不能为空!'
                success_dic['username'] = username
                success_dic['password'] = password
            return render(request, 'my_ab_form.html', locals())
    """
    # 1. 先产生一个空对象
    form_obj = MySelfForm()
    if request.method == 'POST':
        # 3. 获取用户数据并且校验. 校验数据需要构造成字典的格式传入MySelfForm才行. 但是request.POST可以看成就是一个字典
        form_obj = MySelfForm(request.POST)
        # 4. 判断数据是否合法
        if form_obj.is_valid():
            return HttpResponse('效验成功!')
        # 5. 不合法 有错误
    # 2. 直接将该空对象传递给html页面
    return render(request, 'my_index.html', locals())

2. 模板层my_index.html

<form action="" method="post" novalidate>
    <h3>第三种渲染方式: 代码书写简单 并且扩展性也高 (推荐使用)</h3>
    {% 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-info">
</form>

3. 视图层views.py

from django import forms

class MySelfForm(forms.Form):
    # username字符串类型最小3位最大8位
    username = forms.CharField(min_length=3, max_length=8, label='用户账户',
                               error_messages={
                                   'min_length': '用户账户最少3位!',
                                   'max_length': '用户账户最大8位',
                                   'required': '输入不能为空!',
                               }
                               )
    # password字符串类型最小3位最大8位
    password = forms.CharField(min_length=3, max_length=8, label='用户密码',
                               error_messages={
                                   'min_length': '用户密码最少3位!',
                                   'max_length': '用户密码最大8位',
                                   'required': '输入不能为空!',
                               }
                               )
    # email字段必须符合邮箱格式  xxx@xx.com
    email = forms.EmailField(label='用户邮箱',
                             error_messages={
                                 'invalid': '用户邮箱格式不正确!',
                                 'required': '输入不能为空!',
                             }
                             )

4. 总结

'''
提示: 浏览器会自动帮你校验数据 但是前端的校验弱不禁风
关闭浏览器的默认效验: form 标签指定属性 novalidate
前端获取到对应字段的错误提示信息:
    {% for form in form_obj %}
        {{ form.errors.0 }}
    {% endfor %}
后端自定义错误提示信息: 指定参数error_messages
    参数必须指定的键值对: required='输入不能为空!'
    email的特殊指定格式: invalid='用户邮箱格式不正确'
'''

六. 钩子函数(HOOK)

1. 简介

"""
钩子函数会在特定的节点自动触发完成响应操作
钩子函数在forms组件中就类似于第二道关卡,能够让我们自定义校验规则

在forms组件中有两类钩子
    1. 局部钩子
        当你需要给单个字段增加校验规则的时候可以使用
    2. 全局钩子
        当你需要给多个字段增加校验规则的时候可以使用

两类钩子函数书写:
    第一类: 局部
        def clean_字段(self):
            username = self.cleaned_data.get('username')
            return 字段
    第二类: 全局
        def clean_字段(self):
            password = self.cleaned_data.get('password')
            confirm_password = self.cleaned_data.get('confirm_password')
            return self.cleaned_data
    添加错误提示信息:
        self.add_error('字段', 'error')
"""

2. 校验用户名中不能含有666 只是校验

username字段(局部钩子)
def clean_username(self):  # 局部钩子
    # 获取到用户名
    username = self.cleaned_data.get('username')
    if '666' in username:
        # 提示前端展示错误信息
        self.add_error('username', '光喊666是不行滴~')
    # 将钩子函数钩去出来数据再放回去
    return username

3. 校验密码和确认密码是否一致 password confirm两个字段(全局钩子)

def clean(self):   # 全局钩子
    password = self.cleaned_data.get('password')
    confirm_password = self.cleaned_data.get('confirm_password')
    if password != confirm_password:
        self.add_error('confirm_password', '2次密码输入不一致!')
    # 将钩子函数钩出来数据再放回去
    return self.cleaned_data

七. forms组件源码分析

# 切入点: form_obj.is_valid()
# 第一步:
def is_valid(self):
    """
    Returns True if the form has no errors. Otherwise, False. If errors are
    being ignored, returns False.
    如果表单没有错误,则返回True。否则,False。如果错误是被忽略,则返回False。
    """
    return self.is_bound and not self.errors
    # 如果is_valid要返回True的话 那么self.is_bound要为True self.errors要为False


# 第二步:  self.is_bound
self.is_bound = data is not None or files is not None  # 只要你传值了肯定为True


# 第三步: self.errors
self._errors = None
@property
def errors(self):
    "Returns an ErrorDict for the data provided for the form"  # 为表单提供的数据返回一个错误
    if self._errors is None:     # self._errors默认为None, self.full_clean()一定执行
        self.full_clean()
    return self._errors


# 第四步: forms组件核心.  forms组件所有的功能基本都出自于该方法
def full_clean(self):
    ...
    self._clean_fields()  # 校验字段 + 局部钩子
    self._clean_form()    # 全局钩子
    self._post_clean()    # 内置的

def _clean_fields(self):
    for name, field in self.fields.items():
        '''
        name就是字段, field就是产生字段的CharField对象.
        通过for循环hasattr(self, 'clean_%s' % name)中name就能被定位,
        就就能获取到我们定义的局部钩子函数, 并接往下执行.
        '''
        ...     # 这里都是对字段进行的效验步骤处理
        try:
            ...
            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)
            '''
            这里当try中抛出ValidationError异常,
            那么就会在本次的钩子函数中对你声明的局部钩子函数对应的字段增添错误信息.
            我们可以通过次方法手动使用raise ValidationError('')抛出异常.
            那么就会实现等同于self.add_error()的效果
            '''

def _clean_form(self):
    try:
        cleaned_data = self.clean()    # 这里就会执行我们定义的全局钩子self.clean(), 它同局部一样也要有个返回值
    except ValidationError as e:
        self.add_error(None, e)
    else:
        if cleaned_data is not None:   # 我们指定的返回值它就会走这一步
            self.cleaned_data = cleaned_data
            '''
            我们发现它又赋值回了self.cleaned_data.
            因此就能明白之前之所以全局要返回全部.
            是因为你获取了全部的cleaned_data同时原封不动的返回给它.
            如果不是这样做那么, self.cleaned_data将是一个不全的效验合格的数据
            '''            

八. forms组件其他参数及补充

label                自定义标记
error_messages       自定义报错信息
    {'invalid': '邮箱格式错误!', 'require': '输入内容不能为空!'}
initial              默认值. 对应input框的value值
required             控制字段是否必填

'''
1.字段没有样式
2.针对不同类型的input如何修改
    text
    password
    date
    radio
    checkbox
...
widget=forms.widgets.PasswordInput(attrs={'class':'form-control c1 c2'})

# 在第一道关卡设置正则RegexValidator验证器
from django.core.validators import RegexValidator
validators=[
    RegexValidator(r'^[0-9]+$', '请输入数字'),
    RegexValidator(r'^159[0-9]+$', '数字必须以159开头')
]

九. 常用字段与插件

1. 初始值initial

username = forms.CharField(
        min_length=8,
        label="用户名",
        initial="张三"  # 设置默认值
    )

2. 自定义错误提示error_massages

username = forms.CharField(
        min_length=8,
        label="用户名",
        initial="张三",
        error_messages={
            "required": "不能为空",
            "invalid": "格式错误",
            "min_length": "用户名最短8位"
        }
    )

3. 密码框password

# 设置render_value在输入错误的时候, 保留输入的内容value值
pwd = forms.CharField(
        min_length=6,
        label="密码",
        widget=forms.widgets.PasswordInput(attrs={'class': 'c1'}, render_value=True)
    )

4. 反选radio

gender = forms.fields.ChoiceField(
    choices=((1, "男"), (2, "女"), (3, "保密")),
    label="性别",
    initial=3,
    widget=forms.widgets.RadioSelect()
)

5. 单选checkbox

keep = forms.ChoiceField(
    label="是否记住密码",
    initial="checked",
    widget=forms.widgets.CheckboxInput()
)

6. 多选checkbox

hobbies1 = forms.MultipleChoiceField(
    choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
    label="爱好",
    initial=[1, 3],
    widget=forms.widgets.CheckboxSelectMultiple()
)

7. 单选Select

education  = forms.ChoiceField(
    choices=((1, "高中"), (2, "中专"), (3, "大专"), (4, "本科"), (5, "研究生"), (6, "硕士")),
    label="学历",
    initial=3,
    widget=forms.widgets.Select(attrs={'class': 'form-control'})
)

8. 多选Select

hobbies2 = forms.MultipleChoiceField(
    choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
        label="爱好",
    initial=[1, 3],
    widget=forms.widgets.SelectMultiple(attrs={'class': 'form-control'})
)

十. 总结

# forms组件能够完成的3件事情
    1. 渲染html代码
    2. 效验数据
    3. 展示提示信息

# 基本使用
    from django import forms
    class MySelfForm(forms.Form):
        username = forms.CharField(min_length=2, max_length=8)
        password = forms.CharField(min_length=2, max_length=8)
        Email = forms.Email()

# 校验数据的1个方法. 2个属性
    form_obj.is_valid()     判断数据合法. 需要所有数据全部合法才会返回True
    form_obj.cleaned_data   获取效验通过数据.
    form_obj.errors         获取效验不通过数据.
    MySelfForm传值特点: 多传不影响. 少传不可以

# 渲染标签的三种方式
    第一种:
        form_obj.as_p
        form_obj.as_ul
        form_obj.as_table
    第二种:
        form_obj.字段.label  form_obj.字段
    第三种: form 等同于 form_obj.字段.label
        {% for form in form_obj %}
            {{ form.label  }}: {{ form }}
        {% endfor %}
    提示: 只会渲染用户输入标签, 不会渲染提交按钮
    label属性默认是字段名首字母大写. 可以指定
         username = forms.CharField(min_length=2, max_length=8, label='用户名')

# 展示提示信息
    # 后端
        def index(request):
            form_obj = MySelfForm()
            if request.method == "POST":
                form_obj = MySelfForm(request.POST)
                if form_obj.is_valid():
                    return HttpResponse('效验成功!')
            return render(request, 'my_index.html', locals())

    # 前端: (提示: 前端会默认帮你效验. 也可以取消, 对form表单指定属性novalidate)
        {% for form in form_obj %}
            <p>
                {{ form.label  }}: {{ form }}
                <span>{{ form.errors.0 }}</span>
            </p>
        {% endfor %}

    # errors可以自定义
        error_messages={
            'min_length': '最小',
            'max_length': '最大',
            'invalid': '邮箱格式',  # 邮箱格式独有
            'required': '必填',     # 必有. 必须指定
        }

# 钩子函数hook
    # 在第一道关卡满足以后. 才会执行第二道关卡钩子函数. 定义在MySelfForm中.
        1. 全局钩子  clean        争对多个效验
        2. 局部钩子  clean_字段    争对单个效验
    # 使用注意事项:  钩子指定完毕要把数据放回
        全局钩子需返回所有数据: return self.cleaned_data
        局部钩子只争对指定字段:
            username = self.cleaned_data.get('username')
            return username
    # 添加错误信息:
        self.add_errors(field, error)

# forms组件源码分析
    # 切入点: form_obj.is_valid()
    # 强调: 面向对象的查找方法.    
        对象本身
        实例化对象的类
        继承的父类....
    # forms组件核心: 
        def full_clean(self);
            ...
            self._clean_fields()  # 实现效验字段 + 局部钩子
            self._clean_form()    # 实现全局钩子
            self._post_clean()    # 内置          
        
# forms组件其他参数及补充
    label          自定义标记
    error_messages 自定义报错信息
    initial        默认值. 对应input框的value值
    required       控制字段是否必填

    # 提示: 字段默认没有样式. input框默认text类型
    widget=forms.widgets.PasswordInput(attrs={'class': 'form-control', 'myself_attr': 'myself_value'})

    # 第一道关卡也可以使用正则效验
    from django.core.validators import RegexValidator
    validators = [
        RegexValidator(r'正则匹配规则1', '错误提示1'),
        RegexValidator(r'正则匹配规则2', '错误提示2'),
    ]
    
# 常用字段与插件
   # 反选radio:
        字段: ChoiceField
        widget参数: RadioSelect
   # 单选checkbox:
        字段: ChoiceField
        widget参数: CheckboxInput
   # 多选checkbox:
        字段: MultipleChoiceField
        widget参数: CheckboxSelectMultiple
   # 单选Select
        字段: ChoiceField
        widget参数: Select
   # 多选Select:
        字段: MultipleChoiceField
        widget参数: SelectMultiple
posted @ 2020-06-05 01:33  给你加马桶唱疏通  阅读(193)  评论(0编辑  收藏  举报