Form表单验证

这里不是验证用户名密码是否正确,这部分内容之前已经讲过了。这里要验证的是数据格式,这步验证是在收到请求后先执行的验证。只有数据格式验证通过,才会验证用户名密码是否正确。如果数据格式验证不通过,则返回错误信息。
讲师的博客地址:http://www.cnblogs.com/wupeiqi/articles/6144178.html

测试环境

先写一个form表单,host.html:

<form action="/host/" method="POST">
    {% csrf_token %}
    <p><input type="text" name="hostname" placeholder="主机名"></p>
    <p><input type="text" name="ip" placeholder="IP地址"></p>
    <p><input type="text" name="port" placeholder="端口号"></p>
    <p><input type="text" name="email" placeholder="E-Mail"></p>
    <p><input type="submit" value="登录"></p>
</form>

然后导入验证的模块写一个类:

from django import forms
class FM(forms.Form):
    # 变量的字段名匹配form表单里的name属性的值,必须一样
    hostname = forms.CharField()
    ip = forms.GenericIPAddressField(protocol='ipv4')
    port = forms.IntegerField()
    email = forms.EmailField()
    # 上面只有这么几个,如果提交的数据有很多,那么其他数据都不收(丢弃)

然后是在处理函数里,通过这个类进行验证:

def host(request):
    if request.method == 'GET':
        return render(request, 'host.html')
    if request.method == 'POST':
        obj = FM(request.POST)  # 实例化,把POST的数据传入
        res = obj.is_valid()  # 获取结果
        print(res)  # 验证通过是True,不通过则是False
        if res:
            print(obj.cleaned_data)  # 这是一个原生的字典,里面就是提交来的数据
            return HttpResponse('OK')
        else:
            print(obj.errors)  # 这里是一串html的列表的代码
            print(type(str(obj.errors)))  # 这里的str方法居然是django提供的,变成(<class 'django.utils.safestring.SafeText'>)
            print(obj.errors.as_json())  # 也可以拿到JSON的格式
            return HttpResponse(str(obj.errors))  # 通过str方法后,页面上会直接按html代码处理

现在可以打开页面测试效果。
验证通过,obj.cleaned_data 里就是合法的数据的字典,可以进行后续的操作。
验证不通过,obj.errors是错误信息(html的格式,一个ul无序列表),也可以通过 obj.errors.as_json() 获取一个JSON格式的错误信息:

{"hostname": [{"message": "This field is required.", "code": "required"}],
 "ip": [{"message": "Enter a valid IPv4 address.", "code": "invalid"}],
 "port": [{"message": "Enter a whole number.", "code": "invalid"}],
 "email": [{"message": "Enter a valid email address.", "code": "invalid"}]}

错误信息包括,错误类型(code)和错误信息(message),这里的错误信息也可以自定义,我要中文的。

自定义错误信息

通过参数error_messages设置自定义的错误信息,code的值就是key,然后把你希望的内容填在value里:

from django import forms
class FM(forms.Form):
    # 变量的字段名匹配form表单里的name属性的值,必须一样
    hostname = forms.CharField(
        max_length=12,
        min_length=6,
        error_messages={'required': "设备名不能为空",
                        'max_length': "设备名太长,不能超过12",
                        'min_length': "设备名太短,不能小于6"})
    ip = forms.GenericIPAddressField(protocol='ipv4')
    port = forms.IntegerField()
    email = forms.EmailField(error_messages={'required': "邮箱不能为空", 'invalid': "邮箱格式错误"})

返回错误信息到表单页面

现在使用render方法,把错误信息返回给页面。所有内容都在obj里,把整个obj返回:

return render(request, 'host.html', {'obj': obj})

通过模板语言获取错误信息。这里注意最后要在.0拿到的才是错误信息的内容

<form action="/host/" method="POST">
    {% csrf_token %}
    <p><input type="text" name="hostname" placeholder="主机名">{{ obj.errors.hostname.0 }}</p>
    <p><input type="text" name="ip" placeholder="IP地址">{{ obj.errors.ip.0 }}</p>
    <p><input type="text" name="port" placeholder="端口号">{{ obj.errors.port.0 }}</p>
    <p><input type="text" name="email" placeholder="E-Mail">{{ obj.errors.email.0 }}</p>
    <p><input type="submit" value="登录"></p>
