Django表单验证

  1. 普通表单验证;
  2. 模型表单验证;

一、普通表单验证

普通表单验证其实就是针对不同的Field字段进行针对性的验证。一般都是分为几个步骤:

  1. Field的to_python方法;表单value转换为python对象,每个Field子类都重构了该方法;
  2. Field的validate方法;对于这个方法,官网给出的说法是: 当你遇到不可以或不想放在validator 中的验证逻辑时,应该覆盖它来处理验证。
  3. Field的run_validator方法; 这步骤主要是验证各种验证器

Field的clean方法会按顺序执行这三个方法:

  def to_python(self, value):
        return value

    def validate(self, value):
        if value in self.empty_values and self.required:
            raise ValidationError(self.error_messages['required'], code='required')

    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)

    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

BaseForm实现了is_valid基本方法:


@html_safe
@python_2_unicode_compatible
class BaseForm(object):

..........

    @property
    def errors(self):
        "Returns an ErrorDict for the data provided for the form"
        if self._errors is None:
            self.full_clean()
        return self._errors

    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

代码很明显,is_valid访问self.errors,该方法是被描述符property修饰的,它会调用full_clean法,看代码:

class BaseForm:

    def full_clean(self):
       
        self._errors = ErrorDict()
       
        self.cleaned_data = {}
        
        self._clean_fields()
        self._clean_form()
        self._post_clean()

form的_clean_field最终会调用Field的clean方法:

     def _clean_fields(self):
       try:

                value = field.clean(value)
                self.cleaned_data[name] = value
            
            except ValidationError as e:
                self.add_error(name, e)

对于开发者,我们可以重构_post_clean等方法,或者自定义验证器等。

综述:代码调用情况:当form调用is_valid的时候最终就会调用Field的clean方法。调用顺序is_valid-->errors-->full_clean-->clean_fields。

其实到这,表单验证过程,还没有完呢,还有一个表单的clean_fieldname方法。该 方法完成于特定属性相关的验证,这个验证与字段的类型无关 。我们在下面讲。

二、模型表单验证

模型表单验证主要经历两个过程,一是和普通表单一样的表单验证,然后是模型验证。

普通表单在上面已经说了,主要说模型验证。

验证一个模型,要经历三个步骤:

  1. 验证模型的字段 Models.clean_fields();
  2. 验证模型的完整性 Models.clean();
  3. 验证模型的唯一性 Models.validate_unique()。

验证模型的字段,包括required,invalid,max_length等等是否有效;

验证唯一性是如果有些字段是唯一健,或者主键,那么如果填写字段颖存在了,就会抛出错误,看代码:

def unique_error_message(self, model_class, unique_check):
        opts = model_class._meta

        params = {
            'model': self,
            'model_class': model_class,
            'model_name': six.text_type(capfirst(opts.verbose_name)),
            'unique_check': unique_check,
        }

        # A unique field
        if len(unique_check) == 1:
            field = opts.get_field(unique_check[0])
            params['field_label'] = six.text_type(capfirst(field.verbose_name))
            return ValidationError(
                message=field.error_messages['unique'],
                code='unique',
                params=params,
            )

如果字段值已经出现过,那么就会抛出一个验证错误,看上面代码,返回的信息是field字段的errormessage,因此你完全可以自定义此类消息的内容。我自己在项目中也定义了。当用户名已经存在的时候,就会抛出错误信息:


class CustomUserCreationForm(UserCreationForm):
   
    error_messages = {
        'password_mismatch': u"两次新密码不匹配",
   }
   
    class Meta(UserCreationForm.Meta):
        model = User
        fields = UserCreationForm.Meta.fields + ('email',)
        error_messages = {
            'username': {
                    'unique': u"用户名已经注册,请更改用户名",
                   },
        }

当你填写一个已经存在的用户名的时候,虽然表单验证通过了,但模型验证未通过,这时就会抛出错误信息了。

在上面的普通表单验证中,我们提到了类方法调用关系,当在视图类中调用form.is_valid之后,就会调用form的full_clean方法,然后该方法调用了_post_clean,ModelForm对此进行了重构,看下源代码:

ModelForm继承自BaseForm,,ModelForm对此方法进行了重构:

class BaseModelForm:

 def _post_clean(self):
        opts = self._meta

        try:
            self.instance.full_clean(exclude=exclude, validate_unique=False)
        except ValidationError as e:
            self._update_errors(e)

