summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHasan Ramezani <hasan.r67@gmail.com>2019-07-31 17:06:59 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2019-08-26 14:48:40 +0200
commit03dbdfd9bbbbd0b0172aad648c6bbe3f39541137 (patch)
treecca311097a1679b6f50b4f26730d5c5eb4c18e3e
parent5dac63bb844d0a976e1dd1591a323c5ba9674a97 (diff)
Fixed #29019 -- Added ManyToManyField support to REQUIRED_FIELDS.
-rw-r--r--django/contrib/auth/management/commands/createsuperuser.py44
-rw-r--r--docs/releases/3.0.txt3
-rw-r--r--docs/topics/auth/customizing.txt8
-rw-r--r--tests/auth_tests/models/__init__.py10
-rw-r--r--tests/auth_tests/models/with_many_to_many.py40
-rw-r--r--tests/auth_tests/test_management.py85
6 files changed, 175 insertions, 15 deletions
diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py
index ece66dfad1..3b76477f01 100644
--- a/django/contrib/auth/management/commands/createsuperuser.py
+++ b/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 '',
)
diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt
index b9afc2ad59..722baeeeaa 100644
--- a/docs/releases/3.0.txt
+++ b/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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt
index 7d287051d8..afc9bb3e62 100644
--- a/docs/topics/auth/customizing.txt
+++ b/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::
diff --git a/tests/auth_tests/models/__init__.py b/tests/auth_tests/models/__init__.py
index 785e6953ff..003d8eeaa7 100644
--- a/tests/auth_tests/models/__init__.py
+++ b/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',
)
diff --git a/tests/auth_tests/models/with_many_to_many.py b/tests/auth_tests/models/with_many_to_many.py
new file mode 100644
index 0000000000..82142048ec
--- /dev/null
+++ b/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)
diff --git a/tests/auth_tests/test_management.py b/tests/auth_tests/test_management.py
index bf9b063102..7792a374f8 100644
--- a/tests/auth_tests/test_management.py
+++ b/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.