From b9df2b74b98b4d63933e8061d3cfc1f6f39eb747 Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Wed, 28 Apr 2021 21:15:26 +0200 Subject: [PATCH] Fixed #32676 -- Prevented migrations from rendering related field attributes when not passed during initialization. Thanks Simon Charette for the implementation idea. --- django/db/models/fields/related.py | 42 +++++++++++++++++++++++------ docs/releases/4.0.txt | 16 ++++++++--- tests/field_deconstruction/tests.py | 28 +++++++++++++++++++ tests/schema/fields.py | 7 ++++- 4 files changed, 80 insertions(+), 13 deletions(-) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 899ae8efe8..a0e8da10fd 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -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 diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index 831bb9b854..eecbc9e4d8 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -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()``. diff --git a/tests/field_deconstruction/tests.py b/tests/field_deconstruction/tests.py index b746e46458..846f6d3ed7 100644 --- a/tests/field_deconstruction/tests.py +++ b/tests/field_deconstruction/tests.py @@ -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() diff --git a/tests/schema/fields.py b/tests/schema/fields.py index e4b62eab39..aaba202364 100644 --- a/tests/schema/fields.py +++ b/tests/schema/fields.py @@ -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 (