django之--forms组件

forms组件

前戏

"""
写一个注册功能
    获取用户名和密码 利用form表单提交数据
    在后端判断用户名和密码是否符合一定的条件
        用户名中不能含有666
        密码不能少于三位
    
    如何符合条件需要你将提示信息展示到前端页面
"""
def ab_form(request):
    back_dic = {'username':'','password':''}
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        if '666' in username:
            back_dic['username'] = '不能666'
        if len(password) < 4:
            back_dic['password'] = '密码不能少于4位'
    """
    无论是post请求还是get请求
    页面都能够获取到字典 只不过get请求来的时候 字典值都是空的
    而post请求来之后 字典可能有值
    """
    return render(request,'ab_form.html',locals())

<form action="" method="post">
    <p>username:
        <input type="text" name="username">
        <span style="color: red">{{ back_dic.username }}</span>
    </p>
    <p>password:
        <input type="text" name="password">
        <span style="color: red">{{ back_dic.password }}</span>
    </p>
    <input type="submit" class="btn btn-info">
</form>


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

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

为什么数据校验非要去后端 不能在前端利用js直接完成呢?
    因为前端校验不安全,可以直接修改,爬虫程序也可以绕过前端页面直接朝后端提交数据
    
    购物网站    
        选取了货物之后 会计算一个价格发送给后端 如果后端不做价格的校验
        
        实际是获取到用户选择的所有商品的主键值
        然后在后端查询出所有商品的价格 再次计算一遍
        如果跟前端一致 那么完成支付如果不一致直接拒绝
"""

基本使用

 

from django import forms


class MyForm(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字段必须符合邮箱格式  [email protected]
    email = forms.EmailField()

 

校验数据

"""
1.测试环境的准备 可以自己拷贝代码准备
2.其实在pycharm里面已经帮你准备一个测试环境
    python console
    
基本方法:
    1、is_valid():判断是否合法,只有所有都合法才返回true
    2、errors:查看所有不符合校验规则以及不符合的原因
    3、cleaned_data:查看所有校验通过的数据
