对于用户输入前后端都是需要做校验的;按照以前的话我们需要对每一个输入进行校验,这样当输入内容增多,复杂度也会增加。对于这种情景Django为我们提供了form组件。
通过这篇文章,你能了解到:
- form组件是什么?
- 局部钩子与全局钩子的定义。
- 字段、字段参数、widget插件、后端API等表格。
- 源码剖析 form组件是如何校验的。
form组件
对于一个form我们需要做很多事情:不同的类型的数据要有不同的渲染;校验数据;获取检验后的干净数据,并将数据反序列化为相应数据类型如时间对象;保存传递给处理程序等等。
Django的forms组件就完成了这些复杂的工作,提供方便的操作form的接口API给我们。
form组件的主要功能
- 生成页面可用的HTML标签
- 对用户提交的数据进行校验
- 保留上次输入内容(源码剖析中会提到)
为什么要用form组件
以前的方式
- 写form表单以及所需input框
1 | # reg1.html |
- 获取数据,然后写校验规则
1 | # views.py |
缺点:当需要校验的内容多了,就会复杂
引例
生成页面可用的HTML标签
- 与前面相比,只需要在form表单内写待渲染的字段就可以
1 | # reg2.html |
对用户提交的数据进行校验
- 定义一个继承Form类的子类,很像orm中定义model的方式,我们定义form表单内相关元素(input,radio等等)的字段,字段参数是我们对该字段定义的规则。
- 对于GET请求返回页面的时候,只需要实例化定义的这个Form类
- 对于POST请求,将
request.POST
这个QueryDict作为参数重新实例化,有了这个对象就可以进行校验 - 通过
对象.is_valid()
的方法来进行校验 - 如果用户输入不符合该字段的规则,就会有提示产生。
1 | #forms.py |
通过这个例子我们就要开始学习django为我们提供的这个方便工具。
完善引例
我们显然需要更加细粒度的操作:
- 我们想要定义label的名字;错误提示信息;
- 对提交内容自定义校验;
- 使用全局钩子来做密码与确认密码的一致性检验;
- 当注册成功后,注册下一个的时候信息已经更新,而不是重启项目来更新数据。
渲染
自动渲染
引例中使用的 {{form_obj.as_p }} 是自动渲染的方式,按照P段落渲染,将所有的表单元素包裹在P标签内,效果如下图所示:
更详细的自动渲染见后文 FORM后端API
自定义错误信息
- 在该字段的参数内覆写error_messages
1 | user = forms.CharField( |
手动渲染
- 自定义p标签和label标签,label标签的内容为 {{ field.label }}
- 标签后跟 {{ field }}
- span标签内放产生错误的信息 {{ field.errors.0 }} 由于本例中错误只会产生一个,取第一个就可以
- 这样这样变量就会被渲染
1 | <form action="" method="post" novalidate> |
- 完善常用注册需要的字段
- 使用 is_valid方法进行校验
校验
内置校验器
- 使用内置校验器RegexValidator 完成对手机号的校验
1 | from django.core.validators import RegexValidator |
自定义校验器
字段参数中有一个是validators 这个参数是自定义校验器需要附加的。
1 | # views.py |
钩子
局部钩子
局部钩子就是在子类中重写父类的 clean_field
方法
没有参数,通过self.cleaned_data来获取字段值
如果不通过建议抛出ValidationError
通过校验必须返回当前字段的值
例子:
- 继续写对手机的校验
1 | class RegForm(forms.Form): |
为什么要返回当前字段的值呢?
1 | def _clean_fields(self): |
更多细节见后文源码剖析。
全局钩子
定义全局钩子:
- 没有参数,通过self.cleaned_data来获取字段值
例子:判断密码和确认密码是否一致
1 | class RegForm(forms.Form): |
随着数据库更新而更新
有些字段是会随着数据库的更新而更新
例如:ModelChoiceField(QuerySet)
- 在定义一个字段
1 | # views.py |
- models中写写入这个类
1 | # models.py |
- 这样页面中的信息会随着数据库中该字段的改变而改变
表格汇总
常用字段
字段名称 | 默认插件 | 错误使用参数 | 解释 |
---|---|---|---|
BooleanField | CheckboxInput | required | 所有field的子类默认都设置了required=True.而布尔字段没有选中意味着False,会触发requird错误。 因此在布尔字段上要特别设置required=False |
CharField | TextInput | required, max_length, min_length | 得到一个unicode字符串, 如果设置最大和最小长度,在HTML内就会验证.如果不设置最大和最小,任何输入都可以通过验证 参数strip,默认为True,表示去除输入前后空格. |
ChoiceField | Select | required, invalid_choice | 用于单选的字段,更改默认的widget时候必须注意搭配, choices的参数必须是可迭代的序列,每一个元素是一个2个元素的元组,第一个元素是值,第二个是显示的内容.如choices=((1, ‘男’), (2, ‘女’)) |
DateField | DateInput | required, invalid | 返回一个Python的 datetime.date 对象,HTML表现形式是一个日期输入框.可以用input_formats字符串格式化参数指定具体样式 forms.DateField(input_formats=[‘%Y-%m’]) |
DateTimeField | DateTimeInput | required, invalid | 与DateField类似.也有input_formats字符串格式化参数 如input_formats=[‘%Y-%m’] |
DecimalField | NumberInput或TextInput | required, invalid, max_value, min_value, max_digits, max_decimal_places, max_whole_digits | 十进制浮点数字段,返回Python的decimal对象,可选参数是最大值,最小值,最大位数,最大小数位数 |
DurationField | TextInput | required, invalid | 返回一个Python timedelta对象,表示间隔. |
EmailField | EmailInput | required, invalid | 返回unicode字符串的邮件地址.可选参数是 max_length min_length. |
FileField | ClearableFileInput | required, invalid, missing, empty, max_length | 上传文件.返回一个Uploadfile对象,包含文件名和文件内容.两个可选参数max_length和allow_empty_file.上传文件的时候还需要对form元素进行设置. |
FilePathField | Select | required, invalid, max_value, min_value | 选择文件上传,有一个必须参数path来指定想要开始选择的目录.具体看这里 |
FloatField | NumberInput或TextInput | required, invalid | 可选参数为max_value 和 min_value,控制最大和最小值. |
ImageField | ClearableFileInput | required, invalid, missing, empty, invalid_image | 与上传文件类似,但使用ImageField需要安装pillow库. |
IntegerField | NumberInput或TextInput | required, invalid, max_value, min_value | 可选参数是max_value 和 min_value |
MultipleChoiceField | SelectMultiple | required, invalid_choice, invalid_list | 使用choices属性传入选择项.用于多选.更改默认对应的widget时候注意搭配 |
所有字段可在 django.forms.fields
下查看
1 | # django.forms.fields.py |
字段参数
验证相关的条件
参考Core field arguments 核心字段属性,这些Core field arguments是建立Form对象里的fields时一定要包含的属性
属性名 | 解释 |
---|---|
Field.required | 默认设置为True,表示一定要输入内容,None或者空字符串都会引发错误. |
Field.label | 用于生成HTML代码对应该输入元素的label标签的text内容. |
Field.label_suffix | 用于覆盖整个表单级别的label_suffix,就是给label的text部分加上后缀 |
Field.initial | 设置初始化的值,也就是设置标签的value属性. 注意,不同的field,initial需要被设置成对应的对象,比如时间字段就必须用datetime系列对象赋值给initial属性 |
Field.widget | 设置对应的widget类,用于控制具体的HTML代码 |
Field.help_text | 在HTML中显示帮助文本信息 |
Field.error_messages | 用于覆盖默认的错误信息,需要采用error_messages={‘required’: ‘Please enter your name’}类似的方法来传入,前边的键就是错误键的名称,值是自定义的错误信息. |
Field.validators | 选择针对该字段的验证器,验证器的详细看这里 |
Field.localize | 和本地化有关,控制结果的本地化输出. |
Field.disabled | 设置表单元素的属性是否为disabled |
Field.has_changed() | 检测元素的值是否从initial值发生了变化,返回布尔类型. |
Field.choices | choices的参数必须是可迭代的序列,每一个元素是一个2个元素的元组,第一个元素是值,第二个是显示的内容.如choices=((1, ‘男’), (2, ‘女’)) |
Field.min_length | 字段最短长度 |
Field.max_length | 字段最长长度 |
error_messages中的key
null
, blank
, invalid
, invalid_choice
, unique
, unique_for_date
and invalid_date
内置validators
校验器 | 解释 |
---|---|
RegexValidator ( regex=None, message=None , code=None,…) | 正则校验器 regex正则表达式 message:错误提示 code用于覆盖原来 |
EmailValidator ( message=None ) | 邮箱校验器 message:错误提示 |
URLValidator (schemes=None*, regex=None**,** *message=None,…) | URL校验器 默认schemes是 ['http', 'https', 'ftp', 'ftps'] |
validate_email | 一个 EmailValidator 的不带任何参数的实例 |
validate_slug | 一个正则表达式验证器的实例,仅能验证字母,数字,减号和下划线的组合 |
validate_unicode_slug | 一个正则表达式验证器的实例,仅能验证UNICODE的字母,数字,减号和下划线的组合 |
validate_ipv4_address | 一个正则表达式验证器的实例,验证合法的ipv4地址 |
validate_ipv6_address | 这是一个用了django.utils.ipv6 模块的ipv6地址的验证器 |
validate_ipv46_address | 实际上是同时使用了前边两个验证器的实例 |
validate_comma_separated_integer_list | 一个正则表达式验证器的实例,验证逗号分割的数字 |
MaxValueValidator(max_value, message=None) | 最大值验证器,max_value参数默认使用该验证器 |
MinValueValidator(min_value, message=None) | 最小值验证器 |
MaxLengthValidator(max_length, message=None) | 最大长度验证器 |
MinLengthValidator(min_length, message=None | 最小长度验证器 |
DecimalValidator(max_digits, decimal_places) | Decimal类型验证器 max_digits 是总的最长位数 decimal_places 是小数的位数 |
更多详见 django-validators
Widgets 插件
渲染成想要的HTML。
widget,官方文档的原话是: A widget is Django’s representation of an HTML input element.也就是说一个插件就对应着一段HTML代码.
通过fields可以知道要拿到哪一种数据类型,通过字段参数可以得到验证相关的条件,widget则是最后一步,即将字段的逻辑通过HTML展示出来.同时widget也有各种属性可以设置,用于更好的控制具体HTML代码.
所有的widget类都继承自 Widget 和 MultiWidget 两个类,其中Widget有attrs属性,用来设置HTML标签的各种属性,常用的是设置css类从而应用样式.
内建的Widget类 | |
---|---|
类名 | 解释 |
TextInput | 输入类型是text,渲染的时候按照<input type=”text” …>渲染 |
NumberInput | 输入类型是number,渲染的时候是number类型的input标签 |
EmailInput | 渲染的时候是email类型的input标签 |
URLInput | URL类型的input标签 |
PasswordInput | password类型的input标签,可以带一个额外属性是render_value,表示验证失败之后填写在密码框内的值,默认是False即保留原来的值 |
HiddenInput | 类型是hidden 的input标签 |
DateInput | 类型是text的input标签,可以使用额外参数format来控制格式化 |
DateTimeInput | 类型是text的input标签,同样有format属性 |
TimeInput | 类型是text的input标签,同样有format属性 |
Textarea | 渲染为textarea标签 |
CheckboxInput | 渲染为checkbox对象,有一个调用方法是check_test,检查是否应该选中这个值 |
Select | 渲染为select及内嵌的option标签.有choices属性用于设置各个选项 |
SelectMultiple | 多选,渲染为<select multiple="multiple"> |
RadioSelect | 渲染成一个ul,每个li内部包含一个radio类型的input,模板内的标签使用方法比较多,具体看这里 |
CheckboxSelectMultiple | 渲染成一个ul,每个li内包含一个类型是checkbox的input标签 |
FileInput | 渲染成<input type=”file” …> |
例如:
1 | gender = forms.ChoiceField(choices=((1, '男'), (2, '女')),widget=forms.RadioSelect) |
form后端API
Form API | |
---|---|
属性或方法名 | 解释 |
Form.is_bound | 如果没有任何数据传入而新建Form对象,这是一个没有绑定的Form对象,如果传入了数据比如request.POST,这就是一个绑定了一个具体表单的数据,这个方法返回Form对象是否是一个绑定的对象 |
Form.clean() | 执行校验;使用clean方法意味着调用is_valid() 方法然后返回一个布尔值 |
Form.errors | 返回错误键与错误内容的字典.调用该属性和is_valid()方法都会触发对Form对象的校验. |
Form.errors.as_data() | 错误键不变,值变成原始的错误对象 |
Form.errors.as_json(escape_html=False) | 将错误序列化为JSON对象,可加上 escape_html=True进行转义以便直接在HTML内使用 |
Form.initial | 用字典的形式设置初始值,如果Form对象通过initial属性和字段的initial属性都设置了初始值,以Form对象的优先. |
Form.get_initial_for_field(field, field_name) | 取得初始值,按照先取Form.initial,再取fields.initial的顺序,如果初始值需要求值也会被求值. |
Form.has_changed() | 整个表单的初始值是否改变,需要先设置Form的initial属性,然后调用该方法即可查看是否改变. |
Form.changed_data | 返回一个列表,包含所有与初始值有变化的字段名称. |
Form.fields | 直接用对象的字段变量名就可以访问该字段.之后再用field的那些arguments就可以访问字段的各种属性 |
Form.cleaned_data | 当is_valid()或其他触发验证的动作实行后,如果通过了验证,则所有的数据会被包含在这个属性对应的一个字典里.而且所有的数据都被整理过,比如从前边可以知道,时间类型默认对应的widget是text类型,但是在cleaned_data中,时间类型的数据会被整理成datetime类型.其他的数据类型可以参考field部分的表格. |
Form.as_p() | 按照P段落渲染,将所有的表单元素包裹在P标签内.改变的是直接print(Form对象)的结果. |
Form.as_ul() | 将每一个表单元素放进一个ul的li元素中,影响print结果 |
Form.as_table() | 包裹在tr th标签里,但是table元素需要页面来提供,一般不采用该方法. |
Form.label_suffix | 这个属性的内容会在渲染的时候追加到所有的label 的text内容之后. |
Form.use_required_attribute | 这个属性被设置成True的时候,所有必须填写的表单元素标签内都会带有required 的HTML 5 属性. |
在模板内使用Form对象
- 表单的关键,是展示提示,输入框以及错误信息.Form对象如何在HTML中展示
自动渲染
自动渲染就是一次性将整个form按照某种形式渲染出来,不单独操作表单的各个元素.
- {{ form.as_table }}
- {{ form.as_p }}
- {{ form.as_ul }}
如果在对象内不做任何设置,那么元素的id会被自动设置成id_属性名.这种方法可自定义的部分较少,需要后期慢慢配样式.一般采用第二种方法.
手动渲染
手动渲染就是将传入模板的form对象的各个字段和错误信息取出,自行编写.
- {{ form.name_of_field }} 表示渲染表单中的一个输入元素.
- {{ form.name_of_field.label }}表示 该字段对应的label标签.
- {{ form.name_of_field.errors }}表示经过验证后的该字段对应的错误消息.由于错误信息只会同时有一个,所以一般用{{ form.name_of_field.errors.0 }}取出错误信息.
例如:
1 | <p> |
模板内的操作列表
- user字段 = form_obj.user
Form对象在模板内的操作 | |
---|---|
tag名称 | 解释 |
字段的label属性的内容,就是一个字符串 | |
一个完整的label标签,推荐使用该tag与field搭配 | |
这个字段使用的id | |
字段的值,提交表单之后会动态根据当前值改变 | |
html的name属性值 | |
帮助信息 | |
当前字段的错误信息,如果验证通过则不会有错误信息.可以对其迭代取出所有错误或者用.0取第一个错误内容 如何过当前对象的话就是所有字段的错误 | |
判断当前字段是否是隐藏的 | |
这里注意之前的是field的属性,这里是form的属性,表示表单内的全部hidden字段,可以迭代取出具体字段 | |
这个是所有的可视字段. |
推荐阅读 Django 14 Django进阶-Django Form组件
源码剖析
第一块
回到最开始!我们使用 is_valid() 进行校验 点击进入
- is_valid 返回布尔值
1 | # django\forms\forms.py |
点击进入 self.errors
1 |
|
点击进入full_clean
- self._errors 是一个存放错误信息的字典
- self.cleaned_data 是一个存放通过校验的数据字典
1 | def full_clean(self): |
第二块
点击进入_clean_fields
1 | def _clean_fields(self): |
点击 value = field.clean(value) 进入clean
1 | def clean(self, value): |
点击进入validate
1 | def validate(self, value): |
返回,然后点击进入run_validators
- 对一个字段,遍历这个字段的校验器并执行
- 校验器出现异常会抛出ValidationError 异常
1 | def run_validators(self, value): |
返回到_clean_fields这里
self.cleaned_data[name] = value
是为了给局部钩子使用的,因为局部钩子没有传参clean_%s 就是我们定义局部钩子的所在,为什么局部钩子必须返回该字段的值呢? 因为 成功的话会给cleaned_data添加这个键值对。
1
2value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
点击进入 add_error
- 对于未绑定字段如
__all__
,就会存放在non_field_errors中 - 删除未通过局部钩子的无效字段
del self.cleaned_data[field]
1 | def add_error(self, field, error): |
关于_clean_fields就结束了
第三块
返回到full_clean 中,点击进入_clean_form
1 | def _clean_form(self): |
点击进入clean
- 这里就是我们定义全局钩子的地方啦
1 | def clean(self): |
返回_clean_form , 由于 使用cleaned_data接收结果,所以我们在定义全局钩子的时候当校验成功必须返回cleaned_data。所以这里的思想就是我们要对通过校验的字段,再一次使用全局钩子进行校验。
再次返回到full_clean 中,点击进入_post_clean
- 这也是一个额外的钩子,在form cleaning之后再次进行校验,用于与model进行校验
1 | def _post_clean(self): |
到此,我们的full_clean 方法执行完毕,errors方法执行完毕,返回错误信息这个字典(ErrorDict)
到此is_valid执行结束。
小结
- is_valid()中要执行full_clean():
- self._errors ={} 定义一个存放错误信息的字典
- self.cleaned_data = {} # 定义一个存放有效的数据
- 执行self._clean_fields()
- 先执行内置的校验和校验器的校验
- 有局部钩子,执行局部钩子
- 执行 self.clean() 全局钩子