diff --git a/django/db/models/base.py b/django/db/models/base.py index 1096a58b22..33c562138a 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1265,7 +1265,11 @@ class Model(six.with_metaclass(ModelBase)): errors.extend(cls._check_fields(**kwargs)) errors.extend(cls._check_m2m_through_same_relationship()) errors.extend(cls._check_long_column_names()) - clash_errors = cls._check_id_field() + cls._check_field_name_clashes() + clash_errors = ( + cls._check_id_field() + + cls._check_field_name_clashes() + + cls._check_model_name_db_lookup_clashes() + ) errors.extend(clash_errors) # If there are field name clashes, hide consequent column name # clashes. @@ -1469,6 +1473,30 @@ class Model(six.with_metaclass(ModelBase)): return errors + @classmethod + def _check_model_name_db_lookup_clashes(cls): + errors = [] + model_name = cls.__name__ + if model_name.startswith('_') or model_name.endswith('_'): + errors.append( + checks.Error( + "The model name '%s' cannot start or end with an underscore " + "as it collides with the query lookup syntax." % model_name, + obj=cls, + id='models.E023' + ) + ) + elif LOOKUP_SEP in model_name: + errors.append( + checks.Error( + "The model name '%s' cannot contain double underscores as " + "it collides with the query lookup syntax." % model_name, + obj=cls, + id='models.E024' + ) + ) + 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 638f9af564..a3c659aad5 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -141,6 +141,10 @@ Models * **models.E022**: ```` contains a lazy reference to ``.``, but app ```` isn't installed or doesn't provide model ````. +* **models.E023**: The model name ```` cannot start or end with an + 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. Fields ~~~~~~ diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py index 1d27e98faa..b50b265bd4 100644 --- a/tests/invalid_models_tests/test_models.py +++ b/tests/invalid_models_tests/test_models.py @@ -689,6 +689,45 @@ class OtherModelTests(SimpleTestCase): self.assertFalse(Child.check()) + def test_name_beginning_with_underscore(self): + class _Model(models.Model): + pass + + self.assertEqual(_Model.check(), [ + Error( + "The model name '_Model' cannot start or end with an underscore " + "as it collides with the query lookup syntax.", + obj=_Model, + id='models.E023', + ) + ]) + + def test_name_ending_with_underscore(self): + class Model_(models.Model): + pass + + self.assertEqual(Model_.check(), [ + Error( + "The model name 'Model_' cannot start or end with an underscore " + "as it collides with the query lookup syntax.", + obj=Model_, + id='models.E023', + ) + ]) + + def test_name_contains_double_underscores(self): + class Test__Model(models.Model): + pass + + self.assertEqual(Test__Model.check(), [ + Error( + "The model name 'Test__Model' cannot contain double underscores " + "as it collides with the query lookup syntax.", + obj=Test__Model, + id='models.E024', + ) + ]) + @override_settings(TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model') def test_swappable_missing_app_name(self): class Model(models.Model): diff --git a/tests/invalid_models_tests/test_relative_fields.py b/tests/invalid_models_tests/test_relative_fields.py index d178d6936e..7c20a0fb99 100644 --- a/tests/invalid_models_tests/test_relative_fields.py +++ b/tests/invalid_models_tests/test_relative_fields.py @@ -794,7 +794,7 @@ class RelativeFieldTests(SimpleTestCase): pass for related_name in related_names: - Child = type(str('Child_%s') % str(related_name), (models.Model,), { + Child = type(str('Child%s') % str(related_name), (models.Model,), { 'parent': models.ForeignKey('Parent', models.CASCADE, related_name=related_name), '__module__': Parent.__module__, })