"""
from app01 import views
# 1 将带校验的数据组织成字典的形式传入即可
form_obj = views.MyForm({'username':'jason','password':'123','email':'123'})
# 2 判断数据是否合法        注意该方法只有在所有的数据全部合法的情况下才会返回True
form_obj.is_valid()
False
# 3 查看所有校验通过的数据
form_obj.cleaned_data
{'username': 'jason', 'password': '123'}
# 4 查看所有不符合校验规则以及不符合的原因
form_obj.errors
{
  'email': ['Enter a valid email address.']
}
# 5 校验数据只校验类中出现的字段 多传不影响 多传的字段直接忽略
form_obj = views.MyForm({'username':'jason','password':'123','email':'[email protected]','hobby':'study'})
form_obj.is_valid()
True
# 6 校验数据 默认情况下 类里面所有的字段都必须传值
form_obj = views.MyForm({'username':'jason','password':'123'})
form_obj.is_valid()
False
"""
也就意味着校验数据的时候 默认情况下数据可以多传但是绝不可能少传
"""

字段校验

RegexValidator验证器

from django.forms import Form
from django.forms import widgets
from django.forms import fields
from django.core.validators import RegexValidator
 
class MyForm(Form):
    user = fields.CharField(
        validators=[RegexValidator(r'^[0-9]+$', '请输入数字'), RegexValidator(r'^159[0-9]+$', '数字必须以159开头')],
    )

自定义验证函数

import re
from django.forms import Form
from django.forms import widgets
from django.forms import fields
from django.core.exceptions import ValidationError
 
 
# 自定义验证规则
def mobile_validate(value):
    mobile_re = re.compile(r'^(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$')
    if not mobile_re.match(value):
        raise ValidationError('手机号码格式错误')
 
 
class PublishForm(Form):
 
 
    title = fields.CharField(max_length=20,
                            min_length=5,
                            error_messages={'required': '标题不能为空',
                                            'min_length': '标题最少为5个字符',
                                            'max_length': '标题最多为20个字符'},
                            widget=widgets.TextInput(attrs={'class': "form-control",
                                                          'placeholder': '标题5-20个字符'}))
 
 
    # 使用自定义验证规则
    phone = fields.CharField(validators=[mobile_validate, ],
                            error_messages={'required': '手机不能为空'},
                            widget=widgets.TextInput(attrs={'class': "form-control",
                                                          'placeholder': u'手机号码'}))
 
    email = fields.EmailField(required=False,
                            error_messages={'required': u'邮箱不能为空','invalid': u'邮箱格式错误'},
                            widget=widgets.TextInput(attrs={'class': "form-control", 'placeholder': u'邮箱'}))

渲染标签

'''
forms组件只会自动帮我们渲染获取用户输入的标签(input、select、radio、checkbox)
无法渲染提交按钮
'''
def register(request):
    # 产生一个空对象
    form_obj = MyForms()
    # 直接将空对象传递给html页面
    return render(request,'register.html',locals())

# 渲染标签的三种方式
'''
方式一:
    优点:代码简洁
    缺点:封装程度太高,不利于扩展
    {{ form_obj.as_p }}
    {{ form_obj.as_ul }}
    {{ form_obj.as_table }}
'''
'''
方式二:
    优点:可扩展性强
    缺点:代码过于冗余
    <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>
'''
'''
方式三:
    优点:代码简洁,可扩展性也高,推荐使用
    {% for form in form_obj %}
        <p>{{ form.label }}:{{ form }}</p>
    {% endfor %}
    
label属性默认展示的是类中定义的字段首字母大写的形式
也可以自己修改 直接给字段对象加label属性即可
     username = forms.CharField(min_length=3,max_length=8,label='用户名')
'''

展示信息

'''
使用forms组件时浏览器会自动帮我们校验数据,但是前端的校验太弱了
可以给form标签设置novalidate参数使浏览器不自动校验
forms组件当数据不合法的情况下,会保存上次的数据 让你基于之前的结果进行修改
更加的人性化
'''
def register(request):
    # 1、先产生空对象
    form_obj = MyForms()
    if request.method == 'POST':
        # 3、获取数据信息并校验
        """
        1、校验数据需要传入字典格式
        2、request.POST可以看成是一个字典
        3、保证get请求与post请求传给html页面的变量名必须一样
        """
        form_obj = MyForms(request.POST)
        # 4、判断数据是否合法 ps只有当类中数据全部合法才算合法
        if form_obj.is_valid():
            # 合法时的操作
            return HttpResponse('OK')
        
    # 2、将空对象传递给html页面
    return render(request,'register.html',locals())

{% for form in form_obj %}
        <p>{{ form.label }}:{{ form }}
            <span style="color: red">{{ form.errors.0 }}</span></p>
{% endfor %}

# 针对错误提示可以使用error_messages来自定义错误提示 (第一道防线)
'''
error_messages中需要记忆的几个参数
    1、'invalid': '邮箱不合法'   用于邮箱不合法时的错误提示
    2、'required': 'xxx不能为空' 用于提交信息为空时的错误提示
'''
# 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字段必须符合邮箱格式  [email protected]
    email = forms.EmailField(label='邮箱',
                             error_messages={
                                 'invalid':'邮箱格式不正确',
                                 'required': "邮箱不能为空"})

钩子函数(HOOK)

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

在forms组件中有两类钩子
    1.局部钩子
        案例:校验用户输入的用户名必须是字母数字下划线的组合
        当你需要给单个字段增加校验规则的时候可以使用
    2.全局钩子
        案例:校验用户两次密码输入是否一致
          当你需要给多个字段增加校验规则的时候可以使用
'''
# 局部钩子
def clean_user(self):
    # 获取到用户名
    user = self.cleaned_data.get('user')
    obj = models.User.objects.filter(user=user).first()
    if obj:
        # 提示前端展示错误信息
        self.add_error('user', '用户名已存在')
    # 将钩子函数钩去出来数据再放回去
    return user
# 全局钩子
def clean(self):
    pwd = self.cleaned_data.get('pwd')
    pwd2 = self.cleaned_data.get('pwd2')
    if pwd != pwd2:
        self.add_error('pwd2', '两次密码不一致')
    # 将钩子函数钩出来数据再放回去
    return self.cleaned_data

其他参数及补充知识点

# 必须掌握的
'''
label    自定义字段名
error_messages    自定义报错信息(第一道防线)
initial        给字段设置默认值
required    控制字段是否必填
render_value=True   展示错误信息时让密码不重置
PasswordInput(render_value=True) 默认为False,展示错误信息重置密码
'''

# 修改字段样式
# EmailInput会将input的type属性由text修改成email
widget=forms.widgets.EmailInput(attrs={'class': 'form-control', 'placeholder': '邮箱', 'name': 'email'}),
# PasswordInput会将input的type属性由text修改成password
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control', 'placeholder': '密码', 'name': 'pwd'}),
# 多个属性值之间用空格分隔

# 第一道防线————正则校验
from django.core.validators import RegexValidator
validators=[
    RegexValidator(r'^1[3456789]\d{9}', '手机号格式错误')
]

其他类型渲染

 # radio
    gender = forms.ChoiceField(
        choices=((1, ""), (2, ""), (3, "保密")),
        label="性别",
        initial=3,
        widget=forms.widgets.RadioSelect()
    )
    # select
    hobby = forms.ChoiceField(
        choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
        label="爱好",
        initial=3,
        widget=forms.widgets.Select()
    )
    # 多选
    hobby1 = forms.MultipleChoiceField(
        choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
        label="爱好",
        initial=[1, 3],
        widget=forms.widgets.SelectMultiple()
    )
    # 单选checkbox
    keep = forms.ChoiceField(
        label="是否记住密码",
        initial="checked",
        widget=forms.widgets.CheckboxInput()
    )
    # 多选checkbox
    hobby2 = forms.MultipleChoiceField(
        choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
        label="爱好",
        initial=[1, 3],
        widget=forms.widgets.CheckboxSelectMultiple()
    )

forms组件源码

'''forms组件内部原理'''
'''
通过手动抛出异常来展示提示信息(通过看源码知道的第二种展示信息的方法)
# 不推荐,应为过于繁琐,知道有这个方法即可
from django.core.exceptions import ValidationError
raise ValidationError('用户名已存在')
'''
# 切入点:forms_obj.is_valid()
    def is_valid(self):
        """
        Returns True if the form has no errors. Otherwise, False. If errors are
        being ignored, returns False.
        """
        return self.is_bound and not self.errors

# self.is_bound:只要你往组件传值(request.POST)那么self.is_bound就为True

# self.errors
    @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()
        return self._errors
    
# self.full_clean()
    def full_clean(self):
        """
        Cleans all of self.data and populates self._errors and
        self.cleaned_data.
        """
        self._errors = ErrorDict() # 我们的errors就是从这里开始创建出来的
        if not self.is_bound:  # 只要传值了就不会触发
            return
        self.cleaned_data = {} # 我们的cleaned_data就是在这里定义的
        # If the form is permitted to be empty, and none of the form data has
        # changed from the initial data, short circuit any validation.
        if self.empty_permitted and not self.has_changed():
            return
        
        # forms组件的精髓就在下面三句代码里
        self._clean_fields() # 校验字段、局部钩子
        self._clean_form() # 全局钩子
        self._post_clean()
        
# self._clean_fields()
    def _clean_fields(self):
        for name, field in self.fields.items():
            # value_from_datadict() gets the data from the data dictionaries.
            # Each widget type knows how to retrieve its own data, because some
            # widgets split data over several HTML fields.
            ...
            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)

# 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

froms常用字段插件

创建Form类时,主要涉及到 【字段】 和 【插件】,字段用于对用户请求数据的验证,插件用于自动生成HTML;

initial

初始值,input框里面的初始值。

class LoginForm(forms.Form):
    username = forms.CharField(
        min_length=8,
        label="用户名",
        initial="张三"  # 设置默认值
    )
    pwd = forms.CharField(min_length=6, label="密码")

error_messages

重写错误信息。

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

password

class LoginForm(forms.Form):
    ...
    pwd = forms.CharField(
        min_length=6,
        label="密码",
        widget=forms.widgets.PasswordInput(attrs={'class': 'c1'}, render_value=True)
    )

radioSelect

单radio值为字符串

class LoginForm(forms.Form):
    username = forms.CharField(
        min_length=8,
        label="用户名",
        initial="张三",
        error_messages={
            "required": "不能为空",
            "invalid": "格式错误",
            "min_length": "用户名最短8位"
        }
    )
    pwd = forms.CharField(min_length=6, label="密码")
    gender = forms.ChoiceField(
        choices=((1, ""), (2, ""), (3, "保密")),
        label="性别",
        initial=3,
        widget=forms.widgets.RadioSelect()
    )

单选Select

class LoginForm(forms.Form):
    ...
    hobby = forms.ChoiceField(
        choices=((1, "篮球"), (2, "足球"), (3, "双色球"), ),
        label="爱好",
        initial=3,
        widget=forms.widgets.Select()
    )

多选Select

class LoginForm(forms.Form):
    ...
    hobby = forms.MultipleChoiceField(
        choices=((1, "篮球"), (2, "足球"), (3, "双色球"), ),
        label="爱好",
        initial=[1, 3],
        widget=forms.widgets.SelectMultiple()
    )

单选checkbox

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

多选checkbox

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

choice字段注意事项

在使用选择标签时,需要注意choices的选项可以配置从数据库中获取,但是由于是静态字段 获取的值无法实时更新,需要重写构造方法从而实现choice实时更新。

方式一:

from django.forms import Form
from django.forms import widgets
from django.forms import fields

 
class MyForm(Form):
 
    user = fields.ChoiceField(
        # choices=((1, '上海'), (2, '北京'),),
        initial=2,
        widget=widgets.Select
    )
 
    def __init__(self, *args, **kwargs):
        super(MyForm,self).__init__(*args, **kwargs)
        # self.fields['user'].choices = ((1, '上海'), (2, '北京'),)
        #
        self.fields['user'].choices = models.Classes.objects.all().values_list('id','caption')

方式二:

from django import forms
from django.forms import fields
from django.forms import models as form_model

 
class FInfo(forms.Form):
    authors = form_model.ModelMultipleChoiceField(queryset=models.NNewType.objects.all())  # 多选
    # authors = form_model.ModelChoiceField(queryset=models.NNewType.objects.all())  # 单选

Django Form所有内置字段

Field
    required=True,               是否允许为空
    widget=None,                 HTML插件
    label=None,                  用于生成Label标签或显示内容
    initial=None,                初始值
    help_text='',                帮助信息(在标签旁边显示)
    error_messages=None,         错误信息 {'required': '不能为空', 'invalid': '格式错误'}
    validators=[],               自定义验证规则
    localize=False,              是否支持本地化
    disabled=False,              是否可以编辑
    label_suffix=None            Label内容后缀
 
 
CharField(Field)
    max_length=None,             最大长度
    min_length=None,             最小长度
    strip=True                   是否移除用户输入空白
 
IntegerField(Field)
    max_value=None,              最大值
    min_value=None,              最小值
 
FloatField(IntegerField)
    ...
 
DecimalField(IntegerField)
    max_value=None,              最大值
    min_value=None,              最小值
    max_digits=None,             总长度
    decimal_places=None,         小数位长度
 
BaseTemporalField(Field)
    input_formats=None          时间格式化   
 
DateField(BaseTemporalField)    格式:2015-09-01
TimeField(BaseTemporalField)    格式:11:12
DateTimeField(BaseTemporalField)格式:2015-09-01 11:12
 
DurationField(Field)            时间间隔:%d %H:%M:%S.%f
    ...
 
RegexField(CharField)
    regex,                      自定制正则表达式
    max_length=None,            最大长度
    min_length=None,            最小长度
    error_message=None,         忽略,错误信息使用 error_messages={'invalid': '...'}
 
EmailField(CharField)      
    ...
 
FileField(Field)
    allow_empty_file=False     是否允许空文件
 
ImageField(FileField)      
    ...
    注:需要PIL模块,pip3 install Pillow
    以上两个字典使用时,需要注意两点:
        - form表单中 enctype="multipart/form-data"
        - view函数中 obj = MyForm(request.POST, request.FILES)
 
URLField(Field)
    ...
 
 
BooleanField(Field)  
    ...
 
NullBooleanField(BooleanField)
    ...
 
ChoiceField(Field)
    ...
    choices=(),                选项,如:choices = ((0,'上海'),(1,'北京'),)
    required=True,             是否必填
    widget=None,               插件,默认select插件
    label=None,                Label内容
    initial=None,              初始值
    help_text='',              帮助提示
 
 
ModelChoiceField(ChoiceField)
    ...                        django.forms.models.ModelChoiceField
    queryset,                  # 查询数据库中的数据
    empty_label="---------",   # 默认空显示内容
    to_field_name=None,        # HTML中value的值对应的字段
    limit_choices_to=None      # ModelForm中对queryset二次筛选
     
ModelMultipleChoiceField(ModelChoiceField)
    ...                        django.forms.models.ModelMultipleChoiceField
 
 
     
TypedChoiceField(ChoiceField)
    coerce = lambda val: val   对选中的值进行一次转换
    empty_value= ''            空值的默认值
 
MultipleChoiceField(ChoiceField)
    ...
 
TypedMultipleChoiceField(MultipleChoiceField)
    coerce = lambda val: val   对选中的每一个值进行一次转换
    empty_value= ''            空值的默认值
 
ComboField(Field)
    fields=()                  使用多个验证,如下:即验证最大长度20,又验证邮箱格式
                               fields.ComboField(fields=[fields.CharField(max_length=20), fields.EmailField(),])
 
MultiValueField(Field)
    PS: 抽象类,子类中可以实现聚合多个字典去匹配一个值,要配合MultiWidget使用
 
SplitDateTimeField(MultiValueField)
    input_date_formats=None,   格式列表:['%Y--%m--%d', '%m%d/%Y', '%m/%d/%y']
    input_time_formats=None    格式列表:['%H:%M:%S', '%H:%M:%S.%f', '%H:%M']
 
FilePathField(ChoiceField)     文件选项,目录下文件显示在页面中
    path,                      文件夹路径
    match=None,                正则匹配
    recursive=False,           递归下面的文件夹
    allow_files=True,          允许文件
    allow_folders=False,       允许文件夹
    required=True,
    widget=None,
    label=None,
    initial=None,
    help_text=''
 
GenericIPAddressField
    protocol='both',           both,ipv4,ipv6支持的IP格式
    unpack_ipv4=False          解析ipv4地址,如果是::ffff:192.0.2.1时候,可解析为192.0.2.1, PS:protocol必须为both才能启用
 
SlugField(CharField)           数字,字母,下划线,减号(连字符)
    ...
 
UUIDField(CharField)           uuid类型

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章