Bingo!这段代码显示调用了模型实例的full_clean方法,full_clean方法将会执行clean_fields,clean,validate_unique等方法进行验证。:

来吧,让我们看下Model的代码吧:

Class Model:
 
   def full_clean(self, exclude=None, validate_unique=True):
        """
        Calls clean_fields, clean, and validate_unique, on the model,
        and raises a ``ValidationError`` for any errors that occurred.
        """
        errors = {}
        if exclude is None:
            exclude = []
        else:
            exclude = list(exclude)

        try:
            self.clean_fields(exclude=exclude)
        except ValidationError as e:
            errors = e.update_error_dict(errors)

        # Form.clean() is run even if other validation fails, so do the
        # same with Model.clean() for consistency.
        try:
            self.clean()
        except ValidationError as e:
            errors = e.update_error_dict(errors)

        # Run unique checks, but only for fields that passed validation.
        if validate_unique:
            for name in errors.keys():
                if name != NON_FIELD_ERRORS and name not in exclude:
                    exclude.append(name)
            try:
                self.validate_unique(exclude=exclude)
            except ValidationError as e:
                errors = e.update_error_dict(errors)

        if errors:
            raise ValidationError(errors)

上面讲了一大堆关于表单验证和模型验证的,细心的人可能会注意到,你一般都是对单个字段的验证,如果我还想验证和字段本身类型无关,而是字段间某种关联性的时候,该怎么办呢?

就比如说,上面的密码,两次密码要一直,不一致就会抛出错误,那到底是怎么实现的呢?其实就是用到了在第一小节提到的clean_fieldname方法。

在UserCreattionForm有一个方法是clean_password2方法:


class UserCreationForm(forms.ModelForm):

    def clean_password2(self):
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise forms.ValidationError(
                self.error_messages['password_mismatch'],
                code='password_mismatch',
            )
       

该方法是用来验证密码是否一致,如果不一致就抛出错误。那该方法是如何调用的呢?

在上面我们说到,form.is_valid()会调用_clean_field,除了处理单个字段,还有一段代码:

def _clean_fields(self):
           try:
              for name, field in self.fields.items():
           
                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)

看到没?hasattr(self,'clean_%s'%name)

如果存在clean_fieldname,就会执行该方法进行验证。牛逼吧?!!!

Django真的是太牛逼了!!!

再说下User的密码验证。Django提供了内置的验证器,具体配置在settings.py中:

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

User在save时,会根据配置的密码验证器来验证密码,看一下源代码:

def get_password_validators(validator_config):
    validators = []
    for validator in validator_config:
        try:
            klass = import_string(validator['NAME'])
        except ImportError:
            msg = "The module in NAME could not be imported: %s. Check your AUTH_PASSWORD_VALIDATORS setting."
            raise ImproperlyConfigured(msg % validator['NAME'])
        validators.append(klass(**validator.get('OPTIONS', {})))

    return validators


def validate_password(password, user=None, password_validators=None):
    """
    Validate whether the password meets all validator requirements.

    If the password is valid, return ``None``.
    If the password is invalid, raise ValidationError with all error messages.
    """
    errors = []
    if password_validators is None:
        password_validators = get_default_password_validators()
    for validator in password_validators:
        try:
            validator.validate(password, user)
        except ValidationError as error:
            errors.append(error)
    if errors:
        raise ValidationError(errors)

内置的密码验证器,包括长度最短验证等:

  • UserAttributeSimilarityValidator , 验证和别的用户的相似性
  • MinimumLengthValidator , 验证最短长度,默认是9.
  • CommonPasswordValidator ,验证师傅出现在常见的比较差的密码列表中。
  • NumericPasswordValidator ,验证是否都是数字,不能裙式数字

你自己完全可以自定义的。我写了一个MinimumLengthValidator的子类,就是我想用汉语。


from django.contrib.auth.password_validation import MinimumLengthValidator
from django.core.exceptions import ImproperlyConfigured, ValidationError


class MinLenValidator(MinimumLengthValidator):

    def validate(self, password, user=None):
        if len(password) < self.min_length:
            raise ValidationError(
                '密码长度太短,至少要8个字符!',
                code='password_too_short',
                params={'min_length': self.min_length},
            )

其实很简单,除了Django内置的验证器,你可以自定义任何你想要的验证器。

--------EOF---------
微信分享/微信扫码阅读