2
0
Эх сурвалжийг харах

Refs #21379, #26719 -- Moved username normalization to AbstractBaseUser.

Thanks Huynh Thanh Tam for the initial patch and Claude Paroz for review.
Tim Graham 8 жил өмнө
parent
commit
39805686b3

+ 7 - 4
django/contrib/auth/base_user.py

@@ -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
django/contrib/auth/models.py

@@ -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
docs/releases/1.10.txt

@@ -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
docs/topics/auth/customizing.txt

@@ -608,6 +608,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
@@ -722,14 +738,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
tests/auth_tests/test_forms.py

@@ -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):
         """

+ 18 - 0
tests/auth_tests/test_models.py

@@ -1,3 +1,6 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
 from django.conf.global_settings import PASSWORD_HASHERS
 from django.contrib.auth import get_user_model
 from django.contrib.auth.hashers import get_hasher
@@ -143,6 +146,21 @@ class UserManagerTestCase(TestCase):
             )
 
 
+class AbstractBaseUserTests(TestCase):
+
+    def test_clean_normalize_username(self):
+        # The normalization happens in AbstractBaseUser.clean()
+        ohm_username = 'iamtheΩ'  # U+2126 OHM SIGN
+        for model in ('auth.User', 'auth_tests.CustomUser'):
+            with self.settings(AUTH_USER_MODEL=model):
+                User = get_user_model()
+                user = User(**{User.USERNAME_FIELD: ohm_username, 'password': 'foo'})
+                user.clean()
+                username = user.get_username()
+                self.assertNotEqual(username, ohm_username)
+                self.assertEqual(username, 'iamtheΩ')  # U+03A9 GREEK CAPITAL LETTER OMEGA
+
+
 class AbstractUserTestCase(TestCase):
     def test_email_user(self):
         # valid send_mail parameters