</form>

还有一个问题是,你一点提交你之前在input里填的内容会被清空。这样不好。

自动生成html标签(保留上次输入的信息)

上面讲的都是form组件的一个功能,其实django的form组件主要是完成下面的2个功能的
form组件的2大功能:

  • 验证(显示错误信息)
  • 保留用户上次输入的信息,通过自动生成的html标签实现

自动生成input标签
用下面的方法可以自动生成input标签。form标签这里加上了一个novalidate属性,是为了禁用客户端的表单验证功能,可以直接看到服务端返回的验证信息。换句话说,就是自动生成的标签还帮我么加上了简单的客户端的初步验证的功能:

<form novalidate action="/host/" method="POST">
    {% csrf_token %}
    <p>{{ obj.hostname.label_tag }}{{ obj.hostname }}{{ obj.errors.hostname.0 }}</p>
    <p>{{ obj.ip.label_tag }}{{ obj.ip }}{{ obj.errors.ip.0 }}</p>
    <p>{{ obj.port.label_tag }}{{ obj.port }}{{ obj.errors.port.0 }}</p>
    <p>{{ obj.email.label_tag }}{{ obj.email }}{{ obj.errors.email.0 }}</p>
    <p><input type="submit" value="登录"></p>
</form>

上面只是生成了input标签,里面没有设置placeholder,如果需要加上自定义属性后面会讲。
这里还有这些常用的变量:

  • {{ obj.hostname.label_tag }} :自动生成标签,里面是这个样子的 &lt;label for="id_hostname"&gt;Hostname:&lt;/label&gt;,一个for一个value,可以用下面2个变量单独拿到这2个值。
  • {{ obj.hostname.label }} :标签的value值
  • {{ obj.hostname.id_for_label }} :input标签的id,就是label标签里for的值,有这个可以自己写label标签了
  • {{ obj.hostname.errors }} :错误信息,貌似和{{ obj.errors.hostname.0 }}是一样的,只是存在不同的位置

这里处理函数要注意,因为GET请求也需要返回obj对象(但是GET里不用填参数)给页面了,所以要做如下的修改:

def host(request):
    if request.method == 'GET':
        obj = FM()  # 这里也需要创建一个对象,因为需要它生成标签,但是不需要传参数
        return render(request, 'host.html', {'obj': obj})
    if request.method == 'POST':
        obj = FM(request.POST)  # 实例化,把POST的数据传入
        res = obj.is_valid()  # 获取结果
        if res:
            print(obj.cleaned_data)
            return HttpResponse('OK')
        else:
            # return HttpResponse(str(obj.errors))
            return render(request, 'host.html', {'obj': obj})

自动生成表单
还是推荐用上面的,这个可定制性太差了。
form标签自己写,submit自己写,其他的都不用写。有3中方式:

  • {{ obj.as_p }} :p标签。
  • {{ obj.as_ul }} :生成的是li标签,所以外面应该再包一层ul标签?
  • {{ obj.as_table }} :生成table的tbody标签,所以外面得自己再包一层table标签。
<form novalidate action="/host/" method="POST">
    {% csrf_token %}
    {{ obj.as_p }}
    <p><input type="submit" value="登录"></p>
</form>
<form novalidate action="/host/" method="POST">
    {% csrf_token %}
    {{ obj.as_ul }}
    <p><input type="submit" value="登录"></p>
</form>
<form novalidate action="/host/" method="POST">
    {% csrf_token %}
    <table>
    {{ obj.as_table }}
    </table>
    <p><input type="submit" value="登录"></p>
</form>

HTML插件(widgets)

继续看form组件的2大功能:

  • 验证(显示错误信息),forms.CharField,负责验证
  • 保留用户上次输入的信息,forms.CharField内部包含一个插件widget,负责生成html标签。设了公有属性,成员属性默认参数是None,所以没定义都是取默认的公有属性

插件可以定义标签的类型,默认是文本框,可以改变成多行文本、单选复选,总之是所有的input标签的类型。
插件还可以定义标签的属性,这样就实现了自定义样式。
定义前先要导入插件的模块,from django.forms import widgets

