Commit 1b0b6f03 authored by Tim Graham's avatar Tim Graham
Browse files

[1.10.x] Refs #21379, #26719 -- Moved username normalization to AbstractBaseUser.

Thanks Huynh Thanh Tam for the initial patch and Claude Paroz for review.

Backport of 39805686 from master
parent 45a65077
Loading
Loading
Loading
Loading
+7 −4
Original line number Diff line number Diff line
@@ -33,10 +33,6 @@ class BaseUserManager(models.Manager):
            email = '@'.join([email_name, domain_part.lower()])
        return email

    @classmethod
    def normalize_username(cls, username):
        return unicodedata.normalize('NFKC', force_text(username))

    def make_random_password(self, length=10,
                             allowed_chars='abcdefghjkmnpqrstuvwxyz'
                                           'ABCDEFGHJKLMNPQRSTUVWXYZ'
@@ -77,6 +73,9 @@ class AbstractBaseUser(models.Model):
    def __str__(self):
        return self.get_username()

    def clean(self):
        setattr(self, self.USERNAME_FIELD, self.normalize_username(self.get_username()))

    def save(self, *args, **kwargs):
        super(AbstractBaseUser, self).save(*args, **kwargs)
        if self._password is not None:
@@ -137,3 +136,7 @@ class AbstractBaseUser(models.Model):
        """
        key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
        return salted_hmac(key_salt, self.password).hexdigest()

    @classmethod
    def normalize_username(cls, username):
        return unicodedata.normalize('NFKC', force_text(username))
+1 −1
Original line number Diff line number Diff line
@@ -145,7 +145,7 @@ class UserManager(BaseUserManager):
        if not username:
            raise ValueError('The given username must be set')
        email = self.normalize_email(email)
        username = self.normalize_username(username)
        username = self.model.normalize_username(username)
        user = self.model(username=username, email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
+4 −0
Original line number Diff line number Diff line
@@ -887,6 +887,10 @@ Miscellaneous
* Accessing a deleted field on a model instance, e.g. after ``del obj.field``,
  reloads the field's value instead of raising ``AttributeError``.

* If you subclass ``AbstractBaseUser`` and override ``clean()``, be sure it
  calls ``super()``. :meth:`.AbstractBaseUser.normalize_username` is called in
  a new :meth:`.AbstractBaseUser.clean` method.

.. _deprecated-features-1.10:

Features deprecated in 1.10
+16 −8
Original line number Diff line number Diff line
@@ -612,6 +612,22 @@ The following attributes and methods are available on any subclass of

        Returns the value of the field nominated by ``USERNAME_FIELD``.

    .. method:: clean()

        .. versionadded:: 1.10

        Normalizes the username by calling :meth:`normalize_username`. If you
        override this method, be sure to call ``super()`` to retain the
        normalization.

    .. classmethod:: normalize_username(username)

        .. versionadded:: 1.10

        Applies NFKC Unicode normalization to usernames so that visually
        identical characters with different Unicode code points are considered
        identical.

    .. attribute:: models.AbstractBaseUser.is_authenticated

        Read-only attribute which is always ``True`` (as opposed to
@@ -726,14 +742,6 @@ utility methods:
        Normalizes email addresses by lowercasing the domain portion of the
        email address.

    .. classmethod:: models.BaseUserManager.normalize_username(email)

        .. versionadded:: 1.10

        Applies NFKC Unicode normalization to usernames so that visually
        identical characters with different Unicode code points are considered
        identical.

    .. method:: models.BaseUserManager.get_by_natural_key(username)

        Retrieves a user instance using the contents of the field
+16 −0
Original line number Diff line number Diff line
@@ -119,6 +119,22 @@ class UserCreationFormTest(TestDataMixin, TestCase):
        else:
            self.assertFalse(form.is_valid())

    @skipIf(six.PY2, "Python 2 doesn't support unicode usernames by default.")
    def test_normalize_username(self):
        # The normalization happens in AbstractBaseUser.clean() and ModelForm
        # validation calls Model.clean().
        ohm_username = 'testΩ'  # U+2126 OHM SIGN
        data = {
            'username': ohm_username,
            'password1': 'pwd2',
            'password2': 'pwd2',
        }
        form = UserCreationForm(data)
        self.assertTrue(form.is_valid())
        user = form.save()
        self.assertNotEqual(user.username, ohm_username)
        self.assertEqual(user.username, 'testΩ')  # U+03A9 GREEK CAPITAL LETTER OMEGA

    @skipIf(six.PY2, "Python 2 doesn't support unicode usernames by default.")
    def test_duplicate_normalized_unicode(self):
        """
Loading