Loading django/db/models/fields/related.py +11 −4 Original line number Diff line number Diff line Loading @@ -1348,11 +1348,18 @@ class ManyToManyRel(object): def get_related_field(self): """ Returns the field in the to' object to which this relationship is tied (this is always the primary key on the target model). Provided for symmetry with ManyToOneRel. Returns the field in the 'to' object to which this relationship is tied. Provided for symmetry with ManyToOneRel. """ return self.to._meta.pk opts = self.through._meta if self.through_fields: field = opts.get_field(self.through_fields[0]) else: for field in opts.fields: rel = getattr(field, 'rel', None) if rel and rel.to == self.to: break return field.foreign_related_fields[0] class ForeignObject(RelatedField): Loading tests/m2m_through/models.py +22 −0 Original line number Diff line number Diff line Loading @@ -113,3 +113,25 @@ class Relationship(models.Model): another = models.ForeignKey(Employee, related_name="rel_another_set", null=True) target = models.ForeignKey(Employee, related_name="rel_target_set") source = models.ForeignKey(Employee, related_name="rel_source_set") class Ingredient(models.Model): iname = models.CharField(max_length=20, unique=True) class Meta: ordering = ('iname',) class Recipe(models.Model): rname = models.CharField(max_length=20, unique=True) ingredients = models.ManyToManyField( Ingredient, through='RecipeIngredient', related_name='recipes', ) class Meta: ordering = ('rname',) class RecipeIngredient(models.Model): ingredient = models.ForeignKey(Ingredient, to_field='iname') recipe = models.ForeignKey(Recipe, to_field='rname') tests/m2m_through/tests.py +29 −1 Original line number Diff line number Diff line Loading @@ -6,7 +6,8 @@ from operator import attrgetter from django.test import TestCase from .models import (Person, Group, Membership, CustomMembership, PersonSelfRefM2M, Friendship, Event, Invitation, Employee, Relationship) PersonSelfRefM2M, Friendship, Event, Invitation, Employee, Relationship, Ingredient, Recipe, RecipeIngredient) class M2mThroughTests(TestCase): Loading Loading @@ -426,3 +427,30 @@ class M2mThroughReferentialTests(TestCase): ['peter', 'mary', 'harry'], attrgetter('name') ) class M2mThroughToFieldsTests(TestCase): def setUp(self): self.pea = Ingredient.objects.create(iname='pea') self.potato = Ingredient.objects.create(iname='potato') self.tomato = Ingredient.objects.create(iname='tomato') self.curry = Recipe.objects.create(rname='curry') RecipeIngredient.objects.create(recipe=self.curry, ingredient=self.potato) RecipeIngredient.objects.create(recipe=self.curry, ingredient=self.pea) RecipeIngredient.objects.create(recipe=self.curry, ingredient=self.tomato) def test_retrieval(self): # Forward retrieval self.assertQuerysetEqual( self.curry.ingredients.all(), [self.pea, self.potato, self.tomato], lambda x: x ) # Backward retrieval self.assertEqual(self.tomato.recipes.get(), self.curry) def test_choices(self): field = Recipe._meta.get_field('ingredients') self.assertEqual( [choice[0] for choice in field.get_choices(include_blank=False)], ['pea', 'potato', 'tomato'] ) Loading
django/db/models/fields/related.py +11 −4 Original line number Diff line number Diff line Loading @@ -1348,11 +1348,18 @@ class ManyToManyRel(object): def get_related_field(self): """ Returns the field in the to' object to which this relationship is tied (this is always the primary key on the target model). Provided for symmetry with ManyToOneRel. Returns the field in the 'to' object to which this relationship is tied. Provided for symmetry with ManyToOneRel. """ return self.to._meta.pk opts = self.through._meta if self.through_fields: field = opts.get_field(self.through_fields[0]) else: for field in opts.fields: rel = getattr(field, 'rel', None) if rel and rel.to == self.to: break return field.foreign_related_fields[0] class ForeignObject(RelatedField): Loading
tests/m2m_through/models.py +22 −0 Original line number Diff line number Diff line Loading @@ -113,3 +113,25 @@ class Relationship(models.Model): another = models.ForeignKey(Employee, related_name="rel_another_set", null=True) target = models.ForeignKey(Employee, related_name="rel_target_set") source = models.ForeignKey(Employee, related_name="rel_source_set") class Ingredient(models.Model): iname = models.CharField(max_length=20, unique=True) class Meta: ordering = ('iname',) class Recipe(models.Model): rname = models.CharField(max_length=20, unique=True) ingredients = models.ManyToManyField( Ingredient, through='RecipeIngredient', related_name='recipes', ) class Meta: ordering = ('rname',) class RecipeIngredient(models.Model): ingredient = models.ForeignKey(Ingredient, to_field='iname') recipe = models.ForeignKey(Recipe, to_field='rname')
tests/m2m_through/tests.py +29 −1 Original line number Diff line number Diff line Loading @@ -6,7 +6,8 @@ from operator import attrgetter from django.test import TestCase from .models import (Person, Group, Membership, CustomMembership, PersonSelfRefM2M, Friendship, Event, Invitation, Employee, Relationship) PersonSelfRefM2M, Friendship, Event, Invitation, Employee, Relationship, Ingredient, Recipe, RecipeIngredient) class M2mThroughTests(TestCase): Loading Loading @@ -426,3 +427,30 @@ class M2mThroughReferentialTests(TestCase): ['peter', 'mary', 'harry'], attrgetter('name') ) class M2mThroughToFieldsTests(TestCase): def setUp(self): self.pea = Ingredient.objects.create(iname='pea') self.potato = Ingredient.objects.create(iname='potato') self.tomato = Ingredient.objects.create(iname='tomato') self.curry = Recipe.objects.create(rname='curry') RecipeIngredient.objects.create(recipe=self.curry, ingredient=self.potato) RecipeIngredient.objects.create(recipe=self.curry, ingredient=self.pea) RecipeIngredient.objects.create(recipe=self.curry, ingredient=self.tomato) def test_retrieval(self): # Forward retrieval self.assertQuerysetEqual( self.curry.ingredients.all(), [self.pea, self.potato, self.tomato], lambda x: x ) # Backward retrieval self.assertEqual(self.tomato.recipes.get(), self.curry) def test_choices(self): field = Recipe._meta.get_field('ingredients') self.assertEqual( [choice[0] for choice in field.get_choices(include_blank=False)], ['pea', 'potato', 'tomato'] )