from django.forms import Form  # 这个是要继承的类
from django.forms import fields  # 学到这里,字段导入这个模块
from django.forms import widgets  # 这个模块是插件
class FM(forms.Form):
    # 变量的字段名匹配form表单里的name属性的值,必须一样
    hostname = fields.CharField(
        max_length=12,
        min_length=6,
        error_messages={'required': "设备名不能为空",
                        'max_length': "设备名太长,不能超过12",
                        'min_length': "设备名太短,不能小于6"})
    ip = fields.GenericIPAddressField(
        widget=widgets.Textarea,  # 这里改成多行文本,不定义样式后面就不用跟括号
        protocol='ipv4')
    port = fields.IntegerField(
        widget=widgets.TextInput(attrs={'class': 'c1'})  # 后面跟括号,写上你要自定义的属性
    )
    email = fields.EmailField(error_messages={'required': "邮箱不能为空", 'invalid': "邮箱格式错误"})

学到这里,之前导入模块的方式不太好,明确导入需要的模块。
导入 from django.forms import Form 模块,这个是要继承的类
导入 from django.forms import fields 模块来定义字段,这个 fields 是所有字段类型的基类。开始用的是 forms.CharField 都改成 fields.CharField
导入 from django.forms import widgets 模块来定义插件。如果只定义类型,后面就不用加括号。如果需要自定义属性,就在后面加括号,attrs里以字典的形式写上各种自定义的属性。
密码的input框类型,可以用这个插件 widget=widgets.PasswordInput,
所有的验证类型
查看文件 django\forms\fields.py ,里面有这么多种验证类型。第一个Field是基类,后面的都是继承这个Field的子类或者是孙子类:

__all__ = (
    'Field', 'CharField', 'IntegerField',
    'DateField', 'TimeField', 'DateTimeField', 'DurationField',
    'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField',
    'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
    'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
    'SplitDateTimeField', 'GenericIPAddressField', 'FilePathField',
    'SlugField', 'TypedChoiceField', 'TypedMultipleChoiceField', 'UUIDField',
)

内置字段

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

  • Field
    • required=True,是否允许为空,默认都是必填的,有非必填项置为False
    • widget=None,HTML插件
    • label=None,用于生成Label标签或显示内容,自定义label标签的value的内容,不包括最后的一个冒号
    • label_suffix=None,Label内容后缀,自定义label标签的后缀,应该是拼接到所有label后面生成页面显示的label的value的值,默认是英文的冒号
    • initial=None,初始值,可以为标签设置初始的内容
    • help_text='',帮助信息(在标签旁边显示)
    • error_messages=None,自定义错误信息 {'required': '不能为空', 'invalid': '格式错误'}
    • show_hidden_initial=False,是否在当前插件后面再加一个隐藏的且具有默认值的插件(可用于检验两次输入是否一直)。就是在你的标签的旁边在生成一个隐藏的一模一样的标签,你可以比较用户的标签相对于之前的状态是否变化了。
    • validators=[],自定义验证规则,这里导入一个错误异常,比如RegexValidator。写上自定义的正则表达式和错误信息,可以按自定义的规则进行验证。这是个列表,可以定义多个规则,后面有例子。
    • localize=False,是否支持本地化。比如默认的时间都是UTC时间,设置了本地化,就直接显示的是本地的时间了
    • disabled=False, 是否可以编辑。
  • 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),自定义正则进行验证。和在 fields.CharField 里定义validators是一样的。
    • regex,自定制正则表达式
    • max_length=None,最大长度
    • min_length=None,最小长度
    • error_message=None,忽略,错误信息使用 error_messages={'invalid': '...'}
  • EmailField(CharField)
  • FileField(Field)
    • allow_empty_file=False,是否允许空文件
    • 使用时注意1:form表单中 enctype="multipart/form-data"
    • 使用时注意2:view函数中 obj = MyForm(request.POST, request.FILES)
  • ImageField(FileField)
    • 需要PIL模块,pip3 install Pillow
    • 使用时的注意点同FileField
  • URLField(Field)
  • BooleanField(Field)
  • NullBooleanField(BooleanField)
  • ChoiceField(Field),单选返回值为字符串,多选返回值为列表
    • choices=(),选项,如:choices = ((0,'上海'),(1,'北京'),)
    • required=True,是否必填
    • widget=None,插件,默认 widget=widgets.Select ,还可以SelectMultiple(复选select),widgets.RadioSelect(单选),CheckboxSelectMultiple(多选)
    • 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= "",空值的默认值
  • ComboField(Field)
    • fields=(),使用多个验证。如下,验证最大长度20,又验证邮箱格式
    • fields.ComboField(fields=[fields.CharField(max_length=20), fields.EmailField(),])
  • MultiValueField(Field)
    • 抽象类,只能被继承。子类中可以实现聚合多个字典去匹配一个值,要配合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。protocol必须为both才能启用
  • SlugField(CharField),数字,字母,下划线,减号(连字符)
  • UUIDField(CharField),uuid类型

