From b3b3db3d954a5226f870a0b4403343c78efae8dc Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 13 Oct 2012 13:36:07 +0800 Subject: [PATCH] Fixed #19067 -- Clarified handling of username in createsuperuser. Thanks to clelland for the report, and Preston Holmes for the draft patch. --- .../management/commands/createsuperuser.py | 90 +++++++------- django/contrib/auth/tests/custom_user.py | 4 +- django/contrib/auth/tests/management.py | 2 +- docs/topics/auth.txt | 110 ++++++++++-------- 4 files changed, 111 insertions(+), 95 deletions(-) diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index cb5d906342d..216d56d7305 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -16,50 +16,56 @@ from django.utils.text import capfirst 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.' def handle(self, *args, **options): - username = options.get('username', None) + username = options.get(self.UserModel.USERNAME_FIELD, None) interactive = options.get('interactive') verbosity = int(options.get('verbosity', 1)) 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 password = None - other_data = {} + user_data = {} # Do quick and dirty validation if --noinput if not interactive: try: if not username: - raise CommandError("You must use --username with --noinput.") - username = username_field.clean(username, None) + raise CommandError("You must use --%s with --noinput." % + 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): - field = UserModel._meta.get_field(field_name) - other_data[field_name] = field.clean(options[field_name], None) + field = self.UserModel._meta.get_field(field_name) + user_data[field_name] = field.clean(options[field_name], None) else: raise CommandError("You must use --%s with --noinput." % field_name) except exceptions.ValidationError as e: @@ -74,9 +80,8 @@ class Command(BaseCommand): # Get a username while username is None: - username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD) if not username: - input_msg = capfirst(username_field.verbose_name) + input_msg = capfirst(self.username_field.verbose_name) if default_username: input_msg += " (leave blank to use '%s')" % default_username raw_value = input(input_msg + ': ') @@ -84,31 +89,30 @@ class Command(BaseCommand): if default_username and raw_value == '': raw_value = default_username try: - username = username_field.clean(raw_value, None) + username = self.username_field.clean(raw_value, None) except exceptions.ValidationError as e: self.stderr.write("Error: %s" % '; '.join(e.messages)) username = None continue try: - UserModel.objects.using(database).get(**{ - UserModel.USERNAME_FIELD: username - }) - except UserModel.DoesNotExist: + self.UserModel.objects.db_manager(database).get_by_natural_key(username) + except self.UserModel.DoesNotExist: pass 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 - for field_name in other_fields: - field = UserModel._meta.get_field(field_name) - other_data[field_name] = options.get(field_name) - while other_data[field_name] is None: + for field_name in self.UserModel.REQUIRED_FIELDS: + field = self.UserModel._meta.get_field(field_name) + user_data[field_name] = options.get(field_name) + while user_data[field_name] is None: raw_value = input(capfirst(field.verbose_name + ': ')) try: - other_data[field_name] = field.clean(raw_value, None) + user_data[field_name] = field.clean(raw_value, None) except exceptions.ValidationError as e: self.stderr.write("Error: %s" % '; '.join(e.messages)) - other_data[field_name] = None + user_data[field_name] = None # Get a password while password is None: @@ -128,6 +132,8 @@ class Command(BaseCommand): self.stderr.write("\nOperation cancelled.") 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: self.stdout.write("Superuser created successfully.") diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py index 9bd74c0ac84..a29ed6a104f 100644 --- a/django/contrib/auth/tests/custom_user.py +++ b/django/contrib/auth/tests/custom_user.py @@ -23,8 +23,8 @@ class CustomUserManager(BaseUserManager): user.save(using=self._db) return user - def create_superuser(self, username, password, date_of_birth): - u = self.create_user(username, password=password, date_of_birth=date_of_birth) + def create_superuser(self, email, password, date_of_birth): + u = self.create_user(email, password=password, date_of_birth=date_of_birth) u.is_admin = True u.save(using=self._db) return u diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index 7074e047997..976c0c49721 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -138,7 +138,7 @@ class CreatesuperuserManagementCommandTestCase(TestCase): new_io = StringIO() call_command("createsuperuser", interactive=False, - username="joe@somewhere.org", + email="joe@somewhere.org", date_of_birth="1976-04-01", stdout=new_io, skip_validation=True diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index fd2e56ebebd..41159984f65 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -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 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 - 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:: + .. attribute:: User.USERNAME_FIELD - class MyUser(AbstractBaseUser): - identfier = models.CharField(max_length=40, unique=True, db_index=True) - ... - USERNAME_FIELD = 'identifier' + 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 + 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:: -.. 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 - a user. For example, here is the partial definition for a User model - that defines two required fields - a date of birth and height:: + .. attribute:: User.REQUIRED_FIELDS - class MyUser(AbstractBaseUser): - ... - date_of_birth = models.DateField() - height = models.FloatField() - ... - REQUIRED_FIELDS = ['date_of_birth', 'height'] + 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 + that defines two required fields - a date of birth and 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 - would be the full name name of the user, but it can be any string that - identifies the user. + .. note:: -.. 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 - 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()`. + .. method:: User.get_full_name(): + + A longer formal identifier for the user. A common interpretation + 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 -:class:`~django.contrib.auth.models.AbstractBaseUser`:: +:class:`~django.contrib.auth.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 additional methods: -.. method:: UserManager.create_user(username, password=None, **other_fields) +.. class:: models.CustomUserManager - The prototype of `create_user()` should accept all required 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:: + .. method:: models.CustomUserManager.create_user(*username_field*, password=None, **other_fields) - def create_user(self, username, date_of_birth, password=None): - # create user here + The prototype of `create_user()` should accept the username field, + 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 - as arguments. For example, if your user model defines `username`, - and `date_of_birth` as required fields, then create_user should be - defined as:: + .. method:: models.CustomUserManager.create_superuser(*username_field*, password, **other_fields) - def create_superuser(self, username, date_of_birth, password): - # create superuser here + The prototype of `create_user()` should accept the username field, + 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 - to provider a password. + def create_superuser(self, email, date_of_birth, 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 utility methods: .. class:: models.BaseUserManager + .. method:: models.BaseUserManager.normalize_email(email) A classmethod that normalizes email addresses by lowercasing @@ -2165,12 +2175,12 @@ authentication app:: user.save(using=self._db) 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 birth and password. """ - user = self.create_user(username, + user = self.create_user(email, password=password, date_of_birth=date_of_birth ) @@ -2223,7 +2233,7 @@ authentication app:: return self.is_admin 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.contrib import admin @@ -2249,7 +2259,7 @@ code would be required in ``admin.py``:: password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: - raise forms.ValidationError('Passwords don't match') + raise forms.ValidationError("Passwords don't match") return password2 def save(self, commit=True):