Fixed #29019 -- Added ManyToManyField support to REQUIRED_FIELDS.

This commit is contained in:
Hasan Ramezani 2019-07-31 17:06:59 +02:00 committed by Mariusz Felisiak
parent 5dac63bb84
commit 03dbdfd9bb
6 changed files with 175 additions and 15 deletions

View File

@ -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 '',
)

View File

@ -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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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::

View File

@ -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',
)

View File

@ -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)

View File

@ -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.