内置字段的一些例子

Field 属性 validators,自定义验证规则

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

FilePathField(ChoiceField)
把你把一个文件夹下的所有的文件列举出来,然后选一个提交:

from django.forms import Form
from django.forms import fields
class FM(Form):
    folder = fields.FilePathField(path='app01')
def folder(request):
    obj = FM()
    return render(request, 'folder.html', {'obj': obj})

模板语言会自动生成下拉列表:

{{ obj.folder }}

页面上会生成一个下拉框,里面显示的是文件名,后面有个提交按钮。提交的是文件路径。

内置插件

下面这些都是内置的插件。插件后面都可以加上 (attrs={'key': 'value'}) 进行自定制属性:

  • TextInput(Input)
  • NumberInput(TextInput)
  • EmailInput(TextInput)
  • URLInput(TextInput)
  • PasswordInput(TextInput)
  • HiddenInput(TextInput)
  • Textarea(Widget)
  • DateInput(DateTimeBaseInput)
  • DateTimeInput(DateTimeBaseInput)
  • TimeInput(DateTimeBaseInput)
  • CheckboxInput
  • Select
  • NullBooleanSelect
  • SelectMultiple
  • RadioSelect
  • CheckboxSelectMultiple
  • FileInput
  • ClearableFileInput
  • MultipleHiddenInput
  • SplitDateTimeWidget
  • SplitHiddenDateTimeWidget
  • SelectDateWidget

初始化数据

在Web应用程序中开发编写功能时,时常用到获取数据库中的数据并将值初始化在HTML中的标签上。
获取数据后把数据放在一个字典里。如果是数据库查询,ORM可以直接获取字典形式的数据。
之前GET请求里用的是 obj = FM() ,相当于传入空值,现在可以把字典作为参数传入 obj = FM(initial=dic) 。这样页面上使用Form生成的html标签里就有字典里的值了。
直接修改之前的例子,先准备好一个有数据的字典(这里就不查数据库了)。然后只需要传入这个字典就好了,别的都不用改。这样生成的页面里的输入框是会把字典里的值填上的:

# 实际使用的时候,通过ORM可以直接获取到一条记录的字典
dic = {
    'hostname': "HOST1",
    'ip': '192.168.2.1',
    'port': 23,
    'email': 'py@dj.cn'
}
def host(request):
    if request.method == 'GET':
        # obj = FM()  # 之前是不传参数的,所有输入框里都是空的
        obj = FM(initial=dic)  # 现在传入参数,输入框里就会把字典里的值作为默认值填上
        return render(request, 'host.html', {'obj': obj})
    if request.method == 'POST':
        obj = FM(request.POST)  # 实例化,把POST的数据传入
        res = obj.is_valid()  # 获取结果
        if res:
            print(obj.cleaned_data)
            return HttpResponse('OK')
        else:
            # return HttpResponse(str(obj.errors))
            return render(request, 'host.html', {'obj': obj})

直接像这样 obj = FM(dic) 传字典也是可以的,或者这样 obj = FM({'hostname': "HOST1", 'ip': '192.168.2.1',}) 。至少效果是一样的。这里看着按位置参数传递的话,参数不是传给initial的。

序列化操作(待补充)

上面都是用form表单来提交的。之前还学过用Ajax提交。提交没有问题,但是Ajax提交返回的要求是字符串。或者是字典、列表可以用JSON序列化为字符串。但是这里返回的是 obj = FM() 对象,如何序列化成字符串返回给Ajax?