mirror of
https://github.com/django/django.git
synced 2024-12-31 21:46:05 +00:00
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 '+' -
|
# Internal FK's - i.e., those with a related name ending with '+' -
|
||||||
# and swapped models don't get a related descriptor.
|
# and swapped models don't get a related descriptor.
|
||||||
if not self.remote_field.is_hidden() and not related.related_model._meta.swapped:
|
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
|
# While 'limit_choices_to' might be a callable, simply pass
|
||||||
# it along for later - this is too early because it's still
|
# it along for later - this is too early because it's still
|
||||||
# model load time.
|
# model load time.
|
||||||
|
@ -198,7 +198,7 @@ class ForwardManyToOneDescriptor(object):
|
|||||||
'Cannot assign None: "%s.%s" does not allow null values.' %
|
'Cannot assign None: "%s.%s" does not allow null values.' %
|
||||||
(instance._meta.object_name, self.field.name)
|
(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(
|
raise ValueError(
|
||||||
'Cannot assign "%r": "%s.%s" must be a "%s" instance.' % (
|
'Cannot assign "%r": "%s.%s" must be a "%s" instance.' % (
|
||||||
value,
|
value,
|
||||||
|
@ -552,15 +552,20 @@ class Options(object):
|
|||||||
is set as a property on every model.
|
is set as a property on every model.
|
||||||
"""
|
"""
|
||||||
related_objects_graph = defaultdict(list)
|
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)
|
all_models = self.apps.get_models(include_auto_created=True)
|
||||||
for model in all_models:
|
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
|
# Abstract model's fields are copied to child models, hence we will
|
||||||
# see the fields from the child models.
|
# see the fields from the child models.
|
||||||
if model._meta.abstract:
|
if opts.abstract:
|
||||||
continue
|
continue
|
||||||
fields_with_relations = (
|
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
|
if f.is_relation and f.related_model is not None
|
||||||
)
|
)
|
||||||
for f in fields_with_relations:
|
for f in fields_with_relations:
|
||||||
@ -573,7 +578,9 @@ class Options(object):
|
|||||||
# __dict__ takes precedence over a data descriptor (such as
|
# __dict__ takes precedence over a data descriptor (such as
|
||||||
# @cached_property). This means that the _meta._relation_tree is
|
# @cached_property). This means that the _meta._relation_tree is
|
||||||
# only called if related_objects is not in __dict__.
|
# 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
|
model._meta.__dict__['_relation_tree'] = related_objects
|
||||||
# It seems it is possible that self is not in all_models, so guard
|
# It seems it is possible that self is not in all_models, so guard
|
||||||
# against that with default for get().
|
# against that with default for get().
|
||||||
|
@ -163,7 +163,11 @@ Migrations
|
|||||||
Models
|
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
|
Requests and Responses
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
@ -113,7 +113,7 @@ class Relating(models.Model):
|
|||||||
|
|
||||||
# ForeignKey to ProxyPerson
|
# ForeignKey to ProxyPerson
|
||||||
proxyperson = models.ForeignKey(ProxyPerson, models.CASCADE, related_name='relating_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
|
# ManyToManyField to BasePerson
|
||||||
basepeople = models.ManyToManyField(BasePerson, related_name='relating_basepeople')
|
basepeople = models.ManyToManyField(BasePerson, related_name='relating_basepeople')
|
||||||
|
@ -331,6 +331,8 @@ TEST_RESULTS = {
|
|||||||
('friends_inherited_rel_+', None),
|
('friends_inherited_rel_+', None),
|
||||||
('relating_people', None),
|
('relating_people', None),
|
||||||
('relating_person', None),
|
('relating_person', None),
|
||||||
|
('relating_proxyperson', None),
|
||||||
|
('relating_proxyperson_hidden+', None),
|
||||||
),
|
),
|
||||||
BasePerson: (
|
BasePerson: (
|
||||||
('+', None),
|
('+', None),
|
||||||
@ -413,6 +415,8 @@ TEST_RESULTS = {
|
|||||||
('relating_baseperson', BasePerson),
|
('relating_baseperson', BasePerson),
|
||||||
('relating_people', None),
|
('relating_people', None),
|
||||||
('relating_person', None),
|
('relating_person', None),
|
||||||
|
('relating_proxyperson', None),
|
||||||
|
('relating_proxyperson_hidden+', None),
|
||||||
),
|
),
|
||||||
BasePerson: (
|
BasePerson: (
|
||||||
('+', None),
|
('+', None),
|
||||||
@ -465,6 +469,7 @@ TEST_RESULTS = {
|
|||||||
('followers_concrete', None),
|
('followers_concrete', None),
|
||||||
('relating_person', None),
|
('relating_person', None),
|
||||||
('relating_people', None),
|
('relating_people', None),
|
||||||
|
('relating_proxyperson', None),
|
||||||
),
|
),
|
||||||
BasePerson: (
|
BasePerson: (
|
||||||
('followers_abstract', None),
|
('followers_abstract', None),
|
||||||
@ -494,6 +499,7 @@ TEST_RESULTS = {
|
|||||||
('followers_concrete', None),
|
('followers_concrete', None),
|
||||||
('relating_person', None),
|
('relating_person', None),
|
||||||
('relating_people', None),
|
('relating_people', None),
|
||||||
|
('relating_proxyperson', None),
|
||||||
),
|
),
|
||||||
BasePerson: (
|
BasePerson: (
|
||||||
('followers_abstract', None),
|
('followers_abstract', None),
|
||||||
|
@ -158,7 +158,7 @@ class ProxyTrackerUser(TrackerUser):
|
|||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Issue(models.Model):
|
class Issue(models.Model):
|
||||||
summary = models.CharField(max_length=255)
|
summary = models.CharField(max_length=255)
|
||||||
assignee = models.ForeignKey(ProxyTrackerUser, models.CASCADE)
|
assignee = models.ForeignKey(ProxyTrackerUser, models.CASCADE, related_name='issues')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return ':'.join((self.__class__.__name__, self.summary,))
|
return ':'.join((self.__class__.__name__, self.summary,))
|
||||||
|
@ -6,7 +6,7 @@ from django.apps import apps
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.models import User as AuthUser
|
from django.contrib.auth.models import User as AuthUser
|
||||||
from django.contrib.contenttypes.models import ContentType
|
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.core.urlresolvers import reverse
|
||||||
from django.db import DEFAULT_DB_ALIAS, models
|
from django.db import DEFAULT_DB_ALIAS, models
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
@ -332,14 +332,19 @@ class ProxyModelTests(TestCase):
|
|||||||
self.assertEqual(resp.name, 'New South Wales')
|
self.assertEqual(resp.name, 'New South Wales')
|
||||||
|
|
||||||
def test_filter_proxy_relation_reverse(self):
|
def test_filter_proxy_relation_reverse(self):
|
||||||
tu = TrackerUser.objects.create(
|
tu = TrackerUser.objects.create(name='Contributor', status='contrib')
|
||||||
name='Contributor', status='contrib')
|
ptu = ProxyTrackerUser.objects.get()
|
||||||
with self.assertRaises(exceptions.FieldError):
|
issue = Issue.objects.create(assignee=tu)
|
||||||
TrackerUser.objects.filter(issue=None),
|
self.assertEqual(tu.issues.get(), issue)
|
||||||
|
self.assertEqual(ptu.issues.get(), issue)
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
ProxyTrackerUser.objects.filter(issue=None),
|
TrackerUser.objects.filter(issues=issue),
|
||||||
[tu], lambda x: x
|
[tu], lambda x: x
|
||||||
)
|
)
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
ProxyTrackerUser.objects.filter(issues=issue),
|
||||||
|
[ptu], lambda x: x
|
||||||
|
)
|
||||||
|
|
||||||
def test_proxy_bug(self):
|
def test_proxy_bug(self):
|
||||||
contributor = ProxyTrackerUser.objects.create(name='Contributor',
|
contributor = ProxyTrackerUser.objects.create(name='Contributor',
|
||||||
|
Loading…
Reference in New Issue
Block a user