diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 18c6c4e607..ca4f4368b6 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1728,11 +1728,15 @@ class ManyToManyField(RelatedField): kwargs["db_table"] = self.db_table if self.remote_field.db_constraint is not True: kwargs["db_constraint"] = self.remote_field.db_constraint - # Rel needs more work. + # Lowercase model names as they should be treated as case-insensitive. if isinstance(self.remote_field.model, str): - kwargs["to"] = self.remote_field.model + if "." in self.remote_field.model: + app_label, model_name = self.remote_field.model.split(".") + kwargs["to"] = "%s.%s" % (app_label, model_name.lower()) + else: + kwargs["to"] = self.remote_field.model.lower() else: - kwargs["to"] = self.remote_field.model._meta.label + kwargs["to"] = self.remote_field.model._meta.label_lower if getattr(self.remote_field, "through", None) is not None: if isinstance(self.remote_field.through, str): kwargs["through"] = self.remote_field.through diff --git a/docs/releases/4.0.3.txt b/docs/releases/4.0.3.txt index 2ef642fe5e..17e9f65074 100644 --- a/docs/releases/4.0.3.txt +++ b/docs/releases/4.0.3.txt @@ -12,4 +12,6 @@ reformatted with `black`_. Bugfixes ======== -* ... +* Prevented, following a regression in Django 4.0.1, :djadmin:`makemigrations` + from generating infinite migrations for a model with ``ManyToManyField`` to + a lowercased swappable model such as ``'auth.user'`` (:ticket:`33515`). diff --git a/tests/field_deconstruction/tests.py b/tests/field_deconstruction/tests.py index 64b90953f1..c78ed62876 100644 --- a/tests/field_deconstruction/tests.py +++ b/tests/field_deconstruction/tests.py @@ -475,34 +475,34 @@ class FieldDeconstructionTests(SimpleTestCase): name, path, args, kwargs = field.deconstruct() self.assertEqual(path, "django.db.models.ManyToManyField") self.assertEqual(args, []) - self.assertEqual(kwargs, {"to": "auth.Permission"}) + self.assertEqual(kwargs, {"to": "auth.permission"}) self.assertFalse(hasattr(kwargs["to"], "setting_name")) # Test swappable field = models.ManyToManyField("auth.User") name, path, args, kwargs = field.deconstruct() self.assertEqual(path, "django.db.models.ManyToManyField") self.assertEqual(args, []) - self.assertEqual(kwargs, {"to": "auth.User"}) + self.assertEqual(kwargs, {"to": "auth.user"}) self.assertEqual(kwargs["to"].setting_name, "AUTH_USER_MODEL") # Test through field = models.ManyToManyField("auth.Permission", through="auth.Group") name, path, args, kwargs = field.deconstruct() self.assertEqual(path, "django.db.models.ManyToManyField") self.assertEqual(args, []) - self.assertEqual(kwargs, {"to": "auth.Permission", "through": "auth.Group"}) + self.assertEqual(kwargs, {"to": "auth.permission", "through": "auth.Group"}) # Test custom db_table field = models.ManyToManyField("auth.Permission", db_table="custom_table") name, path, args, kwargs = field.deconstruct() self.assertEqual(path, "django.db.models.ManyToManyField") self.assertEqual(args, []) - self.assertEqual(kwargs, {"to": "auth.Permission", "db_table": "custom_table"}) + self.assertEqual(kwargs, {"to": "auth.permission", "db_table": "custom_table"}) # Test related_name field = models.ManyToManyField("auth.Permission", related_name="custom_table") name, path, args, kwargs = field.deconstruct() self.assertEqual(path, "django.db.models.ManyToManyField") self.assertEqual(args, []) self.assertEqual( - kwargs, {"to": "auth.Permission", "related_name": "custom_table"} + kwargs, {"to": "auth.permission", "related_name": "custom_table"} ) # Test related_query_name field = models.ManyToManyField("auth.Permission", related_query_name="foobar") @@ -510,7 +510,7 @@ class FieldDeconstructionTests(SimpleTestCase): self.assertEqual(path, "django.db.models.ManyToManyField") self.assertEqual(args, []) self.assertEqual( - kwargs, {"to": "auth.Permission", "related_query_name": "foobar"} + kwargs, {"to": "auth.permission", "related_query_name": "foobar"} ) # Test limit_choices_to field = models.ManyToManyField( @@ -520,7 +520,7 @@ class FieldDeconstructionTests(SimpleTestCase): self.assertEqual(path, "django.db.models.ManyToManyField") self.assertEqual(args, []) self.assertEqual( - kwargs, {"to": "auth.Permission", "limit_choices_to": {"foo": "bar"}} + kwargs, {"to": "auth.permission", "limit_choices_to": {"foo": "bar"}} ) @override_settings(AUTH_USER_MODEL="auth.Permission") @@ -533,7 +533,7 @@ class FieldDeconstructionTests(SimpleTestCase): self.assertEqual(path, "django.db.models.ManyToManyField") self.assertEqual(args, []) - self.assertEqual(kwargs, {"to": "auth.Permission"}) + self.assertEqual(kwargs, {"to": "auth.permission"}) self.assertEqual(kwargs["to"].setting_name, "AUTH_USER_MODEL") def test_many_to_many_field_related_name(self): @@ -551,7 +551,7 @@ class FieldDeconstructionTests(SimpleTestCase): self.assertEqual(args, []) # deconstruct() should not include attributes which were not passed to # the field during initialization. - self.assertEqual(kwargs, {"to": "field_deconstruction.MyModel"}) + 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") @@ -559,7 +559,7 @@ class FieldDeconstructionTests(SimpleTestCase): self.assertEqual( kwargs, { - "to": "field_deconstruction.MyModel", + "to": "field_deconstruction.mymodel", "related_query_name": "custom_query_name", "limit_choices_to": {"flag": True}, }, diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index 36c6ffb872..8ddb339002 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -3279,6 +3279,31 @@ class AutodetectorTests(TestCase): [("__setting__", "AUTH_USER_MODEL")], ) + @override_settings(AUTH_USER_MODEL="thirdapp.CustomUser") + def test_swappable_many_to_many_model_case(self): + document_lowercase = ModelState( + "testapp", + "Document", + [ + ("id", models.AutoField(primary_key=True)), + ("owners", models.ManyToManyField(settings.AUTH_USER_MODEL.lower())), + ], + ) + document = ModelState( + "testapp", + "Document", + [ + ("id", models.AutoField(primary_key=True)), + ("owners", models.ManyToManyField(settings.AUTH_USER_MODEL)), + ], + ) + with isolate_lru_cache(apps.get_swappable_settings_name): + changes = self.get_changes( + [self.custom_user, document_lowercase], + [self.custom_user, document], + ) + self.assertEqual(len(changes), 0) + def test_swappable_changed(self): with isolate_lru_cache(apps.get_swappable_settings_name): before = self.make_project_state([self.custom_user, self.author_with_user])