Browse Source

Fixed #29019 -- Added ManyToManyField support to REQUIRED_FIELDS.

Hasan Ramezani 5 years ago
parent
commit
03dbdfd9bb

+ 34 - 10
django/contrib/auth/management/commands/createsuperuser.py

@@ -51,11 +51,28 @@ class Command(BaseCommand):
             default=DEFAULT_DB_ALIAS,
             help='Specifies the database to use. Default is "default".',
         )
-        for field in self.UserModel.REQUIRED_FIELDS:
-            parser.add_argument(
-                '--%s' % field,
-                help='Specifies the %s for the superuser.' % field,
-            )
+        for field_name in self.UserModel.REQUIRED_FIELDS:
+            field = self.UserModel._meta.get_field(field_name)
+            if field.many_to_many:
+                if field.remote_field.through and not field.remote_field.through._meta.auto_created:
+                    raise CommandError(
+                        "Required field '%s' specifies a many-to-many "
+                        "relation through model, which is not supported."
+                        % field_name
+                    )
+                else:
+                    parser.add_argument(
+                        '--%s' % field_name, action='append',
+                        help=(
+                            'Specifies the %s for the superuser. Can be used '
+                            'multiple times.' % field_name,
+                        ),
+                    )
+            else:
+                parser.add_argument(
+                    '--%s' % field_name,
+                    help='Specifies the %s for the superuser.' % field_name,
+                )
 
     def execute(self, *args, **options):
         self.stdin = options.get('stdin', sys.stdin)  # Used for testing
@@ -75,8 +92,8 @@ class Command(BaseCommand):
             user_data[PASSWORD_FIELD] = None
         try:
             if options['interactive']:
-                # Same as user_data but with foreign keys as fake model
-                # instances instead of raw IDs.
+                # Same as user_data but without many to many fields and with
+                # foreign keys as fake model instances instead of raw IDs.
                 fake_user_data = {}
                 if hasattr(self.stdin, 'isatty') and not self.stdin.isatty():
                     raise NotRunningInTTYException
@@ -111,10 +128,17 @@ class Command(BaseCommand):
                         message = self._get_input_message(field)
                         input_value = self.get_input_data(field, message)
                         user_data[field_name] = input_value
-                        fake_user_data[field_name] = input_value
+                        if field.many_to_many and input_value:
+                            if not input_value.strip():
+                                user_data[field_name] = None
+                                self.stderr.write('Error: This field cannot be blank.')
+                                continue
+                            user_data[field_name] = [pk.strip() for pk in input_value.split(',')]
+                        if not field.many_to_many:
+                            fake_user_data[field_name] = input_value
 
                         # Wrap any foreign keys in fake model instances
-                        if field.remote_field:
+                        if field.many_to_one:
                             fake_user_data[field_name] = field.remote_field.model(input_value)
 
                 # Prompt for a password if the model has one.
@@ -199,7 +223,7 @@ class Command(BaseCommand):
             " (leave blank to use '%s')" % default if default else '',
             ' (%s.%s)' % (
                 field.remote_field.model._meta.object_name,
-                field.remote_field.field_name,
+                field.m2m_target_field_name() if field.many_to_many else field.remote_field.field_name,
             ) if field.remote_field else '',
         )
 

+ 3 - 0
docs/releases/3.0.txt

@@ -118,6 +118,9 @@ Minor features
   password and required fields, when a corresponding command line argument
   isn't provided in non-interactive mode.
 
+* :attr:`~django.contrib.auth.models.CustomUser.REQUIRED_FIELDS` now supports
+  :class:`~django.db.models.ManyToManyField`\s.
+
 :mod:`django.contrib.contenttypes`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 8 - 0
docs/topics/auth/customizing.txt

@@ -576,6 +576,14 @@ password resets. You must then provide some key implementation details:
         ``REQUIRED_FIELDS`` has no effect in other parts of Django, like
         creating a user in the admin.
 
+        .. versionadded:: 3.0
+
+            :attr:`REQUIRED_FIELDS` now supports
+            :class:`~django.db.models.ManyToManyField`\s without a custom
+            through model. Since there is no way to pass model instances during
+            the :djadmin:`createsuperuser` prompt, expect the user to enter IDs
+            of existing instances of the class to which the model is related.
+
         For example, here is the partial definition for a user model that
         defines two required fields - a date of birth and height::
 

+ 7 - 3
tests/auth_tests/models/__init__.py

@@ -11,11 +11,15 @@ from .uuid_pk import UUIDUser
 from .with_foreign_key import CustomUserWithFK, Email
 from .with_integer_username import IntegerUsernameUser
 from .with_last_login_attr import UserWithDisabledLastLoginField
+from .with_many_to_many import (
+    CustomUserWithM2M, CustomUserWithM2MThrough, Organization,
+)
 
 __all__ = (
     'CustomPermissionsUser', 'CustomUser', 'CustomUserNonUniqueUsername',
-    'CustomUserWithFK', 'CustomUserWithoutIsActiveField', 'Email',
-    'ExtensionUser', 'IntegerUsernameUser', 'IsActiveTestUser1', 'MinimalUser',
-    'NoPasswordUser', 'Proxy', 'UUIDUser', 'UserProxy',
+    'CustomUserWithFK', 'CustomUserWithM2M', 'CustomUserWithM2MThrough',
+    'CustomUserWithoutIsActiveField', 'Email', 'ExtensionUser',
+    'IntegerUsernameUser', 'IsActiveTestUser1', 'MinimalUser',
+    'NoPasswordUser', 'Organization', 'Proxy', 'UUIDUser', 'UserProxy',
     'UserWithDisabledLastLoginField',
 )

