From 9bc2d766a0c31701a4c0e20a8b04164bb22cf6d5 Mon Sep 17 00:00:00 2001 From: Anubhav Joshi Date: Mon, 30 Jun 2014 18:36:28 +0530 Subject: [PATCH] Fixed #21755 -- Added ForeignKey support to REQUIRED_FIELDS. This allows specifying ForeignKeys in REQUIRED_FIELDS when using a custom User model. Thanks cjerdonek and bmispelon for suggestion and timgraham for review. --- .../management/commands/createsuperuser.py | 2 +- django/contrib/auth/tests/custom_user.py | 26 ++++++++ django/contrib/auth/tests/test_management.py | 60 ++++++++++++++++++- django/db/utils.py | 8 +-- docs/releases/1.8.txt | 2 + docs/topics/auth/customizing.txt | 12 +++- 6 files changed, 101 insertions(+), 9 deletions(-) diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index 5b660031ba..de0b46770a 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -115,7 +115,7 @@ class Command(BaseCommand): 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(force_str('%s: ' % capfirst(field.verbose_name))) + raw_value = input(force_str('%s%s: ' % (capfirst(field.verbose_name), ' (%s.%s)' % (field.rel.to._meta.object_name, field.rel.field_name) if field.rel else ''))) try: user_data[field_name] = field.clean(raw_value, None) except exceptions.ValidationError as e: diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py index 394baa3204..cfa998ad2f 100644 --- a/django/contrib/auth/tests/custom_user.py +++ b/django/contrib/auth/tests/custom_user.py @@ -38,6 +38,18 @@ class CustomUserManager(BaseUserManager): return u +class CustomUserWithFKManager(BaseUserManager): + def create_superuser(self, username, email, group, password): + user = self.model(username=username, email_id=email, group_id=group) + user.set_password(password) + user.save(using=self._db) + return user + + +class Email(models.Model): + email = models.EmailField(verbose_name='email address', max_length=255, unique=True) + + class CustomUser(AbstractBaseUser): email = models.EmailField(verbose_name='email address', max_length=255, unique=True) is_active = models.BooleanField(default=True) @@ -83,6 +95,20 @@ class CustomUser(AbstractBaseUser): return self.is_admin +class CustomUserWithFK(AbstractBaseUser): + username = models.CharField(max_length=30, unique=True) + email = models.ForeignKey(Email, to_field='email') + group = models.ForeignKey(Group) + + custom_objects = CustomUserWithFKManager() + + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['email', 'group'] + + class Meta: + app_label = 'auth' + + # At this point, temporarily remove the groups and user_permissions M2M # fields from the AbstractUser class, so they don't clash with the related_name # that sets. diff --git a/django/contrib/auth/tests/test_management.py b/django/contrib/auth/tests/test_management.py index fc016135b2..e774d3a071 100644 --- a/django/contrib/auth/tests/test_management.py +++ b/django/contrib/auth/tests/test_management.py @@ -9,8 +9,8 @@ from django.contrib.auth import models, management from django.contrib.auth.checks import check_user_model from django.contrib.auth.management import create_permissions from django.contrib.auth.management.commands import changepassword, createsuperuser -from django.contrib.auth.models import User -from django.contrib.auth.tests.custom_user import CustomUser +from django.contrib.auth.models import User, Group +from django.contrib.auth.tests.custom_user import CustomUser, CustomUserWithFK, Email from django.contrib.auth.tests.utils import skipIfCustomUser from django.contrib.contenttypes.models import ContentType from django.core import checks @@ -349,6 +349,62 @@ class CreatesuperuserManagementCommandTestCase(TestCase): ) self.assertIs(command.stdin, sys.stdin) + @override_settings(AUTH_USER_MODEL='auth.CustomUserWithFK') + def test_required_field_with_fk(self): + new_io = six.StringIO() + group = Group.objects.create(name='mygroup') + email = Email.objects.create(email='mymail@gmail.com') + call_command( + 'createsuperuser', + interactive=False, + username='user', + email='mymail@gmail.com', + group=group.pk, + stdout=new_io, + skip_checks=True, + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + u = CustomUserWithFK._default_manager.get(email=email) + self.assertEqual(u.username, "user") + self.assertEqual(u.group, group) + + non_existent_email = 'mymail2@gmail.com' + with self.assertRaisesMessage(CommandError, + 'email instance with email %r does not exist.' % non_existent_email): + call_command( + 'createsuperuser', + interactive=False, + username='user', + email=non_existent_email, + stdout=new_io, + skip_checks=True, + ) + + @override_settings(AUTH_USER_MODEL='auth.CustomUserWithFK') + def test_required_fields_with_fk_interactive(self): + new_io = six.StringIO() + group = Group.objects.create(name='mygroup') + email = Email.objects.create(email='mymail@gmail.com') + + @mock_inputs({'password': "nopasswd", 'username': "user", 'email': "mymail@gmail.com", 'group': group.pk}) + def test(self): + call_command( + 'createsuperuser', + interactive=True, + stdout=new_io, + stdin=MockTTY(), + skip_checks=True, + ) + + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + u = CustomUserWithFK._default_manager.get(email=email) + self.assertEqual(u.username, 'user') + self.assertEqual(u.group, group) + + test(self) + class CustomUserModelValidationTestCase(TestCase): @override_settings(AUTH_USER_MODEL='auth.CustomUserNonListRequiredFields') diff --git a/django/db/utils.py b/django/db/utils.py index 4fb75e3150..f1f43ee5ed 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -286,10 +286,10 @@ class ConnectionRouter(object): chosen_db = method(model, **hints) if chosen_db: return chosen_db - try: - return hints['instance']._state.db or DEFAULT_DB_ALIAS - except KeyError: - return DEFAULT_DB_ALIAS + instance = hints.get('instance') + if instance is not None and instance._state.db: + return instance._state.db + return DEFAULT_DB_ALIAS return _route_db db_for_read = _router_func('db_for_read') diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 3e2790c602..70858869c9 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -49,6 +49,8 @@ Minor features * The ``max_length`` of :attr:`Permission.name ` has been increased from 50 to 255 characters. Please run the database migration. +* :attr:`~django.contrib.auth.models.CustomUser.REQUIRED_FIELDS` now supports + :class:`~django.db.models.ForeignKey`\s. :mod:`django.contrib.formtools` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index a70753132c..0b0582775a 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -515,8 +515,7 @@ password resets. You must then provide some key implementation details: will be prompted to supply a value for each of these fields. It must include any field for which :attr:`~django.db.models.Field.blank` is ``False`` or undefined and may include additional fields you want - prompted for when a user is created interactively. However, it will not - work for :class:`~django.db.models.ForeignKey` fields. + prompted for when a user is created interactively. ``REQUIRED_FIELDS`` has no effect in other parts of Django, like creating a user in the admin. @@ -536,6 +535,15 @@ password resets. You must then provide some key implementation details: ``User`` model, but should *not* contain the ``USERNAME_FIELD`` or ``password`` as these fields will always be prompted for. + .. versionadded:: 1.8 + + :attr:`REQUIRED_FIELDS` now supports + :class:`~django.db.models.ForeignKey`\s. Since there is no way to pass + model instances during the :djadmin:`createsuperuser` prompt, expect the + user to enter the value of :attr:`~django.db.models.ForeignKey.to_field` + value (the :attr:`~django.db.models.Field.primary_key` by default) of an + existing instance. + .. attribute:: is_active A boolean attribute that indicates whether the user is considered