From 0bce2f102c5734d0c7ff1ebf0b10a316d58ed5ce Mon Sep 17 00:00:00 2001 From: Berker Peksag Date: Fri, 3 Jun 2016 12:55:30 -0700 Subject: [PATCH] Fixed #12810 -- Added a check for clashing ManyToManyField.db_table names. --- django/db/models/fields/related.py | 31 +++++++++++ docs/ref/checks.txt | 2 + tests/invalid_models_tests/test_models.py | 66 +++++++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 4b8b1804b5..0639bf3d79 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1194,6 +1194,7 @@ class ManyToManyField(RelatedField): errors.extend(self._check_unique(**kwargs)) errors.extend(self._check_relationship_model(**kwargs)) errors.extend(self._check_ignored_options(**kwargs)) + errors.extend(self._check_table_uniqueness(**kwargs)) return errors def _check_unique(self, **kwargs): @@ -1429,6 +1430,36 @@ class ManyToManyField(RelatedField): return errors + def _check_table_uniqueness(self, **kwargs): + if isinstance(self.remote_field.through, six.string_types): + return [] + registered_tables = { + model._meta.db_table: model + for model in self.opts.apps.get_models(include_auto_created=True) + if model != self.remote_field.through + } + m2m_db_table = self.m2m_db_table() + if m2m_db_table in registered_tables: + model = registered_tables[m2m_db_table] + if model._meta.auto_created: + def _get_field_name(model): + for field in model._meta.auto_created._meta.many_to_many: + if field.remote_field.through is model: + return field.name + opts = model._meta.auto_created._meta + clashing_obj = '%s.%s' % (opts.label, _get_field_name(model)) + else: + clashing_obj = '%s' % model._meta.label + return [ + checks.Error( + "The field's intermediary table '%s' clashes with the " + "table name of '%s'." % (m2m_db_table, clashing_obj), + obj=self, + id='fields.E340', + ) + ] + return [] + def deconstruct(self): name, path, args, kwargs = super(ManyToManyField, self).deconstruct() # Handle the simpler arguments. diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 074a901de8..aff3bd2bdc 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -245,6 +245,8 @@ Related Fields * **fields.E338**: The intermediary model ```` has no field ````. * **fields.E339**: ``.`` is not a foreign key to ````. +* **fields.E340**: The field's intermediary table ```` clashes with + the table name of ````/``.``. * **fields.W340**: ``null`` has no effect on ``ManyToManyField``. * **fields.W341**: ``ManyToManyField`` does not support ``validators``. * **fields.W342**: Setting ``unique=True`` on a ``ForeignKey`` has the same diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py index b8f15cae60..fa63b9e8a7 100644 --- a/tests/invalid_models_tests/test_models.py +++ b/tests/invalid_models_tests/test_models.py @@ -761,3 +761,69 @@ class OtherModelTests(SimpleTestCase): 'as an implicit link is deprecated.' ) self.assertEqual(ParkingLot._meta.pk.name, 'parent') + + def test_m2m_table_name_clash(self): + class Foo(models.Model): + bar = models.ManyToManyField('Bar', db_table='myapp_bar') + + class Meta: + db_table = 'myapp_foo' + + class Bar(models.Model): + class Meta: + db_table = 'myapp_bar' + + self.assertEqual(Foo.check(), [ + Error( + "The field's intermediary table 'myapp_bar' clashes with the " + "table name of 'invalid_models_tests.Bar'.", + obj=Foo._meta.get_field('bar'), + id='fields.E340', + ) + ]) + + def test_m2m_field_table_name_clash(self): + class Foo(models.Model): + pass + + class Bar(models.Model): + foos = models.ManyToManyField(Foo, db_table='clash') + + class Baz(models.Model): + foos = models.ManyToManyField(Foo, db_table='clash') + + self.assertEqual(Bar.check() + Baz.check(), [ + Error( + "The field's intermediary table 'clash' clashes with the " + "table name of 'invalid_models_tests.Baz.foos'.", + obj=Bar._meta.get_field('foos'), + id='fields.E340', + ), + Error( + "The field's intermediary table 'clash' clashes with the " + "table name of 'invalid_models_tests.Bar.foos'.", + obj=Baz._meta.get_field('foos'), + id='fields.E340', + ) + ]) + + def test_m2m_autogenerated_table_name_clash(self): + class Foo(models.Model): + class Meta: + db_table = 'bar_foos' + + class Bar(models.Model): + # The autogenerated `db_table` will be bar_foos. + foos = models.ManyToManyField(Foo) + + class Meta: + db_table = 'bar' + + self.assertEqual(Bar.check(), [ + Error( + "The field's intermediary table 'bar_foos' clashes with the " + "table name of 'invalid_models_tests.Foo'.", + obj=Bar._meta.get_field('foos'), + id='fields.E340', + ) + ])