Fixed #24910 -- Added createsuperuser support for non-unique USERNAME_FIELDs

Clarified docs to say that a non-unique USERNAME_FIELD is permissable
as long as the custom auth backend can support it.
This commit is contained in:
Alasdair Nicol 2015-06-05 14:33:04 +01:00 committed by Tim Graham
parent a391b17ad2
commit 1ea87c8c79
4 changed files with 58 additions and 17 deletions

View File

@ -101,14 +101,14 @@ class Command(BaseCommand):
username = self.get_input_data(self.username_field, input_msg, default_username) username = self.get_input_data(self.username_field, input_msg, default_username)
if not username: if not username:
continue continue
try: if self.username_field.unique:
self.UserModel._default_manager.db_manager(database).get_by_natural_key(username) try:
except self.UserModel.DoesNotExist: self.UserModel._default_manager.db_manager(database).get_by_natural_key(username)
pass except self.UserModel.DoesNotExist:
else: pass
self.stderr.write("Error: That %s is already taken." % else:
verbose_field_name) self.stderr.write("Error: That %s is already taken." % verbose_field_name)
username = None username = None
for field_name in self.UserModel.REQUIRED_FIELDS: for field_name in self.UserModel.REQUIRED_FIELDS:
field = self.UserModel._meta.get_field(field_name) field = self.UserModel._meta.get_field(field_name)

View File

@ -477,9 +477,11 @@ Specifying a custom User model
Django expects your custom User model to meet some minimum requirements. Django expects your custom User model to meet some minimum requirements.
#. Your model must have a single unique field that can be used for #. If you use the default authentication backend, then your model must have a
identification purposes. This can be a username, an email address, single unique field that can be used for identification purposes. This can
or any other unique attribute. be a username, an email address, or any other unique attribute. A non-unique
username field is allowed if you use a custom authentication backend that
can support it.
#. Your model must provide a way to address the user in a "short" and #. Your model must provide a way to address the user in a "short" and
"long" form. The most common interpretation of this would be to use "long" form. The most common interpretation of this would be to use
@ -506,10 +508,11 @@ password resets. You must then provide some key implementation details:
.. attribute:: USERNAME_FIELD .. attribute:: USERNAME_FIELD
A string describing the name of the field on the User model that is A string describing the name of the field on the User model that is
used as the unique identifier. This will usually be a username of used as the unique identifier. This will usually be a username of some
some kind, but it can also be an email address, or any other unique kind, but it can also be an email address, or any other unique
identifier. The field *must* be unique (i.e., have ``unique=True`` identifier. The field *must* be unique (i.e., have ``unique=True`` set
set in its definition). in its definition), unless you use a custom authentication backend that
can support non-unique usernames.
In the following example, the field ``identifier`` is used In the following example, the field ``identifier`` is used
as the identifying field:: as the identifying field::

View File

@ -1,12 +1,23 @@
from django.contrib.auth.models import AbstractBaseUser from django.contrib.auth.models import AbstractBaseUser, UserManager
from django.db import models from django.db import models
class CustomUserNonUniqueUsername(AbstractBaseUser): class CustomUserNonUniqueUsername(AbstractBaseUser):
"A user with a non-unique username" """
A user with a non-unique username.
This model is not invalid if it is used with a custom authentication
backend which supports non-unique usernames.
"""
username = models.CharField(max_length=30) username = models.CharField(max_length=30)
email = models.EmailField(blank=True)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
USERNAME_FIELD = 'username' USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
objects = UserManager()
class Meta: class Meta:
app_label = 'auth' app_label = 'auth'

View File

@ -305,6 +305,33 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
self.assertEqual(CustomUser._default_manager.count(), 0) self.assertEqual(CustomUser._default_manager.count(), 0)
@override_settings(
AUTH_USER_MODEL='auth.CustomUserNonUniqueUsername',
AUTHENTICATION_BACKENDS=['my.custom.backend'],
)
def test_swappable_user_username_non_unique(self):
@mock_inputs({
'username': 'joe',
'password': 'nopasswd',
})
def createsuperuser():
new_io = six.StringIO()
call_command(
"createsuperuser",
interactive=True,
email="joe@somewhere.org",
stdout=new_io,
stdin=MockTTY(),
)
command_output = new_io.getvalue().strip()
self.assertEqual(command_output, 'Superuser created successfully.')
for i in range(2):
createsuperuser()
users = CustomUserNonUniqueUsername.objects.filter(username="joe")
self.assertEqual(users.count(), 2)
def test_skip_if_not_in_TTY(self): def test_skip_if_not_in_TTY(self):
""" """
If the command is not called from a TTY, it should be skipped and a If the command is not called from a TTY, it should be skipped and a