diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index ac6b8221df..24bab96df6 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -684,7 +684,7 @@ class ForeignObject(RelatedField): # Internal FK's - i.e., those with a related name ending with '+' - # and swapped models don't get a related descriptor. if not self.remote_field.is_hidden() and not related.related_model._meta.swapped: - setattr(cls, related.get_accessor_name(), self.related_accessor_class(related)) + setattr(cls._meta.concrete_model, related.get_accessor_name(), self.related_accessor_class(related)) # While 'limit_choices_to' might be a callable, simply pass # it along for later - this is too early because it's still # model load time. diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py index e9caa2680f..486829386f 100644 --- a/django/db/models/fields/related_descriptors.py +++ b/django/db/models/fields/related_descriptors.py @@ -198,7 +198,7 @@ class ForwardManyToOneDescriptor(object): 'Cannot assign None: "%s.%s" does not allow null values.' % (instance._meta.object_name, self.field.name) ) - elif value is not None and not isinstance(value, self.field.remote_field.model): + elif value is not None and not isinstance(value, self.field.remote_field.model._meta.concrete_model): raise ValueError( 'Cannot assign "%r": "%s.%s" must be a "%s" instance.' % ( value, diff --git a/django/db/models/options.py b/django/db/models/options.py index 28c2088630..28773bc0b3 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -552,15 +552,20 @@ class Options(object): is set as a property on every model. """ related_objects_graph = defaultdict(list) + # Map of concrete models to all options of models it represents. + # Including its options and all its proxy model ones. + concrete_model_classes = defaultdict(list) all_models = self.apps.get_models(include_auto_created=True) for model in all_models: + opts = model._meta + concrete_model_classes[opts.concrete_model].append(opts) # Abstract model's fields are copied to child models, hence we will # see the fields from the child models. - if model._meta.abstract: + if opts.abstract: continue fields_with_relations = ( - f for f in model._meta._get_fields(reverse=False, include_parents=False) + f for f in opts._get_fields(reverse=False, include_parents=False) if f.is_relation and f.related_model is not None ) for f in fields_with_relations: @@ -573,7 +578,9 @@ class Options(object): # __dict__ takes precedence over a data descriptor (such as # @cached_property). This means that the _meta._relation_tree is # only called if related_objects is not in __dict__. - related_objects = related_objects_graph[model._meta] + related_objects = list(chain.from_iterable( + related_objects_graph[opts] for opts in concrete_model_classes[model] + )) model._meta.__dict__['_relation_tree'] = related_objects # It seems it is possible that self is not in all_models, so guard # against that with default for get(). diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index c720c65b80..4868930bf8 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -163,7 +163,11 @@ Migrations Models ^^^^^^ -* ... +* Reverse foreign keys from proxy models are now propagated to their + concrete class. The reverse relation attached by a + :class:`~django.db.models.ForeignKey` pointing to a proxy model is now + accessible as a descriptor on the proxied model class and may be referenced in + queryset filtering. Requests and Responses ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/model_meta/models.py b/tests/model_meta/models.py index 5189543880..a9c1f8f98d 100644 --- a/tests/model_meta/models.py +++ b/tests/model_meta/models.py @@ -113,7 +113,7 @@ class Relating(models.Model): # ForeignKey to ProxyPerson proxyperson = models.ForeignKey(ProxyPerson, models.CASCADE, related_name='relating_proxyperson') - proxyperson_hidden = models.ForeignKey(ProxyPerson, models.CASCADE, related_name='+') + proxyperson_hidden = models.ForeignKey(ProxyPerson, models.CASCADE, related_name='relating_proxyperson_hidden+') # ManyToManyField to BasePerson basepeople = models.ManyToManyField(BasePerson, related_name='relating_basepeople') diff --git a/tests/model_meta/results.py b/tests/model_meta/results.py index a4d9010da8..5975f06465 100644 --- a/tests/model_meta/results.py +++ b/tests/model_meta/results.py @@ -331,6 +331,8 @@ TEST_RESULTS = { ('friends_inherited_rel_+', None), ('relating_people', None), ('relating_person', None), + ('relating_proxyperson', None), + ('relating_proxyperson_hidden+', None), ), BasePerson: ( ('+', None), @@ -413,6 +415,8 @@ TEST_RESULTS = { ('relating_baseperson', BasePerson), ('relating_people', None), ('relating_person', None), + ('relating_proxyperson', None), + ('relating_proxyperson_hidden+', None), ), BasePerson: ( ('+', None), @@ -465,6 +469,7 @@ TEST_RESULTS = { ('followers_concrete', None), ('relating_person', None), ('relating_people', None), + ('relating_proxyperson', None), ), BasePerson: ( ('followers_abstract', None), @@ -494,6 +499,7 @@ TEST_RESULTS = { ('followers_concrete', None), ('relating_person', None), ('relating_people', None), + ('relating_proxyperson', None), ), BasePerson: ( ('followers_abstract', None), diff --git a/tests/proxy_models/models.py b/tests/proxy_models/models.py index 145f70dac6..6c5f98ec14 100644 --- a/tests/proxy_models/models.py +++ b/tests/proxy_models/models.py @@ -158,7 +158,7 @@ class ProxyTrackerUser(TrackerUser): @python_2_unicode_compatible class Issue(models.Model): summary = models.CharField(max_length=255) - assignee = models.ForeignKey(ProxyTrackerUser, models.CASCADE) + assignee = models.ForeignKey(ProxyTrackerUser, models.CASCADE, related_name='issues') def __str__(self): return ':'.join((self.__class__.__name__, self.summary,)) diff --git a/tests/proxy_models/tests.py b/tests/proxy_models/tests.py index 98244327a3..dd15976751 100644 --- a/tests/proxy_models/tests.py +++ b/tests/proxy_models/tests.py @@ -6,7 +6,7 @@ from django.apps import apps from django.contrib import admin from django.contrib.auth.models import User as AuthUser from django.contrib.contenttypes.models import ContentType -from django.core import checks, exceptions, management +from django.core import checks, management from django.core.urlresolvers import reverse from django.db import DEFAULT_DB_ALIAS, models from django.db.models import signals @@ -332,14 +332,19 @@ class ProxyModelTests(TestCase): self.assertEqual(resp.name, 'New South Wales') def test_filter_proxy_relation_reverse(self): - tu = TrackerUser.objects.create( - name='Contributor', status='contrib') - with self.assertRaises(exceptions.FieldError): - TrackerUser.objects.filter(issue=None), + tu = TrackerUser.objects.create(name='Contributor', status='contrib') + ptu = ProxyTrackerUser.objects.get() + issue = Issue.objects.create(assignee=tu) + self.assertEqual(tu.issues.get(), issue) + self.assertEqual(ptu.issues.get(), issue) self.assertQuerysetEqual( - ProxyTrackerUser.objects.filter(issue=None), + TrackerUser.objects.filter(issues=issue), [tu], lambda x: x ) + self.assertQuerysetEqual( + ProxyTrackerUser.objects.filter(issues=issue), + [ptu], lambda x: x + ) def test_proxy_bug(self): contributor = ProxyTrackerUser.objects.create(name='Contributor',