Loading django/db/models/fields/related.py +31 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ from django.core import checks, exceptions from django.db import connection, router from django.db.backends import utils from django.db.models import Q from django.db.models.constants import LOOKUP_SEP from django.db.models.deletion import CASCADE, SET_DEFAULT, SET_NULL from django.db.models.query_utils import PathInfo from django.db.models.utils import make_model_tuple Loading Loading @@ -115,6 +116,7 @@ class RelatedField(Field): def check(self, **kwargs): errors = super(RelatedField, self).check(**kwargs) errors.extend(self._check_related_name_is_valid()) errors.extend(self._check_related_query_name_is_valid()) errors.extend(self._check_relation_model_exists()) errors.extend(self._check_referencing_to_swapped_model()) errors.extend(self._check_clashes()) Loading Loading @@ -148,6 +150,35 @@ class RelatedField(Field): ] return [] def _check_related_query_name_is_valid(self): if self.remote_field.is_hidden(): return [] rel_query_name = self.related_query_name() errors = [] if rel_query_name.endswith('_'): errors.append( checks.Error( "Reverse query name '%s' must not end with an underscore." % (rel_query_name,), hint=("Add or change a related_name or related_query_name " "argument for this field."), obj=self, id='fields.E308', ) ) if LOOKUP_SEP in rel_query_name: errors.append( checks.Error( "Reverse query name '%s' must not contain '%s'." % (rel_query_name, LOOKUP_SEP), hint=("Add or change a related_name or related_query_name " "argument for this field."), obj=self, id='fields.E309', ) ) return errors def _check_relation_model_exists(self): rel_is_missing = self.remote_field.model not in self.opts.apps.get_models() rel_is_string = isinstance(self.remote_field.model, six.string_types) Loading docs/ref/checks.txt +4 −0 Original line number Diff line number Diff line Loading @@ -206,6 +206,10 @@ Related Fields * **fields.E307**: The field ``<app label>.<model>.<field name>`` was declared with a lazy reference to ``<app label>.<model>``, but app ``<app label>`` isn't installed or doesn't provide model ``<model>``. * **fields.E308**: Reverse query name ``<related query name>`` must not end with an underscore. * **fields.E309**: Reverse query name ``<related query name>`` must not contain ``'__'``. * **fields.E310**: No subset of the fields ``<field1>``, ``<field2>``, ... on model ``<model>`` is unique. Add ``unique=True`` on any of those fields or add at least a subset of them to a unique_together constraint. Loading tests/invalid_models_tests/test_relative_fields.py +28 −3 Original line number Diff line number Diff line Loading @@ -714,7 +714,7 @@ class RelativeFieldTests(SimpleTestCase): pass for invalid_related_name in invalid_related_names: Child = type(str('Child_%s') % str(invalid_related_name), (models.Model,), { Child = type(str('Child%s') % str(invalid_related_name), (models.Model,), { 'parent': models.ForeignKey('Parent', models.CASCADE, related_name=invalid_related_name), '__module__': Parent.__module__, }) Loading @@ -723,7 +723,7 @@ class RelativeFieldTests(SimpleTestCase): errors = Child.check() expected = [ Error( "The name '%s' is invalid related_name for field Child_%s.parent" "The name '%s' is invalid related_name for field Child%s.parent" % (invalid_related_name, invalid_related_name), hint="Related name must be a valid Python identifier or end with a '+'", obj=field, Loading @@ -743,7 +743,6 @@ class RelativeFieldTests(SimpleTestCase): '_starts_with_underscore', 'contains_%s_digit' % digit, 'ends_with_plus+', '_', '_+', '+', ] Loading Loading @@ -813,6 +812,32 @@ class RelativeFieldTests(SimpleTestCase): ), ]) def test_invalid_related_query_name(self): class Target(models.Model): pass class Model(models.Model): first = models.ForeignKey(Target, models.CASCADE, related_name='contains__double') second = models.ForeignKey(Target, models.CASCADE, related_query_name='ends_underscore_') self.assertEqual(Model.check(), [ Error( "Reverse query name 'contains__double' must not contain '__'.", hint=("Add or change a related_name or related_query_name " "argument for this field."), obj=Model._meta.get_field('first'), id='fields.E309', ), Error( "Reverse query name 'ends_underscore_' must not end with an " "underscore.", hint=("Add or change a related_name or related_query_name " "argument for this field."), obj=Model._meta.get_field('second'), id='fields.E308', ), ]) @isolate_apps('invalid_models_tests') class AccessorClashTests(SimpleTestCase): Loading Loading
django/db/models/fields/related.py +31 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ from django.core import checks, exceptions from django.db import connection, router from django.db.backends import utils from django.db.models import Q from django.db.models.constants import LOOKUP_SEP from django.db.models.deletion import CASCADE, SET_DEFAULT, SET_NULL from django.db.models.query_utils import PathInfo from django.db.models.utils import make_model_tuple Loading Loading @@ -115,6 +116,7 @@ class RelatedField(Field): def check(self, **kwargs): errors = super(RelatedField, self).check(**kwargs) errors.extend(self._check_related_name_is_valid()) errors.extend(self._check_related_query_name_is_valid()) errors.extend(self._check_relation_model_exists()) errors.extend(self._check_referencing_to_swapped_model()) errors.extend(self._check_clashes()) Loading Loading @@ -148,6 +150,35 @@ class RelatedField(Field): ] return [] def _check_related_query_name_is_valid(self): if self.remote_field.is_hidden(): return [] rel_query_name = self.related_query_name() errors = [] if rel_query_name.endswith('_'): errors.append( checks.Error( "Reverse query name '%s' must not end with an underscore." % (rel_query_name,), hint=("Add or change a related_name or related_query_name " "argument for this field."), obj=self, id='fields.E308', ) ) if LOOKUP_SEP in rel_query_name: errors.append( checks.Error( "Reverse query name '%s' must not contain '%s'." % (rel_query_name, LOOKUP_SEP), hint=("Add or change a related_name or related_query_name " "argument for this field."), obj=self, id='fields.E309', ) ) return errors def _check_relation_model_exists(self): rel_is_missing = self.remote_field.model not in self.opts.apps.get_models() rel_is_string = isinstance(self.remote_field.model, six.string_types) Loading
docs/ref/checks.txt +4 −0 Original line number Diff line number Diff line Loading @@ -206,6 +206,10 @@ Related Fields * **fields.E307**: The field ``<app label>.<model>.<field name>`` was declared with a lazy reference to ``<app label>.<model>``, but app ``<app label>`` isn't installed or doesn't provide model ``<model>``. * **fields.E308**: Reverse query name ``<related query name>`` must not end with an underscore. * **fields.E309**: Reverse query name ``<related query name>`` must not contain ``'__'``. * **fields.E310**: No subset of the fields ``<field1>``, ``<field2>``, ... on model ``<model>`` is unique. Add ``unique=True`` on any of those fields or add at least a subset of them to a unique_together constraint. Loading
tests/invalid_models_tests/test_relative_fields.py +28 −3 Original line number Diff line number Diff line Loading @@ -714,7 +714,7 @@ class RelativeFieldTests(SimpleTestCase): pass for invalid_related_name in invalid_related_names: Child = type(str('Child_%s') % str(invalid_related_name), (models.Model,), { Child = type(str('Child%s') % str(invalid_related_name), (models.Model,), { 'parent': models.ForeignKey('Parent', models.CASCADE, related_name=invalid_related_name), '__module__': Parent.__module__, }) Loading @@ -723,7 +723,7 @@ class RelativeFieldTests(SimpleTestCase): errors = Child.check() expected = [ Error( "The name '%s' is invalid related_name for field Child_%s.parent" "The name '%s' is invalid related_name for field Child%s.parent" % (invalid_related_name, invalid_related_name), hint="Related name must be a valid Python identifier or end with a '+'", obj=field, Loading @@ -743,7 +743,6 @@ class RelativeFieldTests(SimpleTestCase): '_starts_with_underscore', 'contains_%s_digit' % digit, 'ends_with_plus+', '_', '_+', '+', ] Loading Loading @@ -813,6 +812,32 @@ class RelativeFieldTests(SimpleTestCase): ), ]) def test_invalid_related_query_name(self): class Target(models.Model): pass class Model(models.Model): first = models.ForeignKey(Target, models.CASCADE, related_name='contains__double') second = models.ForeignKey(Target, models.CASCADE, related_query_name='ends_underscore_') self.assertEqual(Model.check(), [ Error( "Reverse query name 'contains__double' must not contain '__'.", hint=("Add or change a related_name or related_query_name " "argument for this field."), obj=Model._meta.get_field('first'), id='fields.E309', ), Error( "Reverse query name 'ends_underscore_' must not end with an " "underscore.", hint=("Add or change a related_name or related_query_name " "argument for this field."), obj=Model._meta.get_field('second'), id='fields.E308', ), ]) @isolate_apps('invalid_models_tests') class AccessorClashTests(SimpleTestCase): Loading