Thanks Huynh Thanh Tam for the initial patch and Claude Paroz for review.
This commit is contained in:
parent
5ce660cd65
commit
39805686b3
|
@ -33,10 +33,6 @@ class BaseUserManager(models.Manager):
|
||||||
email = '@'.join([email_name, domain_part.lower()])
|
email = '@'.join([email_name, domain_part.lower()])
|
||||||
return email
|
return email
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def normalize_username(cls, username):
|
|
||||||
return unicodedata.normalize('NFKC', force_text(username))
|
|
||||||
|
|
||||||
def make_random_password(self, length=10,
|
def make_random_password(self, length=10,
|
||||||
allowed_chars='abcdefghjkmnpqrstuvwxyz'
|
allowed_chars='abcdefghjkmnpqrstuvwxyz'
|
||||||
'ABCDEFGHJKLMNPQRSTUVWXYZ'
|
'ABCDEFGHJKLMNPQRSTUVWXYZ'
|
||||||
|
@ -77,6 +73,9 @@ class AbstractBaseUser(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.get_username()
|
return self.get_username()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
setattr(self, self.USERNAME_FIELD, self.normalize_username(self.get_username()))
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super(AbstractBaseUser, self).save(*args, **kwargs)
|
super(AbstractBaseUser, self).save(*args, **kwargs)
|
||||||
if self._password is not None:
|
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"
|
key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
|
||||||
return salted_hmac(key_salt, self.password).hexdigest()
|
return salted_hmac(key_salt, self.password).hexdigest()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def normalize_username(cls, username):
|
||||||
|
return unicodedata.normalize('NFKC', force_text(username))
|
||||||
|
|
|
@ -145,7 +145,7 @@ class UserManager(BaseUserManager):
|
||||||
if not username:
|
if not username:
|
||||||
raise ValueError('The given username must be set')
|
raise ValueError('The given username must be set')
|
||||||
email = self.normalize_email(email)
|
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 = self.model(username=username, email=email, **extra_fields)
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
user.save(using=self._db)
|
user.save(using=self._db)
|
||||||
|
|
|
@ -887,6 +887,10 @@ Miscellaneous
|
||||||
* Accessing a deleted field on a model instance, e.g. after ``del obj.field``,
|
* Accessing a deleted field on a model instance, e.g. after ``del obj.field``,
|
||||||
reloads the field's value instead of raising ``AttributeError``.
|
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:
|
.. _deprecated-features-1.10:
|
||||||
|
|
||||||
Features deprecated in 1.10
|
Features deprecated in 1.10
|
||||||
|
|
|
@ -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``.
|
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
|
.. attribute:: models.AbstractBaseUser.is_authenticated
|
||||||
|
|
||||||
Read-only attribute which is always ``True`` (as opposed to
|
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
|
Normalizes email addresses by lowercasing the domain portion of the
|
||||||
email address.
|
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)
|
.. method:: models.BaseUserManager.get_by_natural_key(username)
|
||||||
|
|
||||||
Retrieves a user instance using the contents of the field
|
Retrieves a user instance using the contents of the field
|
||||||
|
|
|
@ -119,6 +119,22 @@ class UserCreationFormTest(TestDataMixin, TestCase):
|
||||||
else:
|
else:
|
||||||
self.assertFalse(form.is_valid())
|
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.")
|
@skipIf(six.PY2, "Python 2 doesn't support unicode usernames by default.")
|
||||||
def test_duplicate_normalized_unicode(self):
|
def test_duplicate_normalized_unicode(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.global_settings import PASSWORD_HASHERS
|
from django.conf.global_settings import PASSWORD_HASHERS
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.hashers import get_hasher
|
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):
|
class AbstractUserTestCase(TestCase):
|
||||||
def test_email_user(self):
|
def test_email_user(self):
|
||||||
# valid send_mail parameters
|
# valid send_mail parameters
|
||||||
|
|
Loading…
Reference in New Issue