Commit 0bce2f10 authored by Berker Peksag's avatar Berker Peksag Committed by Tim Graham
Browse files

Fixed #12810 -- Added a check for clashing ManyToManyField.db_table names.

parent faeeb84e
Loading
Loading
Loading
Loading
+31 −0
Original line number Diff line number Diff line
@@ -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.
+2 −0
Original line number Diff line number Diff line
@@ -245,6 +245,8 @@ Related Fields
* **fields.E338**: The intermediary model ``<through model>`` has no field
  ``<field name>``.
* **fields.E339**: ``<model>.<field name>`` is not a foreign key to ``<model>``.
* **fields.E340**: The field's intermediary table ``<table name>`` clashes with
  the table name of ``<model>``/``<model>.<field name>``.
* **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
+66 −0
Original line number Diff line number Diff line
@@ -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',
            )
        ])