Fixed #29019 -- Added ManyToManyField support to REQUIRED_FIELDS.
This commit is contained in:
parent
5dac63bb84
commit
03dbdfd9bb
|
@ -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 '',
|
||||
)
|
||||
|
||||
|
|
|
@ -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`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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::
|
||||
|
||||
|
|
|
@ -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',
|
||||
)
|
||||
|
|
|
@ -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)
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue