diff options
| author | Hasan Ramezani <hasan.r67@gmail.com> | 2019-03-01 17:09:33 +0100 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2019-03-01 17:09:33 +0100 |
| commit | 440505cb2cadbe1a5b9fba246bcde6c04f51d07e (patch) | |
| tree | efa7f88f683ecf7246a06600dd677b832c326e7b | |
| parent | 4dcbe6eb2de38a856dae39928692e46fbcf5c475 (diff) | |
Fixed #29408 -- Added validation of related fields and lookups in model Meta.ordering.
| -rw-r--r-- | django/db/models/base.py | 32 | ||||
| -rw-r--r-- | docs/ref/checks.txt | 4 | ||||
| -rw-r--r-- | tests/invalid_models_tests/test_models.py | 86 | ||||
| -rw-r--r-- | tests/postgres_tests/test_json.py | 9 |
4 files changed, 125 insertions, 6 deletions
diff --git a/django/db/models/base.py b/django/db/models/base.py index b53b33e47e..7e4fa9ac68 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1643,9 +1643,35 @@ class Model(metaclass=ModelBase): # Convert "-field" to "field". fields = ((f[1:] if f.startswith('-') else f) for f in fields) - # Skip ordering in the format field1__field2 (FIXME: checking - # this format would be nice, but it's a little fiddly). - fields = (f for f in fields if LOOKUP_SEP not in f) + # Separate related field and non related fields. + _fields = [] + related_fields = [] + for f in fields: + if LOOKUP_SEP in f: + related_fields.append(f) + else: + _fields.append(f) + fields = _fields + + # Check related fields. + for field in related_fields: + _cls = cls + fld = None + for part in field.split(LOOKUP_SEP): + try: + fld = _cls._meta.get_field(part) + if fld.is_relation: + _cls = fld.get_path_info()[-1].to_opts.model + except (FieldDoesNotExist, AttributeError): + if fld is None or fld.get_transform(part) is None: + errors.append( + checks.Error( + "'ordering' refers to the nonexistent field, " + "related field or lookup '%s'." % field, + obj=cls, + id='models.E015', + ) + ) # Skip ordering on pk. This is always a valid order_by field # but is an alias and therefore won't be found by opts.get_field. diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 6ec3028c35..0b9fd39eb9 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -277,8 +277,8 @@ Models supported for that option. * **models.E014**: ``ordering`` must be a tuple or list (even if you want to order by only one field). -* **models.E015**: ``ordering`` refers to the nonexistent field - ``<field name>``. +* **models.E015**: ``ordering`` refers to the nonexistent field, related field + or lookup ``<field name>``. * **models.E016**: ``indexes/index_together/unique_together`` refers to field ``<field_name>`` which is not local to model ``<model>``. * **models.E017**: Proxy model ``<model>`` contains model fields. diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py index 5b7042e127..320e799a6e 100644 --- a/tests/invalid_models_tests/test_models.py +++ b/tests/invalid_models_tests/test_models.py @@ -5,9 +5,10 @@ from django.core.checks import Error, Warning from django.core.checks.model_checks import _check_lazy_references from django.core.exceptions import ImproperlyConfigured from django.db import connection, connections, models +from django.db.models.functions import Lower from django.db.models.signals import post_init from django.test import SimpleTestCase -from django.test.utils import isolate_apps, override_settings +from django.test.utils import isolate_apps, override_settings, register_lookup def get_max_column_name_length(): @@ -665,6 +666,89 @@ class OtherModelTests(SimpleTestCase): ) ]) + def test_ordering_pointing_to_missing_related_field(self): + class Model(models.Model): + test = models.IntegerField() + + class Meta: + ordering = ('missing_related__id',) + + self.assertEqual(Model.check(), [ + Error( + "'ordering' refers to the nonexistent field, related field " + "or lookup 'missing_related__id'.", + obj=Model, + id='models.E015', + ) + ]) + + def test_ordering_pointing_to_missing_related_model_field(self): + class Parent(models.Model): + pass + + class Child(models.Model): + parent = models.ForeignKey(Parent, models.CASCADE) + + class Meta: + ordering = ('parent__missing_field',) + + self.assertEqual(Child.check(), [ + Error( + "'ordering' refers to the nonexistent field, related field " + "or lookup 'parent__missing_field'.", + obj=Child, + id='models.E015', + ) + ]) + + def test_ordering_pointing_to_non_related_field(self): + class Child(models.Model): + parent = models.IntegerField() + + class Meta: + ordering = ('parent__missing_field',) + + self.assertEqual(Child.check(), [ + Error( + "'ordering' refers to the nonexistent field, related field " + "or lookup 'parent__missing_field'.", + obj=Child, + id='models.E015', + ) + ]) + + def test_ordering_pointing_to_two_related_model_field(self): + class Parent2(models.Model): + pass + + class Parent1(models.Model): + parent2 = models.ForeignKey(Parent2, models.CASCADE) + + class Child(models.Model): + parent1 = models.ForeignKey(Parent1, models.CASCADE) + + class Meta: + ordering = ('parent1__parent2__missing_field',) + + self.assertEqual(Child.check(), [ + Error( + "'ordering' refers to the nonexistent field, related field " + "or lookup 'parent1__parent2__missing_field'.", + obj=Child, + id='models.E015', + ) + ]) + + def test_ordering_allows_registered_lookups(self): + class Model(models.Model): + test = models.CharField(max_length=100) + + class Meta: + ordering = ('test__lower',) + + with register_lookup(models.CharField, Lower): + self.assertEqual(Model.check(), []) + def test_ordering_pointing_to_foreignkey_field(self): class Parent(models.Model): pass diff --git a/tests/postgres_tests/test_json.py b/tests/postgres_tests/test_json.py index 6622820ec9..e95939be16 100644 --- a/tests/postgres_tests/test_json.py +++ b/tests/postgres_tests/test_json.py @@ -19,6 +19,15 @@ except ImportError: pass +class TestModelMetaOrdering(PostgreSQLTestCase): + def test_ordering_by_json_field_value(self): + class TestJSONModel(JSONModel): + class Meta: + ordering = ['field__value'] + + self.assertEqual(TestJSONModel.check(), []) + + class TestSaveLoad(PostgreSQLTestCase): def test_null(self): instance = JSONModel() |
