在上一篇对BaseForm的分析中,我只提及了在Form层次的输入验证,在Form.full_clean()主要调用的两个函数self._clean_field(), self._clean_form()。其中,self._clean_field方法代表了Field层次的输入验证。
在Django官方文档中,验证逻辑依次按照如下流程图:
其中self._clean_form对应着form.clean()。
同样,在引言中给出Field的初始化方便查阅
def __init__(self, required=True, widget=None, label=None, initial=None,
help_text='', error_messages=None, show_hidden_initial=False,
validators=[], localize=False, disabled=False, label_suffix=None):
# required -- Boolean that specifies whether the field is required.
# True by default.
# widget -- A Widget class, or instance of a Widget class, that should
# be used for this Field when displaying it. Each Field has a
# default Widget that it'll use if you don't specify this. In
# most cases, the default widget is TextInput.
# label -- A verbose name for this field, for use in displaying this
# field in a form. By default, Django will use a "pretty"
# version of the form field name, if the Field is part of a
# Form.
# initial -- A value to use in this Field's initial display. This value
# is *not* used as a fallback if data isn't given.
# help_text -- An optional string to use as "help text" for this Field.
# error_messages -- An optional dictionary to override the default
# messages that the field will raise.
# show_hidden_initial -- Boolean that specifies if it is needed to render a
# hidden widget with initial value after widget.
# validators -- List of additional validators to use
# localize -- Boolean that specifies if the field should be localized.
# disabled -- Boolean that specifies whether the field is disabled, that
# is its widget is shown in the form but not editable.
# label_suffix -- Suffix to be added to the label. Overrides
# form's label_suffix.
self.required, self.label, self.initial = required, label, initial
self.show_hidden_initial = show_hidden_initial
self.help_text = help_text
self.disabled = disabled
self.label_suffix = label_suffix
widget = widget or self.widget
if isinstance(widget, type):
widget = widget()
# Trigger the localization machinery if needed.
self.localize = localize
if self.localize:
widget.is_localized = True
# Let the widget know whether it should display as required.
widget.is_required = self.required
# Hook into self.widget_attrs() for any Field-specific HTML attributes.
extra_attrs = self.widget_attrs(widget)
if extra_attrs:
widget.attrs.update(extra_attrs)
self.widget = widget
# Increase the creation counter, and save our local copy.
self.creation_counter = Field.creation_counter
Field.creation_counter += 1
messages = {}
for c in reversed(self.__class__.__mro__):
messages.update(getattr(c, 'default_error_messages', {}))
messages.update(error_messages or {})
self.error_messages = messages
self.validators = self.default_validators + validators
super(Field, self).__init__()
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.
if field.disabled:
value = self.initial.get(name, field.initial)
else:
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, FileField):
initial = self.initial.get(name, field.initial)
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)
在Form中self.fields是一个OrderDict结构,可以近似地看为一个key为field的声明名字(string),value为Field(class)的一个dict。
首先看一下Field.disabled这个属性,在初始化的时候默认为False,这个属性是什么意思呢?
# disabled -- Boolean that specifies whether the field is disabled, that
# is its widget is shown in the form but not editable.
当清楚这个属性的作用后,再看判断的逻辑:
if field.disabled:
value = self.initial.get(name, field.initial)
else:
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
即当Filed不可用的时候,value会先从form.initial中获取,如果没有则会从field.initial中获取,这也是在对BaseForm的源码分析中,form.initial的优先级比field.initial高的缘故。(对initial参数的意义有疑惑的也应该去参考一下)
当Field可用的时候,将直接从self.data中获取相应的key。
def value_from_datadict(self, data, files, name):
"""
Given a dictionary of data and this widget's name, returns the value
of this widget. Returns None if it's not provided.
"""
return data.get(name)
字段在检查完可用性和初始值之后开始调用Field.clean()。
def clean(self, value):
"""
Validates the given value and returns its "cleaned" value as an
appropriate Python object.
Raises ValidationError for any errors.
"""
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return value
在field.clean()方法中依次调用了field.to_python(), field.validate(), field.run_validators。同时,在这四个方法中raise的ValidationError都会被form._clean_field()捕捉。
def to_python(self, value):
return value
to_python()主要用于返回正确的python类型,在Field中的实现非常简单,但是在不同的Field中可能会有不同的实现,如CharField:
def to_python(self, value):
"Returns a Unicode object."
if value in self.empty_values:
return ''
value = force_text(value)
if self.strip:
value = value.strip()
return value
def validate(self, value):
if value in self.empty_values and self.required:
raise ValidationError(self.error_messages['required'], code='required')
validate()主要验证field的require属性,如果required为True而value却为一个空值就会引发ValidationError。那么,如果我想更改field的required验证输出的错误信息该怎么办?
首先,要先清楚self.error_messages是如何初始化的:
messages = {}
for c in reversed(self.__class__.__mro__):
messages.update(getattr(c, 'default_error_messages', {}))
messages.update(error_messages or {})
self.error_messages = messages
MRO全名为Method Resolution Order,Python在进行多重继承(关于调用super的时候发生了什么可以参考这篇文章)的时候实例调用方法时会按照MRO的list顺序依次向上寻找,MRO的list是由C3算法计算而成,可以参考这篇文章。那么在这里是什么意思呢?
可以看到,self.__classs__.__mro__本来应该返回实例继承从最近的基类到最远的基类的list,因为使用了reversed方法所以从最顶端的基类开始。(访问class的__mro__以及调用mro()都能得到mro列表)message依次更新每个类下的default_error_messages属性,而这个属性将会是一个字典。
假设从IntegerField开始:
# IntegerField
class IntegerField(Field):
widget = NumberInput
default_error_messages = {
'invalid': _('Enter a whole number.'),
}
# Field
class Field(six.with_metaclass(RenameFieldMethods, object)):
widget = TextInput # Default widget to use when rendering this type of Field.
hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden".
default_validators = [] # Default set of validators
# Add an 'invalid' entry to default_error_message if you want a specific
# field error message not raised by the field validators.
default_error_messages = {
'required': _('This field is required.'),
}
message先更新Field的default_error_messages,接着是IntegerField。最后message更新初始化参数中的error_message。
因此,如果我想更改field中默认的required属性,只需在参数中传入含有key为required的error_message字典,如果你想更改require验证的code属性,就只能重写validate方法了。
def run_validators(self, value):
if value in self.empty_values:
return
errors = []
for v in self.validators:
try:
v(value)
except ValidationError as e:
if hasattr(e, 'code') and e.code in self.error_messages:
e.message = self.error_messages[e.code]
errors.extend(e.error_list)
if errors:
raise ValidationError(errors)
Field.run_validators()依次调用self.validators中的由开发者自定义的validator。(该方法推荐不应该重载)
最后是针对特定字段的验证逻辑,这又是如何实现的呢?
try:
if isinstance(field, FileField):
initial = self.initial.get(name, field.initial)
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)
在调用了field.clean()之后,通过hasattr方法判定开发者是否有针对各字段定义验证。
至此,整个Form表单验证的流程逻辑就清晰了。