From f1f4aeb22e7bc9b504f69f7cb111ac9bdedb5f1e Mon Sep 17 00:00:00 2001 From: Dohyeon Kim Date: Tue, 29 May 2018 21:41:32 +0900 Subject: [PATCH] Fixed #28044 -- Unified the logic for createsuperuser's interactive and --noinput modes. --- .../management/commands/createsuperuser.py | 153 +++++++++--------- tests/auth_tests/test_management.py | 29 ++++ 2 files changed, 109 insertions(+), 73 deletions(-) diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index 3a16c58b3d..d142679168 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -69,34 +69,19 @@ class Command(BaseCommand): # instead of raw IDs. fake_user_data = {} verbose_field_name = self.username_field.verbose_name - - # Do quick and dirty validation if --noinput - if not options['interactive']: - try: - if not username: - raise CommandError("You must use --%s with --noinput." % self.UserModel.USERNAME_FIELD) - username = self.username_field.clean(username, None) - - for field_name in self.UserModel.REQUIRED_FIELDS: - if options[field_name]: - 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: - raise CommandError('; '.join(e.messages)) - - else: - # Prompt for username/password, and any other required fields. - # Enclose this whole thing in a try/except to catch - # KeyboardInterrupt and exit gracefully. - default_username = get_default_username() - try: - + try: + if options['interactive']: if hasattr(self.stdin, 'isatty') and not self.stdin.isatty(): - raise NotRunningInTTYException("Not running in a TTY") - - # Get a username + raise NotRunningInTTYException + default_username = get_default_username() + if username: + error_msg = self._validate_username(username, verbose_field_name, database) + if error_msg: + self.stderr.write(error_msg) + username = None + elif username == '': + raise CommandError('%s cannot be blank.' % capfirst(verbose_field_name)) + # Prompt for username. while username is None: input_msg = capfirst(verbose_field_name) if default_username: @@ -110,21 +95,29 @@ class Command(BaseCommand): ) if username_rel else '' ) username = self.get_input_data(self.username_field, input_msg, default_username) - if not username: - continue - if self.username_field.unique: - try: - self.UserModel._default_manager.db_manager(database).get_by_natural_key(username) - except self.UserModel.DoesNotExist: - pass - else: - self.stderr.write("Error: That %s is already taken." % verbose_field_name) + if username: + error_msg = self._validate_username(username, verbose_field_name, database) + if error_msg: + self.stderr.write(error_msg) username = None + continue + else: + if username is None: + raise CommandError('You must use --%s with --noinput.' % self.UserModel.USERNAME_FIELD) + else: + error_msg = self._validate_username(username, verbose_field_name, database) + if error_msg: + raise CommandError(error_msg) - if not username: - raise CommandError('%s cannot be blank.' % capfirst(verbose_field_name)) - - for field_name in self.UserModel.REQUIRED_FIELDS: + # Prompt for required fields. + for field_name in self.UserModel.REQUIRED_FIELDS: + if not options['interactive']: + if options[field_name]: + 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) + else: field = self.UserModel._meta.get_field(field_name) user_data[field_name] = options[field_name] while user_data[field_name] is None: @@ -142,41 +135,39 @@ class Command(BaseCommand): # Wrap any foreign keys in fake model instances if field.remote_field: fake_user_data[field_name] = field.remote_field.model(input_value) - - # Get a password - while password is None: - password = getpass.getpass() - password2 = getpass.getpass('Password (again): ') - if password != password2: - self.stderr.write("Error: Your passwords didn't match.") - password = None - # Don't validate passwords that don't match. - continue - - if password.strip() == '': - self.stderr.write("Error: Blank passwords aren't allowed.") - password = None - # Don't validate blank passwords. - continue - - try: - validate_password(password2, self.UserModel(**fake_user_data)) - except exceptions.ValidationError as err: - self.stderr.write('\n'.join(err.messages)) - response = input('Bypass password validation and create user anyway? [y/N]: ') - if response.lower() != 'y': + # Prompt for a password. + while password is None: + password = getpass.getpass() + password2 = getpass.getpass('Password (again): ') + if password != password2: + self.stderr.write("Error: Your passwords didn't match.") password = None + # Don't validate passwords that don't match. + continue - except KeyboardInterrupt: - self.stderr.write("\nOperation cancelled.") - sys.exit(1) - - except NotRunningInTTYException: - self.stdout.write( - "Superuser creation skipped due to not running in a TTY. " - "You can run `manage.py createsuperuser` in your project " - "to create one manually." - ) + if password.strip() == '': + self.stderr.write("Error: Blank passwords aren't allowed.") + password = None + # Don't validate blank passwords. + continue + try: + validate_password(password2, self.UserModel(**fake_user_data)) + except exceptions.ValidationError as err: + self.stderr.write('\n'.join(err.messages)) + response = input('Bypass password validation and create user anyway? [y/N]: ') + if response.lower() != 'y': + password = None + except KeyboardInterrupt: + self.stderr.write('\nOperation cancelled.') + sys.exit(1) + except exceptions.ValidationError as e: + raise CommandError('; '.join(e.messages)) + except NotRunningInTTYException: + self.stdout.write( + 'Superuser creation skipped due to not running in a TTY. ' + 'You can run `manage.py createsuperuser` in your project ' + 'to create one manually.' + ) if username: user_data[self.UserModel.USERNAME_FIELD] = username @@ -200,3 +191,19 @@ class Command(BaseCommand): val = None return val + + def _validate_username(self, username, verbose_field_name, database): + """Validate username. If invalid, return a string error message.""" + if self.username_field.unique: + try: + self.UserModel._default_manager.db_manager(database).get_by_natural_key(username) + except self.UserModel.DoesNotExist: + pass + else: + return 'Error: That %s is already taken.' % verbose_field_name + if not username: + return '%s cannot be blank.' % capfirst(verbose_field_name) + try: + self.username_field.clean(username, None) + except exceptions.ValidationError as e: + return '; '.join(e.messages) diff --git a/tests/auth_tests/test_management.py b/tests/auth_tests/test_management.py index b3772b1501..4f29740982 100644 --- a/tests/auth_tests/test_management.py +++ b/tests/auth_tests/test_management.py @@ -568,6 +568,22 @@ class CreatesuperuserManagementCommandTestCase(TestCase): test(self) + def test_blank_username_non_interactive(self): + new_io = StringIO() + + def test(self): + with self.assertRaisesMessage(CommandError, 'Username cannot be blank.'): + call_command( + 'createsuperuser', + username='', + interactive=False, + stdin=MockTTY(), + stdout=new_io, + stderr=new_io, + ) + + test(self) + def test_password_validation_bypass(self): """ Password validation can be bypassed by entering 'y' at the prompt. @@ -672,6 +688,19 @@ class CreatesuperuserManagementCommandTestCase(TestCase): test(self) + def test_existing_username_non_interactive(self): + """Creation fails if the username already exists.""" + User.objects.create(username='janet') + new_io = StringIO() + with self.assertRaisesMessage(CommandError, "Error: That username is already taken."): + call_command( + 'createsuperuser', + username='janet', + email='', + interactive=False, + stdout=new_io, + ) + def test_validation_mismatched_passwords(self): """ Creation should fail if the user enters mismatched passwords.