Fixed #18012 -- Propagated reverse foreign keys from proxy to concrete models.

Thanks to Anssi for the review.
This commit is contained in:
Simon Charette 2015-10-01 14:57:58 -04:00
parent c8f091f5bc
commit 6c9f37ea9e
8 changed files with 36 additions and 14 deletions

View File

@ -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.

View File

@ -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,

View File

@ -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().

View File

@ -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
^^^^^^^^^^^^^^^^^^^^^^

View File

@ -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')

View File

@ -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),

View File

@ -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,))

View File

@ -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',