Fixed #19067 -- Clarified handling of username in createsuperuser.

Thanks to clelland for the report, and Preston Holmes for the draft patch.
This commit is contained in:
Russell Keith-Magee 2012-10-13 13:36:07 +08:00
parent c433fcb3fb
commit b3b3db3d95
4 changed files with 111 additions and 95 deletions

View File

@ -16,50 +16,56 @@ from django.utils.text import capfirst
class Command(BaseCommand): class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--username', dest='username', default=None,
help='Specifies the username for the superuser.'),
make_option('--noinput', action='store_false', dest='interactive', default=True,
help=('Tells Django to NOT prompt the user for input of any kind. '
'You must use --username with --noinput, along with an option for '
'any other required field. Superusers created with --noinput will '
' not be able to log in until they\'re given a valid password.')),
make_option('--database', action='store', dest='database',
default=DEFAULT_DB_ALIAS, help='Specifies the database to use. Default is "default".'),
) + tuple(
make_option('--%s' % field, dest=field, default=None,
help='Specifies the %s for the superuser.' % field)
for field in get_user_model().REQUIRED_FIELDS
)
def __init__(self, *args, **kwargs):
# Options are defined in an __init__ method to support swapping out
# custom user models in tests.
super(Command, self).__init__(*args, **kwargs)
self.UserModel = get_user_model()
self.username_field = self.UserModel._meta.get_field(self.UserModel.USERNAME_FIELD)
self.option_list = BaseCommand.option_list + (
make_option('--%s' % self.UserModel.USERNAME_FIELD, dest=self.UserModel.USERNAME_FIELD, default=None,
help='Specifies the login for the superuser.'),
make_option('--noinput', action='store_false', dest='interactive', default=True,
help=('Tells Django to NOT prompt the user for input of any kind. '
'You must use --%s with --noinput, along with an option for '
'any other required field. Superusers created with --noinput will '
' not be able to log in until they\'re given a valid password.' %
self.UserModel.USERNAME_FIELD)),
make_option('--database', action='store', dest='database',
default=DEFAULT_DB_ALIAS, help='Specifies the database to use. Default is "default".'),
) + tuple(
make_option('--%s' % field, dest=field, default=None,
help='Specifies the %s for the superuser.' % field)
for field in self.UserModel.REQUIRED_FIELDS
)
option_list = BaseCommand.option_list
help = 'Used to create a superuser.' help = 'Used to create a superuser.'
def handle(self, *args, **options): def handle(self, *args, **options):
username = options.get('username', None) username = options.get(self.UserModel.USERNAME_FIELD, None)
interactive = options.get('interactive') interactive = options.get('interactive')
verbosity = int(options.get('verbosity', 1)) verbosity = int(options.get('verbosity', 1))
database = options.get('database') database = options.get('database')
UserModel = get_user_model()
username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
other_fields = UserModel.REQUIRED_FIELDS
# If not provided, create the user with an unusable password # If not provided, create the user with an unusable password
password = None password = None
other_data = {} user_data = {}
# Do quick and dirty validation if --noinput # Do quick and dirty validation if --noinput
if not interactive: if not interactive:
try: try:
if not username: if not username:
raise CommandError("You must use --username with --noinput.") raise CommandError("You must use --%s with --noinput." %
username = username_field.clean(username, None) self.UserModel.USERNAME_FIELD)
username = self.username_field.clean(username, None)
for field_name in other_fields: for field_name in self.UserModel.REQUIRED_FIELDS:
if options.get(field_name): if options.get(field_name):
field = UserModel._meta.get_field(field_name) field = self.UserModel._meta.get_field(field_name)
other_data[field_name] = field.clean(options[field_name], None) user_data[field_name] = field.clean(options[field_name], None)
else: else:
raise CommandError("You must use --%s with --noinput." % field_name) raise CommandError("You must use --%s with --noinput." % field_name)
except exceptions.ValidationError as e: except exceptions.ValidationError as e:
@ -74,9 +80,8 @@ class Command(BaseCommand):
# Get a username # Get a username
while username is None: while username is None:
username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
if not username: if not username:
input_msg = capfirst(username_field.verbose_name) input_msg = capfirst(self.username_field.verbose_name)
if default_username: if default_username:
input_msg += " (leave blank to use '%s')" % default_username input_msg += " (leave blank to use '%s')" % default_username
raw_value = input(input_msg + ': ') raw_value = input(input_msg + ': ')
@ -84,31 +89,30 @@ class Command(BaseCommand):
if default_username and raw_value == '': if default_username and raw_value == '':
raw_value = default_username raw_value = default_username
try: try:
username = username_field.clean(raw_value, None) username = self.username_field.clean(raw_value, None)
except exceptions.ValidationError as e: except exceptions.ValidationError as e:
self.stderr.write("Error: %s" % '; '.join(e.messages)) self.stderr.write("Error: %s" % '; '.join(e.messages))
username = None username = None
continue continue
try: try:
UserModel.objects.using(database).get(**{ self.UserModel.objects.db_manager(database).get_by_natural_key(username)
UserModel.USERNAME_FIELD: username except self.UserModel.DoesNotExist:
})
except UserModel.DoesNotExist:
pass pass
else: else:
self.stderr.write("Error: That username is already taken.") self.stderr.write("Error: That %s is already taken." %
self.username_field.verbose_name)
username = None username = None
for field_name in other_fields: for field_name in self.UserModel.REQUIRED_FIELDS:
field = UserModel._meta.get_field(field_name) field = self.UserModel._meta.get_field(field_name)
other_data[field_name] = options.get(field_name) user_data[field_name] = options.get(field_name)
while other_data[field_name] is None: while user_data[field_name] is None:
raw_value = input(capfirst(field.verbose_name + ': ')) raw_value = input(capfirst(field.verbose_name + ': '))
try: try:
other_data[field_name] = field.clean(raw_value, None) user_data[field_name] = field.clean(raw_value, None)
except exceptions.ValidationError as e: except exceptions.ValidationError as e:
self.stderr.write("Error: %s" % '; '.join(e.messages)) self.stderr.write("Error: %s" % '; '.join(e.messages))
other_data[field_name] = None user_data[field_name] = None
# Get a password # Get a password
while password is None: while password is None:
@ -128,6 +132,8 @@ class Command(BaseCommand):
self.stderr.write("\nOperation cancelled.") self.stderr.write("\nOperation cancelled.")
sys.exit(1) sys.exit(1)
UserModel.objects.db_manager(database).create_superuser(username=username, password=password, **other_data) user_data[self.UserModel.USERNAME_FIELD] = username
user_data['password'] = password
self.UserModel.objects.db_manager(database).create_superuser(**user_data)
if verbosity >= 1: if verbosity >= 1:
self.stdout.write("Superuser created successfully.") self.stdout.write("Superuser created successfully.")

