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,
|
default=DEFAULT_DB_ALIAS,
|
||||||
help='Specifies the database to use. Default is "default".',
|
help='Specifies the database to use. Default is "default".',
|
||||||
)
|
)
|
||||||
for field in self.UserModel.REQUIRED_FIELDS:
|
for field_name in self.UserModel.REQUIRED_FIELDS:
|
||||||
parser.add_argument(
|
field = self.UserModel._meta.get_field(field_name)
|
||||||
'--%s' % field,
|
if field.many_to_many:
|
||||||
help='Specifies the %s for the superuser.' % field,
|
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):
|
def execute(self, *args, **options):
|
||||||
self.stdin = options.get('stdin', sys.stdin) # Used for testing
|
self.stdin = options.get('stdin', sys.stdin) # Used for testing
|
||||||
|
@ -75,8 +92,8 @@ class Command(BaseCommand):
|
||||||
user_data[PASSWORD_FIELD] = None
|
user_data[PASSWORD_FIELD] = None
|
||||||
try:
|
try:
|
||||||
if options['interactive']:
|
if options['interactive']:
|
||||||
# Same as user_data but with foreign keys as fake model
|
# Same as user_data but without many to many fields and with
|
||||||
# instances instead of raw IDs.
|
# foreign keys as fake model instances instead of raw IDs.
|
||||||
fake_user_data = {}
|
fake_user_data = {}
|
||||||
if hasattr(self.stdin, 'isatty') and not self.stdin.isatty():
|
if hasattr(self.stdin, 'isatty') and not self.stdin.isatty():
|
||||||
raise NotRunningInTTYException
|
raise NotRunningInTTYException
|
||||||
|
@ -111,10 +128,17 @@ class Command(BaseCommand):
|
||||||
message = self._get_input_message(field)
|
message = self._get_input_message(field)
|
||||||
input_value = self.get_input_data(field, message)
|
input_value = self.get_input_data(field, message)
|
||||||
user_data[field_name] = input_value
|
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
|
# 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)
|
fake_user_data[field_name] = field.remote_field.model(input_value)
|
||||||
|
|
||||||
# Prompt for a password if the model has one.
|
# 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 '',
|
" (leave blank to use '%s')" % default if default else '',
|
||||||
' (%s.%s)' % (
|
' (%s.%s)' % (
|
||||||
field.remote_field.model._meta.object_name,
|
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 '',
|
) if field.remote_field else '',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,9 @@ Minor features
|
||||||
password and required fields, when a corresponding command line argument
|
password and required fields, when a corresponding command line argument
|
||||||
isn't provided in non-interactive mode.
|
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`
|
: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
|
``REQUIRED_FIELDS`` has no effect in other parts of Django, like
|
||||||
creating a user in the admin.
|
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
|
For example, here is the partial definition for a user model that
|
||||||
defines two required fields - a date of birth and height::
|
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_foreign_key import CustomUserWithFK, Email
|
||||||
from .with_integer_username import IntegerUsernameUser
|
from .with_integer_username import IntegerUsernameUser
|
||||||
from .with_last_login_attr import UserWithDisabledLastLoginField
|
from .with_last_login_attr import UserWithDisabledLastLoginField
|
||||||
|
from .with_many_to_many import (
|
||||||
|
CustomUserWithM2M, CustomUserWithM2MThrough, Organization,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CustomPermissionsUser', 'CustomUser', 'CustomUserNonUniqueUsername',
|
'CustomPermissionsUser', 'CustomUser', 'CustomUserNonUniqueUsername',
|
||||||
'CustomUserWithFK', 'CustomUserWithoutIsActiveField', 'Email',
|
'CustomUserWithFK', 'CustomUserWithM2M', 'CustomUserWithM2MThrough',
|
||||||
'ExtensionUser', 'IntegerUsernameUser', 'IsActiveTestUser1', 'MinimalUser',
|
'CustomUserWithoutIsActiveField', 'Email', 'ExtensionUser',
|
||||||
'NoPasswordUser', 'Proxy', 'UUIDUser', 'UserProxy',
|
'IntegerUsernameUser', 'IsActiveTestUser1', 'MinimalUser',
|
||||||
|
'NoPasswordUser', 'Organization', 'Proxy', 'UUIDUser', 'UserProxy',
|
||||||
'UserWithDisabledLastLoginField',
|
'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 django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
CustomUser, CustomUserNonUniqueUsername, CustomUserWithFK, Email,
|
CustomUser, CustomUserNonUniqueUsername, CustomUserWithFK,
|
||||||
UserProxy,
|
CustomUserWithM2M, Email, Organization, UserProxy,
|
||||||
)
|
)
|
||||||
|
|
||||||
MOCK_INPUT_KEY_TO_PROMPTS = {
|
MOCK_INPUT_KEY_TO_PROMPTS = {
|
||||||
|
@ -500,6 +500,87 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
|
||||||
|
|
||||||
test(self)
|
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):
|
def test_default_username(self):
|
||||||
"""createsuperuser uses a default username when one isn't provided."""
|
"""createsuperuser uses a default username when one isn't provided."""
|
||||||
# Get the default username before creating a user.
|
# Get the default username before creating a user.
|
||||||
|
|
Loading…
Reference in New Issue