Fixed #32676 -- Prevented migrations from rendering related field attributes when not passed during initialization.

Thanks Simon Charette for the implementation idea.
This commit is contained in:
David Wobrock 2021-04-28 21:15:26 +02:00 committed by Mariusz Felisiak
parent b746596f5f
commit b9df2b74b9
4 changed files with 80 additions and 13 deletions

View File

@ -89,6 +89,18 @@ class RelatedField(FieldCacheMixin, Field):
many_to_many = False
many_to_one = False
def __init__(
self,
related_name=None,
related_query_name=None,
limit_choices_to=None,
**kwargs,
):
self._related_name = related_name
self._related_query_name = related_query_name
self._limit_choices_to = limit_choices_to
super().__init__(**kwargs)
@cached_property
def related_model(self):
# Can't cache this property until all the models are loaded.
@ -319,12 +331,12 @@ class RelatedField(FieldCacheMixin, Field):
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self.remote_field.limit_choices_to:
kwargs['limit_choices_to'] = self.remote_field.limit_choices_to
if self.remote_field.related_name is not None:
kwargs['related_name'] = self.remote_field.related_name
if self.remote_field.related_query_name is not None:
kwargs['related_query_name'] = self.remote_field.related_query_name
if self._limit_choices_to:
kwargs['limit_choices_to'] = self._limit_choices_to
if self._related_name is not None:
kwargs['related_name'] = self._related_name
if self._related_query_name is not None:
kwargs['related_query_name'] = self._related_query_name
return name, path, args, kwargs
def get_forward_related_filter(self, obj):
@ -471,7 +483,13 @@ class ForeignObject(RelatedField):
on_delete=on_delete,
)
super().__init__(rel=rel, **kwargs)
super().__init__(
rel=rel,
related_name=related_name,
related_query_name=related_query_name,
limit_choices_to=limit_choices_to,
**kwargs,
)
self.from_fields = from_fields
self.to_fields = to_fields
@ -825,6 +843,9 @@ class ForeignKey(ForeignObject):
super().__init__(
to,
on_delete,
related_name=related_name,
related_query_name=related_query_name,
limit_choices_to=limit_choices_to,
from_fields=[RECURSIVE_RELATIONSHIP_CONSTANT],
to_fields=[to_field],
**kwargs,
@ -1174,7 +1195,12 @@ class ManyToManyField(RelatedField):
)
self.has_null_arg = 'null' in kwargs
super().__init__(**kwargs)
super().__init__(
related_name=related_name,
related_query_name=related_query_name,
limit_choices_to=limit_choices_to,
**kwargs,
)
self.db_table = db_table
self.swappable = swappable

View File

@ -387,6 +387,18 @@ custom middleware::
.. _Content-Security-Policy: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
Migrations autodetector changes
-------------------------------
The migrations autodetector now uses model states instead of model classes.
Also, migration operations for ``ForeignKey`` and ``ManyToManyField`` fields no
longer specify attributes which were not passed to the fields during
initialization.
As a side-effect, running ``makemigrations`` might generate no-op
``AlterField`` operations for ``ManyToManyField`` and ``ForeignKey`` fields in
some cases.
Miscellaneous
-------------
@ -422,10 +434,6 @@ Miscellaneous
* Tests that fail to load, for example due to syntax errors, now always match
when using :option:`test --tag`.
* The migrations autodetector now uses model states instead of model classes.
As a side-effect ``makemigrations`` might generate no-op ``AlterField``
operations for ``ForeignKey`` fields in some cases.
* The undocumented ``django.contrib.admin.utils.lookup_needs_distinct()``
function is renamed to ``lookup_spawns_duplicates()``.

View File

@ -432,6 +432,34 @@ class FieldDeconstructionTests(SimpleTestCase):
self.assertEqual(kwargs, {"to": "auth.Permission"})
self.assertEqual(kwargs['to'].setting_name, "AUTH_USER_MODEL")
def test_many_to_many_field_related_name(self):
class MyModel(models.Model):
flag = models.BooleanField(default=True)
m2m = models.ManyToManyField('self')
m2m_related_name = models.ManyToManyField(
'self',
related_name='custom_name',
related_query_name='custom_query_name',
limit_choices_to={'flag': True},
)
name, path, args, kwargs = MyModel.m2m.field.deconstruct()
self.assertEqual(path, 'django.db.models.ManyToManyField')
self.assertEqual(args, [])
# deconstruct() should not include attributes which were not passed to
# the field during initialization.
self.assertEqual(kwargs, {'to': 'field_deconstruction.MyModel'})
# Passed attributes.
name, path, args, kwargs = MyModel.m2m_related_name.field.deconstruct()
self.assertEqual(path, 'django.db.models.ManyToManyField')
self.assertEqual(args, [])
self.assertEqual(kwargs, {
'to': 'field_deconstruction.MyModel',
'related_name': 'custom_name',
'related_query_name': 'custom_query_name',
'limit_choices_to': {'flag': True},
})
def test_positive_integer_field(self):
field = models.PositiveIntegerField()
name, path, args, kwargs = field.deconstruct()

View File

@ -34,7 +34,12 @@ class CustomManyToManyField(RelatedField):
self.db_table = db_table
if kwargs['rel'].through is not None:
assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
super().__init__(**kwargs)
super().__init__(
related_name=related_name,
related_query_name=related_query_name,
limit_choices_to=limit_choices_to,
**kwargs,
)
def contribute_to_class(self, cls, name, **kwargs):
if self.remote_field.symmetrical and (