@@ -635,6 +635,9 @@ def validate(self, attrs):
635635 for k , v in attrs .items ():
636636 if k not in exclusions and k != 'canonical_address_port' :
637637 setattr (obj , k , v )
638+ # Call validators which need the model object for validation.
639+ self .validate_with_obj (attrs , obj )
640+ #
638641 obj .full_clean (exclude = exclusions )
639642 # full_clean may modify values on the instance; copy those changes
640643 # back to attrs so they are saved.
@@ -663,6 +666,23 @@ def validate(self, attrs):
663666 raise ValidationError (d )
664667 return attrs
665668
669+ def validate_with_obj (self , attrs , obj ):
670+ """
671+ Overwrite this if you need the model instance for your validation.
672+
673+ :param dict attrs: The names and values of the model form fields.
674+ :raise django.core.exceptionsValidationError: Raise this if your
675+ validation fails.
676+
677+ To make the error shown at the respective form field, instantiate
678+ the Exception with a dict containing the field name as key and the
679+ error message as value.
680+
681+ Example: ``ValidationError({"password": "Not good enough!"})``
682+ :return: None
683+ """
684+ return
685+
666686 def reverse (self , * args , ** kwargs ):
667687 kwargs ['request' ] = self .context .get ('request' )
668688 return reverse (* args , ** kwargs )
@@ -1006,15 +1026,15 @@ def validate_password(self, value):
10061026
10071027 return value
10081028
1009- def validate (self , attrs ):
1029+ def validate_with_obj (self , attrs , obj ):
10101030 """
1011- Validate the form input against the Django password validators.
1031+ Validate the password with the Django password validators
10121032
1013- To enable this, configure the Django password validators in
1033+ To enable the Django password validators, configure
10141034 `settings.AUTH_PASSWORD_VALIDATORS` as described in the [Django
10151035 docs](https://docs.djangoproject.com/en/5.1/topics/auth/passwords/#enabling-password-validation)
10161036
1017- :param attrs: The User form field names and their values as a dict.
1037+ :param dict attrs: The User form field names and their values as a dict.
10181038 Example::
10191039
10201040 {
@@ -1024,34 +1044,31 @@ def validate(self, attrs):
10241044 'password': 'secret123'
10251045 }
10261046
1027- :type attrs: dict
1028- :raises ValidationError: If at least one Django password validator
1029- fails.
1030- :return: The unmodified input attrs dict.
1031- :rtype: dict
1047+ :param obj: The User model instance.
1048+ :raises django.core.exceptions.ValidationError: Raise this if at least
1049+ one Django password validator fails.
1050+
1051+ The exception contains a dict ``{"password": <error-message>``}
1052+ which indicates that the password field has failed validation, and
1053+ the reason for failure.
1054+ :return: None.
10321055 """
10331056 # We must do this here instead of in `validate_password` bacause some
10341057 # django password validators need access to other model instance fields,
1035- # e.g. ``username`` for similarity validation .
1058+ # e.g. ``username`` for the ``UserAttributeSimilarityValidator`` .
10361059 password = attrs .get ("password" )
1060+ # Skip validation if no password has been entered. This may happen when
1061+ # an existing User is edited.
10371062 if password and password != '$encrypted$' :
1038- # Create a User instance and set its attributes according to the
1039- # form fields and their values.
1040- #
1041- # Note that we cannot simply do ``obj = User(**attrs)``, because
1042- # then the user instance is written into the database before the
1043- # validation is passed! See also
1044- # https://www.django-rest-framework.org/community/3.0-announcement/#differences-between-modelserializer-validation-and-modelform
1045- exclusions = self .get_validation_exclusions (self .instance )
1046- obj = self .instance or self .Meta .model ()
1047- for k , v in attrs .items ():
1048- if k not in exclusions and k != 'canonical_address_port' :
1049- setattr (obj , k , v )
10501063 # Apply validators from settings.AUTH_PASSWORD_VALIDATORS. This may
10511064 # raise ValidationError.
1052- django_validate_password (password , user = obj )
1053- #
1054- return super ().validate (attrs )
1065+ #
1066+ # If the validation fails, re-raise the exception with adjusted
1067+ # content to make the error appear near the password field.
1068+ try :
1069+ django_validate_password (password , user = obj )
1070+ except DjangoValidationError as exc :
1071+ raise DjangoValidationError ({"password" : exc .messages })
10551072
10561073 def _update_password (self , obj , new_password ):
10571074 if new_password and new_password != '$encrypted$' :
0 commit comments