View File

@ -23,8 +23,8 @@ class CustomUserManager(BaseUserManager):
user.save(using=self._db) user.save(using=self._db)
return user return user
def create_superuser(self, username, password, date_of_birth): def create_superuser(self, email, password, date_of_birth):
u = self.create_user(username, password=password, date_of_birth=date_of_birth) u = self.create_user(email, password=password, date_of_birth=date_of_birth)
u.is_admin = True u.is_admin = True
u.save(using=self._db) u.save(using=self._db)
return u return u

View File

@ -138,7 +138,7 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
new_io = StringIO() new_io = StringIO()
call_command("createsuperuser", call_command("createsuperuser",
interactive=False, interactive=False,
username="joe@somewhere.org", email="joe@somewhere.org",
date_of_birth="1976-04-01", date_of_birth="1976-04-01",
stdout=new_io, stdout=new_io,
skip_validation=True skip_validation=True

View File

@ -1878,47 +1878,54 @@ The easiest way to construct a compliant custom User model is to inherit from
implementation of a `User` model, including hashed passwords and tokenized implementation of a `User` model, including hashed passwords and tokenized
password resets. You must then provide some key implementation details: password resets. You must then provide some key implementation details:
.. attribute:: User.USERNAME_FIELD .. class:: models.CustomUser
A string describing the name of the field on the User model that is .. attribute:: User.USERNAME_FIELD
used as the unique identifier. This will usually be a username of
some kind, but it can also be an email address, or any other unique
identifier. In the following example, the field `identifier` is used
as the identifying field::
class MyUser(AbstractBaseUser): A string describing the name of the field on the User model that is
identfier = models.CharField(max_length=40, unique=True, db_index=True) used as the unique identifier. This will usually be a username of
... some kind, but it can also be an email address, or any other unique
USERNAME_FIELD = 'identifier' identifier. In the following example, the field `identifier` is used
as the identifying field::
.. attribute:: User.REQUIRED_FIELDS class MyUser(AbstractBaseUser):
identfier = models.CharField(max_length=40, unique=True, db_index=True)
...
USERNAME_FIELD = 'identifier'
A list of the field names that *must* be provided when creating .. attribute:: User.REQUIRED_FIELDS
a user. For example, here is the partial definition for a User model
that defines two required fields - a date of birth and height::
class MyUser(AbstractBaseUser): A list of the field names that *must* be provided when creating
... a user. For example, here is the partial definition for a User model
date_of_birth = models.DateField() that defines two required fields - a date of birth and height::
height = models.FloatField()
...
REQUIRED_FIELDS = ['date_of_birth', 'height']
.. method:: User.get_full_name(): class MyUser(AbstractBaseUser):
...
date_of_birth = models.DateField()
height = models.FloatField()
...
REQUIRED_FIELDS = ['date_of_birth', 'height']
A longer formal identifier for the user. A common interpretation .. note::
would be the full name name of the user, but it can be any string that
identifies the user.
.. method:: User.get_short_name(): ``REQUIRED_FIELDS`` must contain all required fields on your User
model, but should *not* contain the ``USERNAME_FIELD``.
A short, informal identifier for the user. A common interpretation .. method:: User.get_full_name():
would be the first name of the user, but it can be any string that
identifies the user in an informal way. It may also return the same A longer formal identifier for the user. A common interpretation
value as :meth:`django.contrib.auth.User.get_full_name()`. would be the full name name of the user, but it can be any string that
identifies the user.
.. method:: User.get_short_name():
A short, informal identifier for the user. A common interpretation
would be the first name of the user, but it can be any string that
identifies the user in an informal way. It may also return the same
value as :meth:`django.contrib.auth.User.get_full_name()`.
The following methods are available on any subclass of The following methods are available on any subclass of
:class:`~django.contrib.auth.models.AbstractBaseUser`:: :class:`~django.contrib.auth.models.AbstractBaseUser`:
.. class:: models.AbstractBaseUser .. class:: models.AbstractBaseUser
@ -1979,33 +1986,36 @@ defines different fields, you will need to define a custom manager that
extends :class:`~django.contrib.auth.models.BaseUserManager` providing two extends :class:`~django.contrib.auth.models.BaseUserManager` providing two
additional methods: additional methods:
.. method:: UserManager.create_user(username, password=None, **other_fields) .. class:: models.CustomUserManager
The prototype of `create_user()` should accept all required fields .. method:: models.CustomUserManager.create_user(*username_field*, password=None, **other_fields)
as arguments. For example, if your user model defines `username`,
and `date_of_birth` as required fields, then create_user should be
defined as::
def create_user(self, username, date_of_birth, password=None): The prototype of `create_user()` should accept the username field,
# create user here plus all required fields as arguments. For example, if your user model
uses `email` as the username field, and has `date_of_birth` as a required
fields, then create_user should be defined as::
.. method:: UserManager.create_superuser(username, password, **other_fields) def create_user(self, email, date_of_birth, password=None):
# create user here
The prototype of `create_superuser()` should accept all required fields .. method:: models.CustomUserManager.create_superuser(*username_field*, password, **other_fields)
as arguments. For example, if your user model defines `username`,
and `date_of_birth` as required fields, then create_user should be
defined as::
def create_superuser(self, username, date_of_birth, password): The prototype of `create_user()` should accept the username field,
# create superuser here plus all required fields as arguments. For example, if your user model
uses `email` as the username field, and has `date_of_birth` as a required
fields, then create_superuser should be defined as::
Unlike `create_user()`, `create_superuser()` *must* require the caller def create_superuser(self, email, date_of_birth, password):
to provider a password. # create superuser here
Unlike `create_user()`, `create_superuser()` *must* require the caller
to provider a password.
:class:`~django.contrib.auth.models.BaseUserManager` provides the following :class:`~django.contrib.auth.models.BaseUserManager` provides the following
utility methods: utility methods:
.. class:: models.BaseUserManager .. class:: models.BaseUserManager
.. method:: models.BaseUserManager.normalize_email(email) .. method:: models.BaseUserManager.normalize_email(email)
A classmethod that normalizes email addresses by lowercasing A classmethod that normalizes email addresses by lowercasing
@ -2165,12 +2175,12 @@ authentication app::
user.save(using=self._db) user.save(using=self._db)
return user return user
def create_superuser(self, username, date_of_birth, password): def create_superuser(self, email, date_of_birth, password):
""" """
Creates and saves a superuser with the given email, date of Creates and saves a superuser with the given email, date of
birth and password. birth and password.
""" """
user = self.create_user(username, user = self.create_user(email,
password=password, password=password,
date_of_birth=date_of_birth date_of_birth=date_of_birth
) )
@ -2223,7 +2233,7 @@ authentication app::
return self.is_admin return self.is_admin
Then, to register this custom User model with Django's admin, the following Then, to register this custom User model with Django's admin, the following
code would be required in ``admin.py``:: code would be required in the app's ``admin.py`` file::
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
@ -2249,7 +2259,7 @@ code would be required in ``admin.py``::
password1 = self.cleaned_data.get("password1") password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2") password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2: if password1 and password2 and password1 != password2:
raise forms.ValidationError('Passwords don't match') raise forms.ValidationError("Passwords don't match")
return password2 return password2
def save(self, commit=True): def save(self, commit=True):