From 0223e213dd690b6b6e0669f836a20efb10998c83 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Mon, 22 Feb 2016 16:05:47 -0500 Subject: [PATCH] Fixed #26186 -- Documented how app relative relationships of abstract models behave. This partially reverts commit bc7d201bdbaeac14a49f51a9ef292d6312b4c45e. Thanks Tim for the review. Refs #25858. --- django/db/models/fields/related.py | 21 ++++-------- docs/ref/models/fields.txt | 29 ++++++++++++++++ docs/releases/1.9.3.txt | 4 +++ tests/model_fields/tests.py | 53 ++++++++++++++++++------------ 4 files changed, 71 insertions(+), 36 deletions(-) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 5357670d4f..f0aa5b230f 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -35,7 +35,7 @@ from .reverse_related import ( RECURSIVE_RELATIONSHIP_CONSTANT = 'self' -def resolve_relation(scope_model, relation, resolve_recursive_relationship=True): +def resolve_relation(scope_model, relation): """ Transform relation into a model or fully-qualified model string of the form "app_label.ModelName", relative to scope_model. @@ -50,11 +50,12 @@ def resolve_relation(scope_model, relation, resolve_recursive_relationship=True) """ # Check for recursive relations if relation == RECURSIVE_RELATIONSHIP_CONSTANT: - if resolve_recursive_relationship: - relation = scope_model + relation = scope_model + # Look for an "app.Model" relation - elif isinstance(relation, six.string_types) and '.' not in relation: - relation = "%s.%s" % (scope_model._meta.app_label, relation) + if isinstance(relation, six.string_types): + if "." not in relation: + relation = "%s.%s" % (scope_model._meta.app_label, relation) return relation @@ -312,11 +313,6 @@ class RelatedField(Field): field.remote_field.model = related field.do_related_class(related, model) lazy_related_operation(resolve_related_class, cls, self.remote_field.model, field=self) - else: - # Bind a lazy reference to the app in which the model is defined. - self.remote_field.model = resolve_relation( - cls, self.remote_field.model, resolve_recursive_relationship=False - ) def get_forward_related_filter(self, obj): """ @@ -1565,11 +1561,6 @@ class ManyToManyField(RelatedField): lazy_related_operation(resolve_through_model, cls, self.remote_field.through, field=self) elif not cls._meta.swapped: self.remote_field.through = create_many_to_many_intermediary_model(self, cls) - else: - # Bind a lazy reference to the app in which the model is defined. - self.remote_field.through = resolve_relation( - cls, self.remote_field.through, resolve_recursive_relationship=False - ) # Add the descriptor for the m2m relation. setattr(cls, self.name, ManyToManyDescriptor(self.remote_field, reverse=False)) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 6b972660a2..8a348f092e 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -1157,6 +1157,35 @@ you can use the name of the model, rather than the model object itself:: # ... pass +Relationships defined this way on :ref:`abstract models +` are resolved when the model is subclassed as a +concrete model and are not relative to the abstract model's ``app_label``: + +.. snippet:: + :filename: products/models.py + + from django.db import models + + class AbstractCar(models.Model): + manufacturer = models.ForeignKey('Manufacturer', on_delete=models.CASCADE) + + class Meta: + abstract = True + +.. snippet:: + :filename: production/models.py + + from django.db import models + from products.models import AbstractCar + + class Manufacturer(models.Model): + pass + + class Car(AbstractCar): + pass + + # Car.manufacturer will point to `production.Manufacturer` here. + To refer to models defined in another application, you can explicitly specify a model with the full application label. For example, if the ``Manufacturer`` model above is defined in another application called ``production``, you'd diff --git a/docs/releases/1.9.3.txt b/docs/releases/1.9.3.txt index e0447d2afe..e94d555ec0 100644 --- a/docs/releases/1.9.3.txt +++ b/docs/releases/1.9.3.txt @@ -52,3 +52,7 @@ Bugfixes * Prevented ``ContentTypeManager`` instances from sharing their cache (:ticket:`26286`). + +* Reverted a change in Django 1.9.2 (:ticket:`25858`) that prevented relative + lazy relationships defined on abstract models to be resolved according to + their concrete model's ``app_label`` (:ticket:`26186`). diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py index 7305e6ab9b..70f039e30e 100644 --- a/tests/model_fields/tests.py +++ b/tests/model_fields/tests.py @@ -254,10 +254,6 @@ class ForeignKeyTests(test.TestCase): @isolate_apps('model_fields', 'model_fields.tests') def test_abstract_model_app_relative_foreign_key(self): - class Refered(models.Model): - class Meta: - app_label = 'model_fields' - class AbstractReferent(models.Model): reference = models.ForeignKey('Refered', on_delete=models.CASCADE) @@ -265,11 +261,19 @@ class ForeignKeyTests(test.TestCase): app_label = 'model_fields' abstract = True - class ConcreteReferent(AbstractReferent): - class Meta: - app_label = 'tests' + def assert_app_model_resolved(label): + class Refered(models.Model): + class Meta: + app_label = label - self.assertEqual(ConcreteReferent._meta.get_field('reference').related_model, Refered) + class ConcreteReferent(AbstractReferent): + class Meta: + app_label = label + + self.assertEqual(ConcreteReferent._meta.get_field('reference').related_model, Refered) + + assert_app_model_resolved('model_fields') + assert_app_model_resolved('tests') class ManyToManyFieldTests(test.SimpleTestCase): @@ -295,14 +299,6 @@ class ManyToManyFieldTests(test.SimpleTestCase): @isolate_apps('model_fields', 'model_fields.tests') def test_abstract_model_app_relative_foreign_key(self): - class Refered(models.Model): - class Meta: - app_label = 'model_fields' - - class Through(models.Model): - refered = models.ForeignKey('Refered', on_delete=models.CASCADE) - referent = models.ForeignKey('tests.ConcreteReferent', on_delete=models.CASCADE) - class AbstractReferent(models.Model): reference = models.ManyToManyField('Refered', through='Through') @@ -310,12 +306,27 @@ class ManyToManyFieldTests(test.SimpleTestCase): app_label = 'model_fields' abstract = True - class ConcreteReferent(AbstractReferent): - class Meta: - app_label = 'tests' + def assert_app_model_resolved(label): + class Refered(models.Model): + class Meta: + app_label = label - self.assertEqual(ConcreteReferent._meta.get_field('reference').related_model, Refered) - self.assertEqual(ConcreteReferent.reference.through, Through) + class Through(models.Model): + refered = models.ForeignKey('Refered', on_delete=models.CASCADE) + referent = models.ForeignKey('ConcreteReferent', on_delete=models.CASCADE) + + class Meta: + app_label = label + + class ConcreteReferent(AbstractReferent): + class Meta: + app_label = label + + self.assertEqual(ConcreteReferent._meta.get_field('reference').related_model, Refered) + self.assertEqual(ConcreteReferent.reference.through, Through) + + assert_app_model_resolved('model_fields') + assert_app_model_resolved('tests') class TextFieldTests(test.TestCase):