Django Form源码分析之Field验证逻辑

来源:互联网 发布:淘宝助手安卓版 编辑:程序博客网 时间:2024/05/21 07:04

引言

在上一篇对BaseForm的分析中,我只提及了在Form层次的输入验证,在Form.full_clean()主要调用的两个函数self._clean_field(), self._clean_form()。其中,self._clean_field方法代表了Field层次的输入验证。
在Django官方文档中,验证逻辑依次按照如下流程图:

Created with Raphaël 2.1.0field.to_python()field.validate()field.run_validators()field.clean()form.clean_<field_name>()form.clean()

其中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__()

Form._clean_field()

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()。

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()捕捉。

Field.to_python()

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

Field.validate()

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开始:

# IntegerFieldclass IntegerField(Field):    widget = NumberInput    default_error_messages = {        'invalid': _('Enter a whole number.'),    }# Fieldclass 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方法了。

Field.run_validators()

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。(该方法推荐不应该重载)

Form.clean_<field_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)

在调用了field.clean()之后,通过hasattr方法判定开发者是否有针对各字段定义验证。
至此,整个Form表单验证的流程逻辑就清晰了。

0 0
原创粉丝点击