Opened 4 years ago

Closed 4 years ago

Last modified 4 years ago

#31461 closed Cleanup/optimization (wontfix)

Make it easier to customize ValidationError messages in contrib.auth.password_validation

Reported by: Joey van Breukelen Owned by: Joey van Breukelen
Component: contrib.auth Version: 3.0
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: yes UI/UX: no

Description

Goal: Make it easier to customize ValidationError messages in contrib.auth.password_validation
Problem: None of the gettext strings are defined in class variables or are wrapped by methods, making it hard change gettext in a subclass.
Suggested solution: Wrap gettext with a method so its easy to change in subclass.

Example:

class MinimumLengthValidator:
    # some code left out
    def validate(self, password, user=None):
        if len(password) < self.min_length:
            raise ValidationError(
                ngettext(
                    "This password is too short. It must contain at least %(min_length)d character.",
                    "This password is too short. It must contain at least %(min_length)d characters.",
                    self.min_length
                ),
                code='password_too_short',
                params={'min_length': self.min_length},
            )

Suggested change:

class MinimumLengthValidator:
    # some code left out
    def get_error_message(self):
        return ngettext(
            "This password is too short. It must contain at least %(min_length)d character.",
            "This password is too short. It must contain at least %(min_length)d characters.",
            self.min_length
        )
    
        if len(password) < self.min_length:
            raise ValidationError(
                self.get_error_message(),
                code='password_too_short',
                params={'min_length': self.min_length},
            )


class CustomMinimumLengthValidator(MinimumLengthValidator):
    def get_error_message(self):
        return ngettext(
            "This password is too short. It must contain at least %(min_length)d character.",
            "This password is too short. It must contain at least %(min_length)d characters.",
            self.min_length
        )

This will be similar to how get_help_text works on these classes:

class MinimumLengthValidator:
    # code left out
    def get_help_text(self):
        return ngettext(
            "Your password must contain at least %(min_length)d character.",
            "Your password must contain at least %(min_length)d characters.",
            self.min_length
        ) % {'min_length': self.min_length}

The problem can also be resolved by creating class variables with gettext_lazy, but this might get messy with plurals (which MinimumLengthValidator currently has) and would be a different pattern than the get_help_text method.

Change History (2)

comment:1 by Carlton Gibson, 4 years ago

Resolution: wontfix
Status: assignedclosed

Hi. Thanks for the report.

I'm going to say no initially.

The trade off here is a whole extra method/hook in order to save very little code.

The whole method is just this:

    def validate(self, password, user=None):
        if len(password) < self.min_length:
            raise ValidationError(
                ngettext(
                    "This password is too short. It must contain at least %(min_length)d character.",
                    "This password is too short. It must contain at least %(min_length)d characters.",
                    self.min_length
                ),
                code='password_too_short',
                params={'min_length': self.min_length},
            )

Do, the check, raise the ValidationError. That's it.

The "Writing your own validator" docs have it like so:

If Django's built-in validators are not sufficient, you can write your own
password validators. Validators have a fairly small interface. They must
implement two methods:...

I'm not convinced custom messages are such a common need to merit making that three methods.
(Although I guess the extra hook would be internal, but still...)

I hope that makes sense.

comment:2 by Joey van Breukelen, 4 years ago

Ok, makes sense to me.

Just want to note that MinimumLengthValidator (used in example) has a very short validate method. UserAttributeSimilarityValidator for example, has a more complicated one.

    def validate(self, password, user=None):
        if not user:
            return

        for attribute_name in self.user_attributes:
            value = getattr(user, attribute_name, None)
            if not value or not isinstance(value, str):
                continue
            value_parts = re.split(r'\W+', value) + [value]
            for value_part in value_parts:
                if SequenceMatcher(a=password.lower(), b=value_part.lower()).quick_ratio() >= self.max_similarity:
                    try:
                        verbose_name = str(user._meta.get_field(attribute_name).verbose_name)
                    except FieldDoesNotExist:
                        verbose_name = attribute_name
                    raise ValidationError(
                        _("The password is too similar to the %(verbose_name)s."),
                        code='password_too_similar',
                        params={'verbose_name': verbose_name},
                    )
Note: See TracTickets for help on using tickets.
Back to Top