summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Graham <timograham@gmail.com>2016-06-21 09:06:34 -0400
committerTim Graham <timograham@gmail.com>2016-06-21 16:20:00 -0400
commit1b0b6f0342e5ac9e3e789ca522ad64a532602c3f (patch)
tree2c7e2c58db94aa83cf8a692d339c4b2dd437980b
parent45a650777e4674bc540c3bf4b3108409f0e1f98a (diff)
[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 39805686b364358af725b695924a5a6dfa7f5302 from master
-rw-r--r--django/contrib/auth/base_user.py11
-rw-r--r--django/contrib/auth/models.py2
-rw-r--r--docs/releases/1.10.txt4
-rw-r--r--docs/topics/auth/customizing.txt24
-rw-r--r--tests/auth_tests/test_forms.py16
-rw-r--r--tests/auth_tests/test_models.py18
6 files changed, 62 insertions, 13 deletions
diff --git a/django/contrib/auth/base_user.py b/django/contrib/auth/base_user.py
index d168262bb0..de069659e1 100644
--- a/django/contrib/auth/base_user.py
+++ b/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))
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
index 87c3adf36b..4812546340 100644
--- a/django/contrib/auth/models.py
+++ b/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)
diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt
index 752684e3fa..c56ac6efc6 100644
--- a/docs/releases/1.10.txt
+++ b/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
diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt
index d12fa6a212..6d84759831 100644
--- a/docs/topics/auth/customizing.txt
+++ b/docs/topics/auth/customizing.txt
@@ -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
diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py
index d2ce828eb8..fe22d1e20b 100644
--- a/tests/auth_tests/test_forms.py
+++ b/tests/auth_tests/test_forms.py
@@ -120,6 +120,22 @@ class UserCreationFormTest(TestDataMixin, TestCase):
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):
"""
To prevent almost identical usernames, visually identical but differing
diff --git a/tests/auth_tests/test_models.py b/tests/auth_tests/test_models.py
index 1f9edbc77c..7967306c43 100644
--- a/tests/auth_tests/test_models.py
+++ b/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