summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2015-10-01 14:57:58 -0400
committerSimon Charette <charette.s@gmail.com>2015-10-12 12:14:26 -0400
commit6c9f37ea9eafb0ca1b02eb71ae8d375672043824 (patch)
treeac1a374541c6a345fe73a0d5a86ff2881b7bc33a
parentc8f091f5bcce317629c4af86c88a2864ab7d380b (diff)
downloaddjango-6c9f37ea9eafb0ca1b02eb71ae8d375672043824.tar.gz
Fixed #18012 -- Propagated reverse foreign keys from proxy to concrete models.
Thanks to Anssi for the review.
-rw-r--r--django/db/models/fields/related.py2
-rw-r--r--django/db/models/fields/related_descriptors.py2
-rw-r--r--django/db/models/options.py13
-rw-r--r--docs/releases/1.10.txt6
-rw-r--r--tests/model_meta/models.py2
-rw-r--r--tests/model_meta/results.py6
-rw-r--r--tests/proxy_models/models.py2
-rw-r--r--tests/proxy_models/tests.py17
8 files changed, 36 insertions, 14 deletions
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',