+ 40 - 0
tests/auth_tests/models/with_many_to_many.py

@@ -0,0 +1,40 @@
+from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
+from django.db import models
+
+
+class Organization(models.Model):
+    name = models.CharField(max_length=255)
+
+
+class CustomUserWithM2MManager(BaseUserManager):
+    def create_superuser(self, username, orgs, password):
+        user = self.model(username=username)
+        user.set_password(password)
+        user.save(using=self._db)
+        user.orgs.add(*orgs)
+        return user
+
+
+class CustomUserWithM2M(AbstractBaseUser):
+    username = models.CharField(max_length=30, unique=True)
+    orgs = models.ManyToManyField(Organization)
+
+    custom_objects = CustomUserWithM2MManager()
+
+    USERNAME_FIELD = 'username'
+    REQUIRED_FIELDS = ['orgs']
+
+
+class CustomUserWithM2MThrough(AbstractBaseUser):
+    username = models.CharField(max_length=30, unique=True)
+    orgs = models.ManyToManyField(Organization, through='Membership')
+
+    custom_objects = CustomUserWithM2MManager()
+
+    USERNAME_FIELD = 'username'
+    REQUIRED_FIELDS = ['orgs']
+
+
+class Membership(models.Model):
+    user = models.ForeignKey(CustomUserWithM2MThrough, on_delete=models.CASCADE)
+    organization = models.ForeignKey(Organization, on_delete=models.CASCADE)

+ 83 - 2
tests/auth_tests/test_management.py

@@ -23,8 +23,8 @@ from django.test import TestCase, override_settings
 from django.utils.translation import gettext_lazy as _
 
 from .models import (
-    CustomUser, CustomUserNonUniqueUsername, CustomUserWithFK, Email,
-    UserProxy,
+    CustomUser, CustomUserNonUniqueUsername, CustomUserWithFK,
+    CustomUserWithM2M, Email, Organization, UserProxy,
 )
 
 MOCK_INPUT_KEY_TO_PROMPTS = {
@@ -500,6 +500,87 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
 
         test(self)
 
+    @override_settings(AUTH_USER_MODEL='auth_tests.CustomUserWithM2m')
+    def test_fields_with_m2m(self):
+        new_io = StringIO()
+        org_id_1 = Organization.objects.create(name='Organization 1').pk
+        org_id_2 = Organization.objects.create(name='Organization 2').pk
+        call_command(
+            'createsuperuser',
+            interactive=False,
+            username='joe',
+            orgs=[org_id_1, org_id_2],
+            stdout=new_io,
+        )
+        command_output = new_io.getvalue().strip()
+        self.assertEqual(command_output, 'Superuser created successfully.')
+        user = CustomUserWithM2M._default_manager.get(username='joe')
+        self.assertEqual(user.orgs.count(), 2)
+
+    @override_settings(AUTH_USER_MODEL='auth_tests.CustomUserWithM2M')
+    def test_fields_with_m2m_interactive(self):
+        new_io = StringIO()
+        org_id_1 = Organization.objects.create(name='Organization 1').pk
+        org_id_2 = Organization.objects.create(name='Organization 2').pk
+
+        @mock_inputs({
+            'password': 'nopasswd',
+            'Username: ': 'joe',
+            'Orgs (Organization.id): ': '%s, %s' % (org_id_1, org_id_2),
+        })
+        def test(self):
+            call_command(
+                'createsuperuser',
+                interactive=True,
+                stdout=new_io,
+                stdin=MockTTY(),
+            )
+            command_output = new_io.getvalue().strip()
+            self.assertEqual(command_output, 'Superuser created successfully.')
+            user = CustomUserWithM2M._default_manager.get(username='joe')
+            self.assertEqual(user.orgs.count(), 2)
+
+        test(self)
+
+    @override_settings(AUTH_USER_MODEL='auth_tests.CustomUserWithM2M')
+    def test_fields_with_m2m_interactive_blank(self):
+        new_io = StringIO()
+        org_id = Organization.objects.create(name='Organization').pk
+        entered_orgs = [str(org_id), ' ']
+
+        def return_orgs():
+            return entered_orgs.pop()
+
+        @mock_inputs({
+            'password': 'nopasswd',
+            'Username: ': 'joe',
+            'Orgs (Organization.id): ': return_orgs,
+        })
+        def test(self):
+            call_command(
+                'createsuperuser',
+                interactive=True,
+                stdout=new_io,
+                stderr=new_io,
+                stdin=MockTTY(),
+            )
+            self.assertEqual(
+                new_io.getvalue().strip(),
+                'Error: This field cannot be blank.\n'
+                'Superuser created successfully.',
+            )
+
+        test(self)
+
+    @override_settings(AUTH_USER_MODEL='auth_tests.CustomUserWithM2MThrough')
+    def test_fields_with_m2m_and_through(self):
+        msg = (
+            "Required field 'orgs' specifies a many-to-many relation through "
+            "model, which is not supported."
+        )
+        with self.assertRaisesMessage(CommandError, msg):
+            call_command('createsuperuser')
+
     def test_default_username(self):
         """createsuperuser uses a default username when one isn't provided."""
         # Get the default username before creating a user.