Fixed #14549 - Removed restriction of single FKs on intermediary tables
Thanks to Loic Bistuer for review. Minor changes to error messages done by committer.
This commit is contained in:
parent
95c74b9d69
commit
c627da0ccc
|
@ -1250,9 +1250,12 @@ class OneToOneRel(ManyToOneRel):
|
|||
|
||||
class ManyToManyRel(object):
|
||||
def __init__(self, to, related_name=None, limit_choices_to=None,
|
||||
symmetrical=True, through=None, db_constraint=True, related_query_name=None):
|
||||
symmetrical=True, through=None, through_fields=None,
|
||||
db_constraint=True, related_query_name=None):
|
||||
if through and not db_constraint:
|
||||
raise ValueError("Can't supply a through model and db_constraint=False")
|
||||
if through_fields and not through:
|
||||
raise ValueError("Cannot specify through_fields without a through model")
|
||||
self.to = to
|
||||
self.related_name = related_name
|
||||
self.related_query_name = related_query_name
|
||||
|
@ -1262,6 +1265,7 @@ class ManyToManyRel(object):
|
|||
self.symmetrical = symmetrical
|
||||
self.multiple = True
|
||||
self.through = through
|
||||
self.through_fields = through_fields
|
||||
self.db_constraint = db_constraint
|
||||
|
||||
def is_hidden(self):
|
||||
|
@ -1849,6 +1853,7 @@ class ManyToManyField(RelatedField):
|
|||
limit_choices_to=kwargs.pop('limit_choices_to', None),
|
||||
symmetrical=kwargs.pop('symmetrical', to == RECURSIVE_RELATIONSHIP_CONSTANT),
|
||||
through=kwargs.pop('through', None),
|
||||
through_fields=kwargs.pop('through_fields', None),
|
||||
db_constraint=db_constraint,
|
||||
)
|
||||
|
||||
|
@ -1878,6 +1883,12 @@ class ManyToManyField(RelatedField):
|
|||
return []
|
||||
|
||||
def _check_relationship_model(self, from_model=None, **kwargs):
|
||||
if hasattr(self.rel.through, '_meta'):
|
||||
qualified_model_name = "%s.%s" % (
|
||||
self.rel.through._meta.app_label, self.rel.through.__name__)
|
||||
else:
|
||||
qualified_model_name = self.rel.through
|
||||
|
||||
errors = []
|
||||
|
||||
if self.rel.through not in apps.get_models(include_auto_created=True):
|
||||
|
@ -1885,13 +1896,28 @@ class ManyToManyField(RelatedField):
|
|||
errors.append(
|
||||
checks.Error(
|
||||
("Field specifies a many-to-many relation through model "
|
||||
"'%s', which has not been installed.") % self.rel.through,
|
||||
"'%s', which has not been installed.") %
|
||||
qualified_model_name,
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='fields.E331',
|
||||
)
|
||||
)
|
||||
|
||||
elif self.rel.through_fields is not None:
|
||||
if not len(self.rel.through_fields) >= 2 or not (self.rel.through_fields[0] and self.rel.through_fields[1]):
|
||||
errors.append(
|
||||
checks.Error(
|
||||
("The field is given an iterable for through_fields, "
|
||||
"which does not provide the names for both link fields "
|
||||
"that Django should use for the relation through model "
|
||||
"'%s'.") % qualified_model_name,
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='fields.E337',
|
||||
)
|
||||
)
|
||||
|
||||
elif not isinstance(self.rel.through, six.string_types):
|
||||
|
||||
assert from_model is not None, \
|
||||
|
@ -1926,13 +1952,16 @@ class ManyToManyField(RelatedField):
|
|||
seen_self = sum(from_model == getattr(field.rel, 'to', None)
|
||||
for field in self.rel.through._meta.fields)
|
||||
|
||||
if seen_self > 2:
|
||||
if seen_self > 2 and not self.rel.through_fields:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
("The model is used as an intermediate model by "
|
||||
"'%s', but it has more than two foreign keys "
|
||||
"to '%s', which is ambiguous.") % (self, from_model_name),
|
||||
hint=None,
|
||||
"to '%s', which is ambiguous. You must specify "
|
||||
"which two foreign keys Django should use via the "
|
||||
"through_fields keyword argument.") % (self, from_model_name),
|
||||
hint=("Use through_fields to specify which two "
|
||||
"foreign keys Django should use."),
|
||||
obj=self.rel.through,
|
||||
id='fields.E333',
|
||||
)
|
||||
|
@ -1945,12 +1974,14 @@ class ManyToManyField(RelatedField):
|
|||
seen_to = sum(to_model == getattr(field.rel, 'to', None)
|
||||
for field in self.rel.through._meta.fields)
|
||||
|
||||
if seen_from > 1:
|
||||
if seen_from > 1 and not self.rel.through_fields:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
("The model is used as an intermediate model by "
|
||||
"'%s', but it has more than one foreign key "
|
||||
"from '%s', which is ambiguous.") % (self, from_model_name),
|
||||
"from '%s', which is ambiguous. You must specify "
|
||||
"which foreign key Django should use via the "
|
||||
"through_fields keyword argument.") % (self, from_model_name),
|
||||
hint=('If you want to create a recursive relationship, '
|
||||
'use ForeignKey("self", symmetrical=False, '
|
||||
'through="%s").') % relationship_model_name,
|
||||
|
@ -1959,12 +1990,14 @@ class ManyToManyField(RelatedField):
|
|||
)
|
||||
)
|
||||
|
||||
if seen_to > 1:
|
||||
if seen_to > 1 and not self.rel.through_fields:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
("The model is used as an intermediate model by "
|
||||
"'%s', but it has more than one foreign key "
|
||||
"to '%s', which is ambiguous.") % (self, to_model_name),
|
||||
"to '%s', which is ambiguous. You must specify "
|
||||
"which foreign key Django should use via the "
|
||||
"through_fields keyword argument.") % (self, to_model_name),
|
||||
hint=('If you want to create a recursive '
|
||||
'relationship, use ForeignKey("self", '
|
||||
'symmetrical=False, through="%s").') % relationship_model_name,
|
||||
|
@ -2057,10 +2090,18 @@ class ManyToManyField(RelatedField):
|
|||
cache_attr = '_m2m_%s_cache' % attr
|
||||
if hasattr(self, cache_attr):
|
||||
return getattr(self, cache_attr)
|
||||
if self.rel.through_fields is not None:
|
||||
link_field_name = self.rel.through_fields[0]
|
||||
else:
|
||||
link_field_name = None
|
||||
for f in self.rel.through._meta.fields:
|
||||
if hasattr(f, 'rel') and f.rel and f.rel.to == related.model:
|
||||
if hasattr(f, 'rel') and f.rel and f.rel.to == related.model and \
|
||||
(link_field_name is None or link_field_name == f.name):
|
||||
setattr(self, cache_attr, getattr(f, attr))
|
||||
return getattr(self, cache_attr)
|
||||
# We only reach here if we're given an invalid field name via the
|
||||
# `through_fields` argument
|
||||
raise FieldDoesNotExist(link_field_name)
|
||||
|
||||
def _get_m2m_reverse_attr(self, related, attr):
|
||||
"Function that can be curried to provide the related accessor or DB column name for the m2m table"
|
||||
|
@ -2068,9 +2109,13 @@ class ManyToManyField(RelatedField):
|
|||
if hasattr(self, cache_attr):
|
||||
return getattr(self, cache_attr)
|
||||
found = False
|
||||
if self.rel.through_fields is not None:
|
||||
link_field_name = self.rel.through_fields[1]
|
||||
else:
|
||||
link_field_name = None
|
||||
for f in self.rel.through._meta.fields:
|
||||
if hasattr(f, 'rel') and f.rel and f.rel.to == related.parent_model:
|
||||
if related.model == related.parent_model:
|
||||
if link_field_name is None and related.model == related.parent_model:
|
||||
# If this is an m2m-intermediate to self,
|
||||
# the first foreign key you find will be
|
||||
# the source column. Keep searching for
|
||||
|
@ -2080,10 +2125,15 @@ class ManyToManyField(RelatedField):
|
|||
break
|
||||
else:
|
||||
found = True
|
||||
else:
|
||||
elif link_field_name is None or link_field_name == f.name:
|
||||
setattr(self, cache_attr, getattr(f, attr))
|
||||
break
|
||||
return getattr(self, cache_attr)
|
||||
try:
|
||||
return getattr(self, cache_attr)
|
||||
except AttributeError:
|
||||
# We only reach here if we're given an invalid reverse field
|
||||
# name via the `through_fields` argument
|
||||
raise FieldDoesNotExist(link_field_name)
|
||||
|
||||
def value_to_string(self, obj):
|
||||
data = ''
|
||||
|
|
|
@ -90,10 +90,11 @@ Related Fields
|
|||
* **fields.E330**: ManyToManyFields cannot be unique.
|
||||
* **fields.E331**: Field specifies a many-to-many relation through model ``%s``, which has not been installed.
|
||||
* **fields.E332**: Many-to-many fields with intermediate tables must not be symmetrical.
|
||||
* **fields.E333**: The model is used as an intermediate model by ``<model>``, but it has more than two foreign keys to ``<model>``, which is ambiguous.
|
||||
* **fields.E334**: The model is used as an intermediate model by ``<model>``, but it has more than one foreign key from ``<model>``, which is ambiguous.
|
||||
* **fields.E335**: The model is used as an intermediate model by ``<model>``, but it has more than one foreign key to ``<model>``, which is ambiguous.
|
||||
* **fields.E336**: The model is used as an intermediary model by ``<model>``, but it does not have foreign key to ``<model>`` or ``<model>``."
|
||||
* **fields.E333**: The model is used as an intermediate model by ``<model>``, but it has more than two foreign keys to ``<model>``, which is ambiguous. You must specify which two foreign keys Django should use via the through_fields keyword argument.
|
||||
* **fields.E334**: The model is used as an intermediate model by ``<model>``, but it has more than one foreign key from ``<model>``, which is ambiguous. You must specify which foreign key Django should use via the through_fields keyword argument.
|
||||
* **fields.E335**: The model is used as an intermediate model by ``<model>``, but it has more than one foreign key to ``<model>``, which is ambiguous. You must specify which foreign key Django should use via the through_fields keyword argument.
|
||||
* **fields.E336**: The model is used as an intermediary model by ``<model>``, but it does not have foreign key to ``<model>`` or ``<model>``.
|
||||
* **fields.E337**: The field is given an iterable for through_fields, which does not provide the names for both link fields that Django should use for the relation through <model>.
|
||||
|
||||
Signals
|
||||
~~~~~~~
|
||||
|
|
|
@ -1337,6 +1337,55 @@ that control how the relationship functions.
|
|||
:ref:`extra data with a many-to-many relationship
|
||||
<intermediary-manytomany>`.
|
||||
|
||||
.. attribute:: ManyToManyField.through_fields
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Only used when a custom intermediary model is specified. Django will
|
||||
normally determine which fields of the intermediary model to use in order
|
||||
to establish a many-to-many relationship automatically. However,
|
||||
consider the following models::
|
||||
|
||||
from django.db import models
|
||||
|
||||
class Person(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
class Group(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
members = models.ManyToManyField(Person, through='Membership', through_fields=('person', 'group'))
|
||||
|
||||
class Membership(models.Model):
|
||||
person = models.ForeignKey(Person)
|
||||
group = models.ForeignKey(Group)
|
||||
inviter = models.ForeignKey(Person, related_name="membership_invites")
|
||||
invite_reason = models.CharField(max_length=64)
|
||||
|
||||
``Membership`` has *two* foreign keys to ``Person`` (``person`` and
|
||||
``inviter``), which makes the relationship ambiguous and Django can't know
|
||||
which one to use. In this case, you must explicitly specify which
|
||||
foreign keys Django should use using ``through_fields``, as in the example
|
||||
above.
|
||||
|
||||
``through_fields`` accepts a 2-tuple ``('field1', 'field2')``, where
|
||||
``field1`` is the name of the foreign key to the target model (``person``
|
||||
in this case), and ``field2`` the name of the foreign key to the model the
|
||||
:class:`ManyToManyField` is defined on (``group`` in this case).
|
||||
|
||||
When you have more than one foreign key on an intermediary model to any
|
||||
(or even both) of the models participating in a many-to-many relationship,
|
||||
you *must* specify ``through_fields``. This also applies to
|
||||
:ref:`recursive relationships <recursive-relationships>`
|
||||
when an intermediary model is used and there are more than two
|
||||
foreign keys to the model, or you want to explicitly specify which two
|
||||
Django should use.
|
||||
|
||||
Recursive relationships using an intermediary model are always defined as
|
||||
non-symmetrical -- that is, with :attr:`symmetrical=False <ManyToManyField.symmetrical>`
|
||||
-- therefore, there is the concept of a "source" and a "target". In that
|
||||
case ``'field1'`` will be treated as the "source" of the relationship and
|
||||
``'field2'`` as the "target".
|
||||
|
||||
.. attribute:: ManyToManyField.db_table
|
||||
|
||||
The name of the table to create for storing the many-to-many data. If this
|
||||
|
|
|
@ -658,6 +658,12 @@ Models
|
|||
* You can use a single list for :attr:`~django.db.models.Options.index_together`
|
||||
(rather than a list of lists) when specifying a single set of fields.
|
||||
|
||||
* Custom intermediate models having more than one foreign key to any of the
|
||||
models participating in a many-to-many relationship are now permitted,
|
||||
provided you explicitly specify which foreign keys should be used by setting
|
||||
the new :attr:`ManyToManyField.through_fields <django.db.models.ManyToManyField.through_fields>`
|
||||
argument.
|
||||
|
||||
Signals
|
||||
^^^^^^^
|
||||
|
||||
|
|
|
@ -434,30 +434,38 @@ something like this::
|
|||
invite_reason = models.CharField(max_length=64)
|
||||
|
||||
When you set up the intermediary model, you explicitly specify foreign
|
||||
keys to the models that are involved in the ManyToMany relation. This
|
||||
keys to the models that are involved in the many-to-many relationship. This
|
||||
explicit declaration defines how the two models are related.
|
||||
|
||||
There are a few restrictions on the intermediate model:
|
||||
|
||||
* Your intermediate model must contain one - and *only* one - foreign key
|
||||
to the target model (this would be ``Person`` in our example). If you
|
||||
have more than one foreign key, a validation error will be raised.
|
||||
to the target model (this would be ``Person`` in our example), or you must
|
||||
explicitly specify the foreign keys Django should use for the relationship
|
||||
using :attr:`ManyToManyField.through_fields <ManyToManyField.through_fields>`.
|
||||
If you have more than one foreign key and ``through_fields`` is not
|
||||
specified, a validation error will be raised. A similar restriction applies
|
||||
to the foreign key to the source model (this would be ``Group`` in our
|
||||
example).
|
||||
|
||||
* Your intermediate model must contain one - and *only* one - foreign key
|
||||
to the source model (this would be ``Group`` in our example). If you
|
||||
have more than one foreign key, a validation error will be raised.
|
||||
|
||||
* The only exception to this is a model which has a many-to-many
|
||||
relationship to itself, through an intermediary model. In this
|
||||
case, two foreign keys to the same model are permitted, but they
|
||||
will be treated as the two (different) sides of the many-to-many
|
||||
relation.
|
||||
* For a model which has a many-to-many relationship to itself through an
|
||||
intermediary model, two foreign keys to the same model are permitted, but
|
||||
they will be treated as the two (different) sides of the many-to-many
|
||||
relationship. If there are *more* than two foreign keys though, you
|
||||
must also specify ``through_fields`` as above, or a validation error
|
||||
will be raised.
|
||||
|
||||
* When defining a many-to-many relationship from a model to
|
||||
itself, using an intermediary model, you *must* use
|
||||
:attr:`symmetrical=False <ManyToManyField.symmetrical>` (see
|
||||
:ref:`the model field reference <manytomany-arguments>`).
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
In Django 1.6 and earlier, intermediate models containing more than one
|
||||
foreign key to any of the models involved in the many-to-many relationship
|
||||
used to be prohibited.
|
||||
|
||||
Now that you have set up your :class:`~django.db.models.ManyToManyField` to use
|
||||
your intermediary model (``Membership``, in this case), you're ready to start
|
||||
creating some many-to-many relationships. You do this by creating instances of
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.core.checks import Error
|
||||
from django.db import models
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.test.utils import override_settings
|
||||
from django.test.testcases import skipIfDBFeature
|
||||
|
||||
|
@ -81,7 +82,9 @@ class RelativeFieldTests(IsolatedModelsTestCase):
|
|||
Error(
|
||||
("The model is used as an intermediate model by "
|
||||
"'invalid_models_tests.Group.field', but it has more than one "
|
||||
"foreign key to 'Person', which is ambiguous."),
|
||||
"foreign key to 'Person', which is ambiguous. You must specify "
|
||||
"which foreign key Django should use via the through_fields "
|
||||
"keyword argument."),
|
||||
hint=('If you want to create a recursive relationship, use '
|
||||
'ForeignKey("self", symmetrical=False, '
|
||||
'through="AmbiguousRelationship").'),
|
||||
|
@ -205,8 +208,10 @@ class RelativeFieldTests(IsolatedModelsTestCase):
|
|||
Error(
|
||||
("The model is used as an intermediate model by "
|
||||
"'invalid_models_tests.Person.friends', but it has more than two "
|
||||
"foreign keys to 'Person', which is ambiguous."),
|
||||
hint=None,
|
||||
"foreign keys to 'Person', which is ambiguous. You must specify "
|
||||
"which two foreign keys Django should use via the through_fields "
|
||||
"keyword argument."),
|
||||
hint='Use through_fields to specify which two foreign keys Django should use.',
|
||||
obj=InvalidRelationship,
|
||||
id='fields.E333',
|
||||
),
|
||||
|
@ -1051,3 +1056,84 @@ class ComplexClashTests(IsolatedModelsTestCase):
|
|||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
|
||||
class M2mThroughFieldsTests(IsolatedModelsTestCase):
|
||||
def test_m2m_field_argument_validation(self):
|
||||
"""
|
||||
Tests that ManyToManyField accepts the ``through_fields`` kwarg
|
||||
only if an intermediary table is specified.
|
||||
"""
|
||||
class Fan(models.Model):
|
||||
pass
|
||||
|
||||
self.assertRaisesMessage(
|
||||
ValueError, 'Cannot specify through_fields without a through model',
|
||||
models.ManyToManyField, Fan, through_fields=('f1', 'f2'))
|
||||
|
||||
def test_invalid_m2m_field(self):
|
||||
"""
|
||||
Tests that providing invalid source field name to ManyToManyField.through_fields
|
||||
raises FieldDoesNotExist.
|
||||
"""
|
||||
class Fan(models.Model):
|
||||
pass
|
||||
|
||||
class Event(models.Model):
|
||||
invitees = models.ManyToManyField(Fan, through='Invitation', through_fields=('invalid_field', 'invitee'))
|
||||
|
||||
class Invitation(models.Model):
|
||||
event = models.ForeignKey(Event)
|
||||
invitee = models.ForeignKey(Fan)
|
||||
inviter = models.ForeignKey(Fan, related_name='+')
|
||||
|
||||
with self.assertRaisesMessage(FieldDoesNotExist, 'invalid_field'):
|
||||
Event().invitees.all()
|
||||
|
||||
def test_invalid_m2m_reverse_field(self):
|
||||
"""
|
||||
Tests that providing invalid reverse field name to ManyToManyField.through_fields
|
||||
raises FieldDoesNotExist.
|
||||
"""
|
||||
class Fan(models.Model):
|
||||
pass
|
||||
|
||||
class Event(models.Model):
|
||||
invitees = models.ManyToManyField(Fan, through='Invitation', through_fields=('event', 'invalid_field'))
|
||||
|
||||
class Invitation(models.Model):
|
||||
event = models.ForeignKey(Event)
|
||||
invitee = models.ForeignKey(Fan)
|
||||
inviter = models.ForeignKey(Fan, related_name='+')
|
||||
|
||||
with self.assertRaisesMessage(FieldDoesNotExist, 'invalid_field'):
|
||||
Event().invitees.all()
|
||||
|
||||
def test_explicit_field_names(self):
|
||||
"""
|
||||
Tests that if ``through_fields`` kwarg is given, it must specify both
|
||||
link fields of the intermediary table.
|
||||
"""
|
||||
class Fan(models.Model):
|
||||
pass
|
||||
|
||||
class Event(models.Model):
|
||||
invitees = models.ManyToManyField(Fan, through='Invitation', through_fields=(None, 'invitee'))
|
||||
|
||||
class Invitation(models.Model):
|
||||
event = models.ForeignKey(Event)
|
||||
invitee = models.ForeignKey(Fan)
|
||||
inviter = models.ForeignKey(Fan, related_name='+')
|
||||
|
||||
field = Event._meta.get_field('invitees')
|
||||
errors = field.check(from_model=Event)
|
||||
expected = [
|
||||
Error(
|
||||
("The field is given an iterable for through_fields, "
|
||||
"which does not provide the names for both link fields "
|
||||
"that Django should use for the relation through model "
|
||||
"'invalid_models_tests.Invitation'."),
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='fields.E337')]
|
||||
self.assertEqual(expected, errors)
|
||||
|
|
|
@ -77,3 +77,39 @@ class Friendship(models.Model):
|
|||
first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
|
||||
second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
|
||||
date_friended = models.DateTimeField()
|
||||
|
||||
|
||||
# Custom through link fields
|
||||
@python_2_unicode_compatible
|
||||
class Event(models.Model):
|
||||
title = models.CharField(max_length=50)
|
||||
invitees = models.ManyToManyField(Person, through='Invitation', through_fields=('event', 'invitee'), related_name='events_invited')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Invitation(models.Model):
|
||||
event = models.ForeignKey(Event, related_name='invitations')
|
||||
# field order is deliberately inverted. the target field is "invitee".
|
||||
inviter = models.ForeignKey(Person, related_name='invitations_sent')
|
||||
invitee = models.ForeignKey(Person, related_name='invitations')
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Employee(models.Model):
|
||||
name = models.CharField(max_length=5)
|
||||
subordinates = models.ManyToManyField('self', through="Relationship", through_fields=('source', 'target'), symmetrical=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Relationship(models.Model):
|
||||
# field order is deliberately inverted.
|
||||
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")
|
||||
|
|
|
@ -6,7 +6,7 @@ from operator import attrgetter
|
|||
from django.test import TestCase
|
||||
|
||||
from .models import (Person, Group, Membership, CustomMembership,
|
||||
PersonSelfRefM2M, Friendship)
|
||||
PersonSelfRefM2M, Friendship, Event, Invitation, Employee, Relationship)
|
||||
|
||||
|
||||
class M2mThroughTests(TestCase):
|
||||
|
@ -276,6 +276,33 @@ class M2mThroughTests(TestCase):
|
|||
attrgetter("name")
|
||||
)
|
||||
|
||||
def test_through_fields(self):
|
||||
"""
|
||||
Tests that relations with intermediary tables with multiple FKs
|
||||
to the M2M's ``to`` model are possible.
|
||||
"""
|
||||
event = Event.objects.create(title='Rockwhale 2014')
|
||||
Invitation.objects.create(event=event, inviter=self.bob, invitee=self.jim)
|
||||
Invitation.objects.create(event=event, inviter=self.bob, invitee=self.jane)
|
||||
self.assertQuerysetEqual(event.invitees.all(), [
|
||||
'Jane',
|
||||
'Jim',
|
||||
], attrgetter('name'))
|
||||
|
||||
def test_through_fields_self_referential(self):
|
||||
john = Employee.objects.create(name='john')
|
||||
peter = Employee.objects.create(name='peter')
|
||||
mary = Employee.objects.create(name='mary')
|
||||
harry = Employee.objects.create(name='harry')
|
||||
Relationship.objects.create(source=john, target=peter, another=None)
|
||||
Relationship.objects.create(source=john, target=mary, another=None)
|
||||
Relationship.objects.create(source=john, target=harry, another=peter)
|
||||
self.assertQuerysetEqual(john.subordinates.all(), [
|
||||
'peter',
|
||||
'mary',
|
||||
'harry',
|
||||
], attrgetter('name'))
|
||||
|
||||
def test_query_tests(self):
|
||||
Membership.objects.create(person=self.jim, group=self.rock)
|
||||
m2 = Membership.objects.create(person=self.jane, group=self.rock)
|
||||
|
|
Loading…
Reference in New Issue