From 21130ce1a9c4fcbfce4c614be9e5408b43092bf0 Mon Sep 17 00:00:00 2001 From: Sergey Fedoseev Date: Tue, 7 Jun 2016 13:55:27 +0500 Subject: [PATCH] Fixed #26718 -- Added system check for existence of the fields specified by ForeignKey.to_field. --- django/db/models/fields/related.py | 23 +++++++++ docs/ref/checks.txt | 2 + .../test_relative_fields.py | 50 +++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 44e053847a..7caba63c3e 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -463,9 +463,32 @@ class ForeignObject(RelatedField): def check(self, **kwargs): errors = super(ForeignObject, self).check(**kwargs) + errors.extend(self._check_to_fields_exist()) errors.extend(self._check_unique_target()) return errors + def _check_to_fields_exist(self): + # Skip nonexistent models. + if isinstance(self.remote_field.model, six.string_types): + return [] + + errors = [] + for to_field in self.to_fields: + if to_field: + try: + self.remote_field.model._meta.get_field(to_field) + except exceptions.FieldDoesNotExist: + errors.append( + checks.Error( + "The to_field '%s' doesn't exist on the related " + "model '%s'." + % (to_field, self.remote_field.model._meta.label), + obj=self, + id='fields.E312', + ) + ) + return errors + def _check_unique_target(self): rel_is_string = isinstance(self.remote_field.model, six.string_types) if rel_is_string or not self.requires_unique_target: diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index ac96fbf412..18b6d88add 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -211,6 +211,8 @@ Related Fields add at least a subset of them to a unique_together constraint. * **fields.E311**: ```` must set ``unique=True`` because it is referenced by a ``ForeignKey``. +* **fields.E312**: The ``to_field`` ```` doesn't exist on the + related model ``.``. * **fields.E320**: Field specifies ``on_delete=SET_NULL``, but cannot be null. * **fields.E321**: The field specifies ``on_delete=SET_DEFAULT``, but has no default value. diff --git a/tests/invalid_models_tests/test_relative_fields.py b/tests/invalid_models_tests/test_relative_fields.py index 8b95cf3e18..2534b345c2 100644 --- a/tests/invalid_models_tests/test_relative_fields.py +++ b/tests/invalid_models_tests/test_relative_fields.py @@ -763,6 +763,56 @@ class RelativeFieldTests(SimpleTestCase): errors = Child.check() self.assertFalse(errors) + def test_to_fields_exist(self): + class Parent(models.Model): + pass + + class Child(models.Model): + a = models.PositiveIntegerField() + b = models.PositiveIntegerField() + parent = ForeignObject( + Parent, + on_delete=models.SET_NULL, + from_fields=('a', 'b'), + to_fields=('a', 'b'), + ) + + field = Child._meta.get_field('parent') + expected = [ + Error( + "The to_field 'a' doesn't exist on the related model 'invalid_models_tests.Parent'.", + obj=field, + id='fields.E312', + ), + Error( + "The to_field 'b' doesn't exist on the related model 'invalid_models_tests.Parent'.", + obj=field, + id='fields.E312', + ), + ] + self.assertEqual(field.check(), expected) + + def test_to_fields_not_checked_if_related_model_doesnt_exist(self): + class Child(models.Model): + a = models.PositiveIntegerField() + b = models.PositiveIntegerField() + parent = ForeignObject( + 'invalid_models_tests.Parent', + on_delete=models.SET_NULL, + from_fields=('a', 'b'), + to_fields=('a', 'b'), + ) + + field = Child._meta.get_field('parent') + self.assertEqual(field.check(), [ + Error( + "Field defines a relation with model 'invalid_models_tests.Parent', " + "which is either not installed, or is abstract.", + id='fields.E300', + obj=field, + ), + ]) + @isolate_apps('invalid_models_tests') class AccessorClashTests(SimpleTestCase):