Fixed #18012 -- Propagated reverse foreign keys from proxy to concrete models.
Thanks to Anssi for the review.
This commit is contained in:
parent
c8f091f5bc
commit
6c9f37ea9e
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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().
|
||||
|
|
|
@ -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
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,))
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue