mirror of https://github.com/django/django.git
Fixed #17002 -- Allowed using a ManyToManyField through model that inherits another.
This commit is contained in:
parent
b5f0b3478d
commit
98359109eb
1
AUTHORS
1
AUTHORS
|
@ -263,6 +263,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Gabriel Grant <g@briel.ca>
|
Gabriel Grant <g@briel.ca>
|
||||||
Gabriel Hurley <gabriel@strikeawe.com>
|
Gabriel Hurley <gabriel@strikeawe.com>
|
||||||
gandalf@owca.info
|
gandalf@owca.info
|
||||||
|
Garry Lawrence
|
||||||
Garry Polley <garrympolley@gmail.com>
|
Garry Polley <garrympolley@gmail.com>
|
||||||
Garth Kidd <http://www.deadlybloodyserious.com/>
|
Garth Kidd <http://www.deadlybloodyserious.com/>
|
||||||
Gary Wilson <gary.wilson@gmail.com>
|
Gary Wilson <gary.wilson@gmail.com>
|
||||||
|
|
|
@ -1529,7 +1529,21 @@ class ManyToManyField(RelatedField):
|
||||||
else:
|
else:
|
||||||
join1infos = linkfield2.get_reverse_path_info()
|
join1infos = linkfield2.get_reverse_path_info()
|
||||||
join2infos = linkfield1.get_path_info()
|
join2infos = linkfield1.get_path_info()
|
||||||
|
|
||||||
|
# Get join infos between the last model of join 1 and the first model
|
||||||
|
# of join 2. Assume the only reason these may differ is due to model
|
||||||
|
# inheritance.
|
||||||
|
join1_final = join1infos[-1].to_opts
|
||||||
|
join2_initial = join2infos[0].from_opts
|
||||||
|
if join1_final is join2_initial:
|
||||||
|
intermediate_infos = []
|
||||||
|
elif issubclass(join1_final.model, join2_initial.model):
|
||||||
|
intermediate_infos = join1_final.get_path_to_parent(join2_initial.model)
|
||||||
|
else:
|
||||||
|
intermediate_infos = join2_initial.get_path_from_parent(join1_final.model)
|
||||||
|
|
||||||
pathinfos.extend(join1infos)
|
pathinfos.extend(join1infos)
|
||||||
|
pathinfos.extend(intermediate_infos)
|
||||||
pathinfos.extend(join2infos)
|
pathinfos.extend(join2infos)
|
||||||
return pathinfos
|
return pathinfos
|
||||||
|
|
||||||
|
|
|
@ -895,7 +895,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
||||||
# For non-autocreated 'through' models, can't assume we are
|
# For non-autocreated 'through' models, can't assume we are
|
||||||
# dealing with PK values.
|
# dealing with PK values.
|
||||||
fk = self.through._meta.get_field(self.source_field_name)
|
fk = self.through._meta.get_field(self.source_field_name)
|
||||||
join_table = self.through._meta.db_table
|
join_table = fk.model._meta.db_table
|
||||||
connection = connections[queryset.db]
|
connection = connections[queryset.db]
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
queryset = queryset.extra(select={
|
queryset = queryset.extra(select={
|
||||||
|
|
|
@ -14,6 +14,7 @@ from django.db.models import Manager
|
||||||
from django.db.models.fields import AutoField
|
from django.db.models.fields import AutoField
|
||||||
from django.db.models.fields.proxy import OrderWrt
|
from django.db.models.fields.proxy import OrderWrt
|
||||||
from django.db.models.fields.related import OneToOneField
|
from django.db.models.fields.related import OneToOneField
|
||||||
|
from django.db.models.query_utils import PathInfo
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.datastructures import ImmutableList, OrderedSet
|
from django.utils.datastructures import ImmutableList, OrderedSet
|
||||||
from django.utils.deprecation import (
|
from django.utils.deprecation import (
|
||||||
|
@ -670,6 +671,50 @@ class Options(object):
|
||||||
# links
|
# links
|
||||||
return self.parents[parent] or parent_link
|
return self.parents[parent] or parent_link
|
||||||
|
|
||||||
|
def get_path_to_parent(self, parent):
|
||||||
|
"""
|
||||||
|
Return a list of PathInfos containing the path from the current
|
||||||
|
model to the parent model, or an empty list if parent is not a
|
||||||
|
parent of the current model.
|
||||||
|
"""
|
||||||
|
if self.model is parent:
|
||||||
|
return []
|
||||||
|
# Skip the chain of proxy to the concrete proxied model.
|
||||||
|
proxied_model = self.concrete_model
|
||||||
|
path = []
|
||||||
|
opts = self
|
||||||
|
for int_model in self.get_base_chain(parent):
|
||||||
|
if int_model is proxied_model:
|
||||||
|
opts = int_model._meta
|
||||||
|
else:
|
||||||
|
final_field = opts.parents[int_model]
|
||||||
|
targets = (final_field.remote_field.get_related_field(),)
|
||||||
|
opts = int_model._meta
|
||||||
|
path.append(PathInfo(final_field.model._meta, opts, targets, final_field, False, True))
|
||||||
|
return path
|
||||||
|
|
||||||
|
def get_path_from_parent(self, parent):
|
||||||
|
"""
|
||||||
|
Return a list of PathInfos containing the path from the parent
|
||||||
|
model to the current model, or an empty list if parent is not a
|
||||||
|
parent of the current model.
|
||||||
|
"""
|
||||||
|
if self.model is parent:
|
||||||
|
return []
|
||||||
|
model = self.concrete_model
|
||||||
|
# Get a reversed base chain including both the current and parent
|
||||||
|
# models.
|
||||||
|
chain = model._meta.get_base_chain(parent)
|
||||||
|
chain.reverse()
|
||||||
|
chain.append(model)
|
||||||
|
# Construct a list of the PathInfos between models in chain.
|
||||||
|
path = []
|
||||||
|
for i, ancestor in enumerate(chain[:-1]):
|
||||||
|
child = chain[i + 1]
|
||||||
|
link = child._meta.get_ancestor_link(ancestor)
|
||||||
|
path.extend(link.get_reverse_path_info())
|
||||||
|
return path
|
||||||
|
|
||||||
def _populate_directed_relation_graph(self):
|
def _populate_directed_relation_graph(self):
|
||||||
"""
|
"""
|
||||||
This method is used by each model to find its reverse objects. As this
|
This method is used by each model to find its reverse objects. As this
|
||||||
|
|
|
@ -19,7 +19,7 @@ from django.db.models.constants import LOOKUP_SEP
|
||||||
from django.db.models.expressions import Col, Ref
|
from django.db.models.expressions import Col, Ref
|
||||||
from django.db.models.fields.related_lookups import MultiColSource
|
from django.db.models.fields.related_lookups import MultiColSource
|
||||||
from django.db.models.query_utils import (
|
from django.db.models.query_utils import (
|
||||||
PathInfo, Q, check_rel_lookup_compatibility, refs_expression,
|
Q, check_rel_lookup_compatibility, refs_expression,
|
||||||
)
|
)
|
||||||
from django.db.models.sql.constants import (
|
from django.db.models.sql.constants import (
|
||||||
INNER, LOUTER, ORDER_DIR, ORDER_PATTERN, QUERY_TERMS, SINGLE,
|
INNER, LOUTER, ORDER_DIR, ORDER_PATTERN, QUERY_TERMS, SINGLE,
|
||||||
|
@ -1342,21 +1342,11 @@ class Query(object):
|
||||||
# field lives in parent, but we are currently in one of its
|
# field lives in parent, but we are currently in one of its
|
||||||
# children)
|
# children)
|
||||||
if model is not opts.model:
|
if model is not opts.model:
|
||||||
# The field lives on a base class of the current model.
|
path_to_parent = opts.get_path_to_parent(model)
|
||||||
# Skip the chain of proxy to the concrete proxied model
|
if path_to_parent:
|
||||||
proxied_model = opts.concrete_model
|
path.extend(path_to_parent)
|
||||||
|
cur_names_with_path[1].extend(path_to_parent)
|
||||||
for int_model in opts.get_base_chain(model):
|
opts = path_to_parent[-1].to_opts
|
||||||
if int_model is proxied_model:
|
|
||||||
opts = int_model._meta
|
|
||||||
else:
|
|
||||||
final_field = opts.parents[int_model]
|
|
||||||
targets = (final_field.remote_field.get_related_field(),)
|
|
||||||
opts = int_model._meta
|
|
||||||
path.append(PathInfo(final_field.model._meta, opts, targets, final_field, False, True))
|
|
||||||
cur_names_with_path[1].append(
|
|
||||||
PathInfo(final_field.model._meta, opts, targets, final_field, False, True)
|
|
||||||
)
|
|
||||||
if hasattr(field, 'get_path_info'):
|
if hasattr(field, 'get_path_info'):
|
||||||
pathinfos = field.get_path_info()
|
pathinfos = field.get_path_info()
|
||||||
if not allow_many:
|
if not allow_many:
|
||||||
|
|
|
@ -94,3 +94,32 @@ class CarDriver(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "pk=%s car=%s driver=%s" % (str(self.pk), self.car, self.driver)
|
return "pk=%s car=%s driver=%s" % (str(self.pk), self.car, self.driver)
|
||||||
|
|
||||||
|
|
||||||
|
# Through models using multi-table inheritance
|
||||||
|
class Event(models.Model):
|
||||||
|
name = models.CharField(max_length=50, unique=True)
|
||||||
|
people = models.ManyToManyField('Person', through='IndividualCompetitor')
|
||||||
|
special_people = models.ManyToManyField(
|
||||||
|
'Person',
|
||||||
|
through='ProxiedIndividualCompetitor',
|
||||||
|
related_name='special_event_set',
|
||||||
|
)
|
||||||
|
teams = models.ManyToManyField('Group', through='CompetingTeam')
|
||||||
|
|
||||||
|
|
||||||
|
class Competitor(models.Model):
|
||||||
|
event = models.ForeignKey(Event, models.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
|
class IndividualCompetitor(Competitor):
|
||||||
|
person = models.ForeignKey(Person, models.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
|
class CompetingTeam(Competitor):
|
||||||
|
team = models.ForeignKey(Group, models.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
|
class ProxiedIndividualCompetitor(IndividualCompetitor):
|
||||||
|
class Meta:
|
||||||
|
proxy = True
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
CompetingTeam, Event, Group, IndividualCompetitor, Membership, Person,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiTableTests(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.alice = Person.objects.create(name='Alice')
|
||||||
|
cls.bob = Person.objects.create(name='Bob')
|
||||||
|
cls.chris = Person.objects.create(name='Chris')
|
||||||
|
cls.dan = Person.objects.create(name='Dan')
|
||||||
|
cls.team_alpha = Group.objects.create(name='Alpha')
|
||||||
|
Membership.objects.create(person=cls.alice, group=cls.team_alpha)
|
||||||
|
Membership.objects.create(person=cls.bob, group=cls.team_alpha)
|
||||||
|
cls.event = Event.objects.create(name='Exposition Match')
|
||||||
|
IndividualCompetitor.objects.create(event=cls.event, person=cls.chris)
|
||||||
|
IndividualCompetitor.objects.create(event=cls.event, person=cls.dan)
|
||||||
|
CompetingTeam.objects.create(event=cls.event, team=cls.team_alpha)
|
||||||
|
|
||||||
|
def test_m2m_query(self):
|
||||||
|
result = self.event.teams.all()
|
||||||
|
self.assertCountEqual(result, [self.team_alpha])
|
||||||
|
|
||||||
|
def test_m2m_reverse_query(self):
|
||||||
|
result = self.chris.event_set.all()
|
||||||
|
self.assertCountEqual(result, [self.event])
|
||||||
|
|
||||||
|
def test_m2m_query_proxied(self):
|
||||||
|
result = self.event.special_people.all()
|
||||||
|
self.assertCountEqual(result, [self.chris, self.dan])
|
||||||
|
|
||||||
|
def test_m2m_reverse_query_proxied(self):
|
||||||
|
result = self.chris.special_event_set.all()
|
||||||
|
self.assertCountEqual(result, [self.event])
|
||||||
|
|
||||||
|
def test_m2m_prefetch_proxied(self):
|
||||||
|
result = Event.objects.filter(name='Exposition Match').prefetch_related('special_people')
|
||||||
|
with self.assertNumQueries(2):
|
||||||
|
self.assertCountEqual(result, [self.event])
|
||||||
|
self.assertEqual(sorted([p.name for p in result[0].special_people.all()]), ['Chris', 'Dan'])
|
||||||
|
|
||||||
|
def test_m2m_prefetch_reverse_proxied(self):
|
||||||
|
result = Person.objects.filter(name='Dan').prefetch_related('special_event_set')
|
||||||
|
with self.assertNumQueries(2):
|
||||||
|
self.assertCountEqual(result, [self.dan])
|
||||||
|
self.assertEqual([event.name for event in result[0].special_event_set.all()], ['Exposition Match'])
|
Loading…
Reference in New Issue