diff --git a/django/db/models/base.py b/django/db/models/base.py index 19648fbed9..972da7bb06 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1191,9 +1191,10 @@ class Model(metaclass=ModelBase): *cls._check_long_column_names(), ] clash_errors = ( - cls._check_id_field() + - cls._check_field_name_clashes() + - cls._check_model_name_db_lookup_clashes() + *cls._check_id_field(), + *cls._check_field_name_clashes(), + *cls._check_model_name_db_lookup_clashes(), + *cls._check_property_name_related_field_accessor_clashes(), ) errors.extend(clash_errors) # If there are field name clashes, hide consequent column name @@ -1421,6 +1422,26 @@ class Model(metaclass=ModelBase): ) return errors + @classmethod + def _check_property_name_related_field_accessor_clashes(cls): + errors = [] + property_names = cls._meta._property_names + related_field_accessors = ( + f.get_attname() for f in cls._meta._get_fields(reverse=False) + if f.is_relation and f.related_model is not None + ) + for accessor in related_field_accessors: + if accessor in property_names: + errors.append( + checks.Error( + "The property '%s' clashes with a related field " + "accessor." % accessor, + obj=cls, + id='models.E025', + ) + ) + return errors + @classmethod def _check_index_together(cls): """Check the value of "index_together" option.""" diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index db86c49ee0..6310500d0c 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -292,6 +292,8 @@ Models underscore as it collides with the query lookup syntax. * **models.E024**: The model name ```` cannot contain double underscores as it collides with the query lookup syntax. +* **models.E025**: The property ```` clashes with a related + field accessor. Security -------- diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py index 85d7bca366..b271ddf55c 100644 --- a/tests/invalid_models_tests/test_models.py +++ b/tests/invalid_models_tests/test_models.py @@ -706,6 +706,22 @@ class OtherModelTests(SimpleTestCase): ) ]) + def test_property_and_related_field_accessor_clash(self): + class Model(models.Model): + fk = models.ForeignKey('self', models.CASCADE) + + @property + def fk_id(self): + pass + + self.assertEqual(Model.check(), [ + Error( + "The property 'fk_id' clashes with a related field accessor.", + obj=Model, + id='models.E025', + ) + ]) + @override_settings(TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model') def test_swappable_missing_app_name(self): class Model(models.Model):