diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index f64ca22b5d..93a0879e0d 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -194,12 +194,6 @@ class RelatedField(Field): if not isinstance(self.rel.to, ModelBase): return [] - # If the field doesn't install backward relation on the target model (so - # `is_hidden` returns True), then there are no clashes to check and we - # can skip these fields. - if self.rel.is_hidden(): - return [] - try: self.rel except AttributeError: @@ -216,12 +210,15 @@ class RelatedField(Field): # foreign = models.ForeignKey(Target) # m2m = models.ManyToManyField(Target) - rel_opts = self.rel.to._meta # rel_opts.object_name == "Target" + rel_opts = self.rel.to._meta + # If the field doesn't install a backward relation on the target model + # (so `is_hidden` returns True), then there are no clashes to check + # and we can skip these fields. + rel_is_hidden = self.rel.is_hidden() rel_name = self.rel.get_accessor_name() # i. e. "model_set" rel_query_name = self.related_query_name() # i. e. "model" - field_name = "%s.%s" % (opts.object_name, - self.name) # i. e. "Model.field" + field_name = "%s.%s" % (opts.object_name, self.name) # i. e. "Model.field" # Check clashes between accessor or reverse query name of `field` # and any other field name -- i.e. accessor for Model.foreign is @@ -230,7 +227,7 @@ class RelatedField(Field): for clash_field in potential_clashes: clash_name = "%s.%s" % (rel_opts.object_name, clash_field.name) # i. e. "Target.model_set" - if clash_field.name == rel_name: + if not rel_is_hidden and clash_field.name == rel_name: errors.append( checks.Error( "Reverse accessor for '%s' clashes with field name '%s'." % (field_name, clash_name), @@ -260,7 +257,7 @@ class RelatedField(Field): clash_name = "%s.%s" % ( # i. e. "Model.m2m" clash_field.related_model._meta.object_name, clash_field.field.name) - if clash_field.get_accessor_name() == rel_name: + if not rel_is_hidden and clash_field.get_accessor_name() == rel_name: errors.append( checks.Error( "Reverse accessor for '%s' clashes with reverse accessor for '%s'." % (field_name, clash_name), diff --git a/docs/releases/1.8.10.txt b/docs/releases/1.8.10.txt index c9e77552e6..1ceada6f54 100644 --- a/docs/releases/1.8.10.txt +++ b/docs/releases/1.8.10.txt @@ -11,3 +11,6 @@ Bugfixes * Fixed a crash on PostgreSQL that prevented using ``TIME_ZONE=None`` and ``USE_TZ=False`` (:ticket:`26177`). + +* Added system checks for query name clashes of hidden relationships + (:ticket:`26162`). diff --git a/tests/invalid_models_tests/test_relative_fields.py b/tests/invalid_models_tests/test_relative_fields.py index c18fe85538..9dd6eada7f 100644 --- a/tests/invalid_models_tests/test_relative_fields.py +++ b/tests/invalid_models_tests/test_relative_fields.py @@ -859,42 +859,66 @@ class ExplicitRelatedNameClashTests(IsolatedModelsTestCase): class ExplicitRelatedQueryNameClashTests(IsolatedModelsTestCase): - def test_fk_to_integer(self): + def test_fk_to_integer(self, related_name=None): self._test_explicit_related_query_name_clash( target=models.IntegerField(), relative=models.ForeignKey('Target', + related_name=related_name, related_query_name='clash')) - def test_fk_to_fk(self): + def test_hidden_fk_to_integer(self, related_name=None): + self.test_fk_to_integer(related_name='+') + + def test_fk_to_fk(self, related_name=None): self._test_explicit_related_query_name_clash( target=models.ForeignKey('Another'), relative=models.ForeignKey('Target', + related_name=related_name, related_query_name='clash')) - def test_fk_to_m2m(self): + def test_hidden_fk_to_fk(self): + self.test_fk_to_fk(related_name='+') + + def test_fk_to_m2m(self, related_name=None): self._test_explicit_related_query_name_clash( target=models.ManyToManyField('Another'), relative=models.ForeignKey('Target', + related_name=related_name, related_query_name='clash')) - def test_m2m_to_integer(self): + def test_hidden_fk_to_m2m(self): + self.test_fk_to_m2m(related_name='+') + + def test_m2m_to_integer(self, related_name=None): self._test_explicit_related_query_name_clash( target=models.IntegerField(), relative=models.ManyToManyField('Target', + related_name=related_name, related_query_name='clash')) - def test_m2m_to_fk(self): + def test_hidden_m2m_to_integer(self): + self.test_m2m_to_integer(related_name='+') + + def test_m2m_to_fk(self, related_name=None): self._test_explicit_related_query_name_clash( target=models.ForeignKey('Another'), relative=models.ManyToManyField('Target', + related_name=related_name, related_query_name='clash')) - def test_m2m_to_m2m(self): + def test_hidden_m2m_to_fk(self): + self.test_m2m_to_fk(related_name='+') + + def test_m2m_to_m2m(self, related_name=None): self._test_explicit_related_query_name_clash( target=models.ManyToManyField('Another'), relative=models.ManyToManyField('Target', + related_name=related_name, related_query_name='clash')) + def test_hidden_m2m_to_m2m(self): + self.test_m2m_to_m2m(related_name='+') + def _test_explicit_related_query_name_clash(self, target, relative): class Another(models.Model): pass