mirror of https://github.com/django/django.git
Fixed #9475 -- Allowed RelatedManager.add(), create(), etc. for m2m with a through model.
This commit is contained in:
parent
f021c110d0
commit
769355c765
|
@ -956,32 +956,23 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
||||||
constrained_target = self.constrained_target
|
constrained_target = self.constrained_target
|
||||||
return constrained_target.count() if constrained_target else super().count()
|
return constrained_target.count() if constrained_target else super().count()
|
||||||
|
|
||||||
def add(self, *objs):
|
def add(self, *objs, through_defaults=None):
|
||||||
if not rel.through._meta.auto_created:
|
|
||||||
opts = self.through._meta
|
|
||||||
raise AttributeError(
|
|
||||||
"Cannot use add() on a ManyToManyField which specifies an "
|
|
||||||
"intermediary model. Use %s.%s's Manager instead." %
|
|
||||||
(opts.app_label, opts.object_name)
|
|
||||||
)
|
|
||||||
self._remove_prefetched_objects()
|
self._remove_prefetched_objects()
|
||||||
db = router.db_for_write(self.through, instance=self.instance)
|
db = router.db_for_write(self.through, instance=self.instance)
|
||||||
with transaction.atomic(using=db, savepoint=False):
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
self._add_items(self.source_field_name, self.target_field_name, *objs)
|
self._add_items(
|
||||||
|
self.source_field_name, self.target_field_name, *objs,
|
||||||
# If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
|
through_defaults=through_defaults,
|
||||||
|
)
|
||||||
|
# If this is a symmetrical m2m relation to self, add the mirror
|
||||||
|
# entry in the m2m table. `through_defaults` aren't used here
|
||||||
|
# because of the system check error fields.E332: Many-to-many
|
||||||
|
# fields with intermediate tables mus not be symmetrical.
|
||||||
if self.symmetrical:
|
if self.symmetrical:
|
||||||
self._add_items(self.target_field_name, self.source_field_name, *objs)
|
self._add_items(self.target_field_name, self.source_field_name, *objs)
|
||||||
add.alters_data = True
|
add.alters_data = True
|
||||||
|
|
||||||
def remove(self, *objs):
|
def remove(self, *objs):
|
||||||
if not rel.through._meta.auto_created:
|
|
||||||
opts = self.through._meta
|
|
||||||
raise AttributeError(
|
|
||||||
"Cannot use remove() on a ManyToManyField which specifies "
|
|
||||||
"an intermediary model. Use %s.%s's Manager instead." %
|
|
||||||
(opts.app_label, opts.object_name)
|
|
||||||
)
|
|
||||||
self._remove_prefetched_objects()
|
self._remove_prefetched_objects()
|
||||||
self._remove_items(self.source_field_name, self.target_field_name, *objs)
|
self._remove_items(self.source_field_name, self.target_field_name, *objs)
|
||||||
remove.alters_data = True
|
remove.alters_data = True
|
||||||
|
@ -1005,15 +996,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
||||||
)
|
)
|
||||||
clear.alters_data = True
|
clear.alters_data = True
|
||||||
|
|
||||||
def set(self, objs, *, clear=False):
|
def set(self, objs, *, clear=False, through_defaults=None):
|
||||||
if not rel.through._meta.auto_created:
|
|
||||||
opts = self.through._meta
|
|
||||||
raise AttributeError(
|
|
||||||
"Cannot set values on a ManyToManyField which specifies an "
|
|
||||||
"intermediary model. Use %s.%s's Manager instead." %
|
|
||||||
(opts.app_label, opts.object_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Force evaluation of `objs` in case it's a queryset whose value
|
# Force evaluation of `objs` in case it's a queryset whose value
|
||||||
# could be affected by `manager.clear()`. Refs #19816.
|
# could be affected by `manager.clear()`. Refs #19816.
|
||||||
objs = tuple(objs)
|
objs = tuple(objs)
|
||||||
|
@ -1022,7 +1005,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
||||||
with transaction.atomic(using=db, savepoint=False):
|
with transaction.atomic(using=db, savepoint=False):
|
||||||
if clear:
|
if clear:
|
||||||
self.clear()
|
self.clear()
|
||||||
self.add(*objs)
|
self.add(*objs, through_defaults=through_defaults)
|
||||||
else:
|
else:
|
||||||
old_ids = set(self.using(db).values_list(self.target_field.target_field.attname, flat=True))
|
old_ids = set(self.using(db).values_list(self.target_field.target_field.attname, flat=True))
|
||||||
|
|
||||||
|
@ -1038,49 +1021,41 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
||||||
new_objs.append(obj)
|
new_objs.append(obj)
|
||||||
|
|
||||||
self.remove(*old_ids)
|
self.remove(*old_ids)
|
||||||
self.add(*new_objs)
|
self.add(*new_objs, through_defaults=through_defaults)
|
||||||
set.alters_data = True
|
set.alters_data = True
|
||||||
|
|
||||||
def create(self, **kwargs):
|
def create(self, through_defaults=None, **kwargs):
|
||||||
# This check needs to be done here, since we can't later remove this
|
|
||||||
# from the method lookup table, as we do with add and remove.
|
|
||||||
if not self.through._meta.auto_created:
|
|
||||||
opts = self.through._meta
|
|
||||||
raise AttributeError(
|
|
||||||
"Cannot use create() on a ManyToManyField which specifies "
|
|
||||||
"an intermediary model. Use %s.%s's Manager instead." %
|
|
||||||
(opts.app_label, opts.object_name)
|
|
||||||
)
|
|
||||||
db = router.db_for_write(self.instance.__class__, instance=self.instance)
|
db = router.db_for_write(self.instance.__class__, instance=self.instance)
|
||||||
new_obj = super(ManyRelatedManager, self.db_manager(db)).create(**kwargs)
|
new_obj = super(ManyRelatedManager, self.db_manager(db)).create(**kwargs)
|
||||||
self.add(new_obj)
|
self.add(new_obj, through_defaults=through_defaults)
|
||||||
return new_obj
|
return new_obj
|
||||||
create.alters_data = True
|
create.alters_data = True
|
||||||
|
|
||||||
def get_or_create(self, **kwargs):
|
def get_or_create(self, through_defaults=None, **kwargs):
|
||||||
db = router.db_for_write(self.instance.__class__, instance=self.instance)
|
db = router.db_for_write(self.instance.__class__, instance=self.instance)
|
||||||
obj, created = super(ManyRelatedManager, self.db_manager(db)).get_or_create(**kwargs)
|
obj, created = super(ManyRelatedManager, self.db_manager(db)).get_or_create(**kwargs)
|
||||||
# We only need to add() if created because if we got an object back
|
# We only need to add() if created because if we got an object back
|
||||||
# from get() then the relationship already exists.
|
# from get() then the relationship already exists.
|
||||||
if created:
|
if created:
|
||||||
self.add(obj)
|
self.add(obj, through_defaults=through_defaults)
|
||||||
return obj, created
|
return obj, created
|
||||||
get_or_create.alters_data = True
|
get_or_create.alters_data = True
|
||||||
|
|
||||||
def update_or_create(self, **kwargs):
|
def update_or_create(self, through_defaults=None, **kwargs):
|
||||||
db = router.db_for_write(self.instance.__class__, instance=self.instance)
|
db = router.db_for_write(self.instance.__class__, instance=self.instance)
|
||||||
obj, created = super(ManyRelatedManager, self.db_manager(db)).update_or_create(**kwargs)
|
obj, created = super(ManyRelatedManager, self.db_manager(db)).update_or_create(**kwargs)
|
||||||
# We only need to add() if created because if we got an object back
|
# We only need to add() if created because if we got an object back
|
||||||
# from get() then the relationship already exists.
|
# from get() then the relationship already exists.
|
||||||
if created:
|
if created:
|
||||||
self.add(obj)
|
self.add(obj, through_defaults=through_defaults)
|
||||||
return obj, created
|
return obj, created
|
||||||
update_or_create.alters_data = True
|
update_or_create.alters_data = True
|
||||||
|
|
||||||
def _add_items(self, source_field_name, target_field_name, *objs):
|
def _add_items(self, source_field_name, target_field_name, *objs, through_defaults=None):
|
||||||
# source_field_name: the PK fieldname in join table for the source object
|
# source_field_name: the PK fieldname in join table for the source object
|
||||||
# target_field_name: the PK fieldname in join table for the target object
|
# target_field_name: the PK fieldname in join table for the target object
|
||||||
# *objs - objects to add. Either object instances, or primary keys of object instances.
|
# *objs - objects to add. Either object instances, or primary keys of object instances.
|
||||||
|
through_defaults = through_defaults or {}
|
||||||
|
|
||||||
# If there aren't any objects, there is nothing to do.
|
# If there aren't any objects, there is nothing to do.
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
|
@ -1130,10 +1105,10 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
||||||
|
|
||||||
# Add the ones that aren't there already
|
# Add the ones that aren't there already
|
||||||
self.through._default_manager.using(db).bulk_create([
|
self.through._default_manager.using(db).bulk_create([
|
||||||
self.through(**{
|
self.through(**dict(through_defaults, **{
|
||||||
'%s_id' % source_field_name: self.related_val[0],
|
'%s_id' % source_field_name: self.related_val[0],
|
||||||
'%s_id' % target_field_name: obj_id,
|
'%s_id' % target_field_name: obj_id,
|
||||||
})
|
}))
|
||||||
for obj_id in new_ids
|
for obj_id in new_ids
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ Related objects reference
|
||||||
In this example, the methods below will be available both on
|
In this example, the methods below will be available both on
|
||||||
``topping.pizza_set`` and on ``pizza.toppings``.
|
``topping.pizza_set`` and on ``pizza.toppings``.
|
||||||
|
|
||||||
.. method:: add(*objs, bulk=True)
|
.. method:: add(*objs, bulk=True, through_defaults=None)
|
||||||
|
|
||||||
Adds the specified model objects to the related object set.
|
Adds the specified model objects to the related object set.
|
||||||
|
|
||||||
|
@ -66,7 +66,15 @@ Related objects reference
|
||||||
Using ``add()`` on a relation that already exists won't duplicate the
|
Using ``add()`` on a relation that already exists won't duplicate the
|
||||||
relation, but it will still trigger signals.
|
relation, but it will still trigger signals.
|
||||||
|
|
||||||
.. method:: create(**kwargs)
|
Use the ``through_defaults`` argument to specify values for the new
|
||||||
|
:ref:`intermediate model <intermediary-manytomany>` instance(s), if
|
||||||
|
needed.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
|
||||||
|
The ``through_defaults`` argument was added.
|
||||||
|
|
||||||
|
.. method:: create(through_defaults=None, **kwargs)
|
||||||
|
|
||||||
Creates a new object, saves it and puts it in the related object set.
|
Creates a new object, saves it and puts it in the related object set.
|
||||||
Returns the newly created object::
|
Returns the newly created object::
|
||||||
|
@ -96,6 +104,14 @@ Related objects reference
|
||||||
parameter ``blog`` to ``create()``. Django figures out that the new
|
parameter ``blog`` to ``create()``. Django figures out that the new
|
||||||
``Entry`` object's ``blog`` field should be set to ``b``.
|
``Entry`` object's ``blog`` field should be set to ``b``.
|
||||||
|
|
||||||
|
Use the ``through_defaults`` argument to specify values for the new
|
||||||
|
:ref:`intermediate model <intermediary-manytomany>` instance, if
|
||||||
|
needed.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
|
||||||
|
The ``through_defaults`` argument was added.
|
||||||
|
|
||||||
.. method:: remove(*objs, bulk=True)
|
.. method:: remove(*objs, bulk=True)
|
||||||
|
|
||||||
Removes the specified model objects from the related object set::
|
Removes the specified model objects from the related object set::
|
||||||
|
@ -149,7 +165,7 @@ Related objects reference
|
||||||
For many-to-many relationships, the ``bulk`` keyword argument doesn't
|
For many-to-many relationships, the ``bulk`` keyword argument doesn't
|
||||||
exist.
|
exist.
|
||||||
|
|
||||||
.. method:: set(objs, bulk=True, clear=False)
|
.. method:: set(objs, bulk=True, clear=False, through_defaults=None)
|
||||||
|
|
||||||
Replace the set of related objects::
|
Replace the set of related objects::
|
||||||
|
|
||||||
|
@ -172,6 +188,14 @@ Related objects reference
|
||||||
race conditions. For instance, new objects may be added to the database
|
race conditions. For instance, new objects may be added to the database
|
||||||
in between the call to ``clear()`` and the call to ``add()``.
|
in between the call to ``clear()`` and the call to ``add()``.
|
||||||
|
|
||||||
|
Use the ``through_defaults`` argument to specify values for the new
|
||||||
|
:ref:`intermediate model <intermediary-manytomany>` instance(s), if
|
||||||
|
needed.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
|
||||||
|
The ``through_defaults`` argument was added.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Note that ``add()``, ``create()``, ``remove()``, ``clear()``, and
|
Note that ``add()``, ``create()``, ``remove()``, ``clear()``, and
|
||||||
|
@ -179,11 +203,6 @@ Related objects reference
|
||||||
related fields. In other words, there is no need to call ``save()``
|
related fields. In other words, there is no need to call ``save()``
|
||||||
on either end of the relationship.
|
on either end of the relationship.
|
||||||
|
|
||||||
Also, if you are using :ref:`an intermediate model
|
|
||||||
<intermediary-manytomany>` for a many-to-many relationship, then the
|
|
||||||
``add()``, ``create()``, ``remove()``, and ``set()`` methods are
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
If you use :meth:`~django.db.models.query.QuerySet.prefetch_related`,
|
If you use :meth:`~django.db.models.query.QuerySet.prefetch_related`,
|
||||||
the ``add()``, ``remove()``, ``clear()``, and ``set()`` methods clear
|
the ``add()``, ``remove()``, ``clear()``, and ``set()`` methods clear
|
||||||
the prefetched cache.
|
the prefetched cache.
|
||||||
|
|
|
@ -256,6 +256,13 @@ Models
|
||||||
specified on initialization to ensure that the aggregate function is only
|
specified on initialization to ensure that the aggregate function is only
|
||||||
called for each distinct value of ``expressions``.
|
called for each distinct value of ``expressions``.
|
||||||
|
|
||||||
|
* The :meth:`.RelatedManager.add`, :meth:`~.RelatedManager.create`,
|
||||||
|
:meth:`~.RelatedManager.remove`, :meth:`~.RelatedManager.set`,
|
||||||
|
``get_or_create()``, and ``update_or_create()`` methods are now allowed on
|
||||||
|
many-to-many relationships with intermediate models. The new
|
||||||
|
``through_defaults`` argument is used to specify values for new intermediate
|
||||||
|
model instance(s).
|
||||||
|
|
||||||
Requests and Responses
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -34,10 +34,7 @@ objects, and a ``Publication`` has multiple ``Article`` objects:
|
||||||
return self.headline
|
return self.headline
|
||||||
|
|
||||||
What follows are examples of operations that can be performed using the Python
|
What follows are examples of operations that can be performed using the Python
|
||||||
API facilities. Note that if you are using :ref:`an intermediate model
|
API facilities.
|
||||||
<intermediary-manytomany>` for a many-to-many relationship, some of the related
|
|
||||||
manager's methods are disabled, so some of these examples won't work with such
|
|
||||||
models.
|
|
||||||
|
|
||||||
Create a few ``Publications``::
|
Create a few ``Publications``::
|
||||||
|
|
||||||
|
|
|
@ -511,37 +511,31 @@ the intermediate model::
|
||||||
>>> beatles.members.all()
|
>>> beatles.members.all()
|
||||||
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>
|
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>
|
||||||
|
|
||||||
Unlike normal many-to-many fields, you *can't* use ``add()``, ``create()``,
|
You can also use ``add()``, ``create()``, or ``set()`` to create relationships,
|
||||||
or ``set()`` to create relationships::
|
as long as your specify ``through_defaults`` for any required fields::
|
||||||
|
|
||||||
>>> # The following statements will not work
|
>>> beatles.members.add(john, through_defaults={'date_joined': date(1960, 8, 1)})
|
||||||
>>> beatles.members.add(john)
|
>>> beatles.members.create(name="George Harrison", through_defaults={'date_joined': date(1960, 8, 1)})
|
||||||
>>> beatles.members.create(name="George Harrison")
|
>>> beatles.members.set([john, paul, ringo, george], through_defaults={'date_joined': date(1960, 8, 1)})
|
||||||
>>> beatles.members.set([john, paul, ringo, george])
|
|
||||||
|
|
||||||
Why? You can't just create a relationship between a ``Person`` and a ``Group``
|
You may prefer to create instances of the intermediate model directly.
|
||||||
- you need to specify all the detail for the relationship required by the
|
|
||||||
``Membership`` model. The simple ``add``, ``create`` and assignment calls
|
|
||||||
don't provide a way to specify this extra detail. As a result, they are
|
|
||||||
disabled for many-to-many relationships that use an intermediate model.
|
|
||||||
The only way to create this type of relationship is to create instances of the
|
|
||||||
intermediate model.
|
|
||||||
|
|
||||||
The :meth:`~django.db.models.fields.related.RelatedManager.remove` method is
|
If the custom through table defined by the intermediate model does not enforce
|
||||||
disabled for similar reasons. For example, if the custom through table defined
|
uniqueness on the ``(model1, model2)`` pair, allowing multiple values, the
|
||||||
by the intermediate model does not enforce uniqueness on the
|
:meth:`~django.db.models.fields.related.RelatedManager.remove` call will
|
||||||
``(model1, model2)`` pair, a ``remove()`` call would not provide enough
|
remove all intermediate model instances::
|
||||||
information as to which intermediate model instance should be deleted::
|
|
||||||
|
|
||||||
>>> Membership.objects.create(person=ringo, group=beatles,
|
>>> Membership.objects.create(person=ringo, group=beatles,
|
||||||
... date_joined=date(1968, 9, 4),
|
... date_joined=date(1968, 9, 4),
|
||||||
... invite_reason="You've been gone for a month and we miss you.")
|
... invite_reason="You've been gone for a month and we miss you.")
|
||||||
>>> beatles.members.all()
|
>>> beatles.members.all()
|
||||||
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>, <Person: Ringo Starr>]>
|
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>, <Person: Ringo Starr>]>
|
||||||
>>> # This will not work because it cannot tell which membership to remove
|
>>> # This deletes both of the intermediate model instances for Ringo Starr
|
||||||
>>> beatles.members.remove(ringo)
|
>>> beatles.members.remove(ringo)
|
||||||
|
>>> beatles.members.all()
|
||||||
|
<QuerySet [<Person: Paul McCartney>]>
|
||||||
|
|
||||||
However, the :meth:`~django.db.models.fields.related.RelatedManager.clear`
|
The :meth:`~django.db.models.fields.related.RelatedManager.clear`
|
||||||
method can be used to remove all many-to-many relationships for an instance::
|
method can be used to remove all many-to-many relationships for an instance::
|
||||||
|
|
||||||
>>> # Beatles have broken up
|
>>> # Beatles have broken up
|
||||||
|
@ -550,10 +544,9 @@ method can be used to remove all many-to-many relationships for an instance::
|
||||||
>>> Membership.objects.all()
|
>>> Membership.objects.all()
|
||||||
<QuerySet []>
|
<QuerySet []>
|
||||||
|
|
||||||
Once you have established the many-to-many relationships by creating instances
|
Once you have established the many-to-many relationships, you can issue
|
||||||
of your intermediate model, you can issue queries. Just as with normal
|
queries. Just as with normal many-to-many relationships, you can query using
|
||||||
many-to-many relationships, you can query using the attributes of the
|
the attributes of the many-to-many-related model::
|
||||||
many-to-many-related model::
|
|
||||||
|
|
||||||
# Find all the groups with a member whose name starts with 'Paul'
|
# Find all the groups with a member whose name starts with 'Paul'
|
||||||
>>> Group.objects.filter(members__name__startswith='Paul')
|
>>> Group.objects.filter(members__name__startswith='Paul')
|
||||||
|
|
|
@ -66,7 +66,7 @@ class CustomMembership(models.Model):
|
||||||
class TestNoDefaultsOrNulls(models.Model):
|
class TestNoDefaultsOrNulls(models.Model):
|
||||||
person = models.ForeignKey(Person, models.CASCADE)
|
person = models.ForeignKey(Person, models.CASCADE)
|
||||||
group = models.ForeignKey(Group, models.CASCADE)
|
group = models.ForeignKey(Group, models.CASCADE)
|
||||||
nodefaultnonull = models.CharField(max_length=5)
|
nodefaultnonull = models.IntegerField()
|
||||||
|
|
||||||
|
|
||||||
class PersonSelfRefM2M(models.Model):
|
class PersonSelfRefM2M(models.Model):
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
|
from django.db import IntegrityError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
|
@ -56,52 +57,76 @@ class M2mThroughTests(TestCase):
|
||||||
expected
|
expected
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_cannot_use_add_on_m2m_with_intermediary_model(self):
|
def test_add_on_m2m_with_intermediate_model(self):
|
||||||
msg = 'Cannot use add() on a ManyToManyField which specifies an intermediary model'
|
self.rock.members.add(self.bob, through_defaults={'invite_reason': 'He is good.'})
|
||||||
|
self.assertSequenceEqual(self.rock.members.all(), [self.bob])
|
||||||
|
self.assertEqual(self.rock.membership_set.get().invite_reason, 'He is good.')
|
||||||
|
|
||||||
with self.assertRaisesMessage(AttributeError, msg):
|
def test_add_on_m2m_with_intermediate_model_value_required(self):
|
||||||
self.rock.members.add(self.bob)
|
self.rock.nodefaultsnonulls.add(self.jim, through_defaults={'nodefaultnonull': 1})
|
||||||
|
self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
|
||||||
|
|
||||||
self.assertQuerysetEqual(
|
def test_add_on_m2m_with_intermediate_model_value_required_fails(self):
|
||||||
self.rock.members.all(),
|
with self.assertRaises(IntegrityError):
|
||||||
[]
|
self.rock.nodefaultsnonulls.add(self.jim)
|
||||||
)
|
|
||||||
|
|
||||||
def test_cannot_use_create_on_m2m_with_intermediary_model(self):
|
def test_create_on_m2m_with_intermediate_model(self):
|
||||||
msg = 'Cannot use create() on a ManyToManyField which specifies an intermediary model'
|
annie = self.rock.members.create(name='Annie', through_defaults={'invite_reason': 'She was just awesome.'})
|
||||||
|
self.assertSequenceEqual(self.rock.members.all(), [annie])
|
||||||
|
self.assertEqual(self.rock.membership_set.get().invite_reason, 'She was just awesome.')
|
||||||
|
|
||||||
with self.assertRaisesMessage(AttributeError, msg):
|
def test_create_on_m2m_with_intermediate_model_value_required(self):
|
||||||
self.rock.members.create(name='Annie')
|
self.rock.nodefaultsnonulls.create(name='Test', through_defaults={'nodefaultnonull': 1})
|
||||||
|
self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
|
||||||
|
|
||||||
self.assertQuerysetEqual(
|
def test_create_on_m2m_with_intermediate_model_value_required_fails(self):
|
||||||
self.rock.members.all(),
|
with self.assertRaises(IntegrityError):
|
||||||
[]
|
self.rock.nodefaultsnonulls.create(name='Test')
|
||||||
)
|
|
||||||
|
|
||||||
def test_cannot_use_remove_on_m2m_with_intermediary_model(self):
|
def test_get_or_create_on_m2m_with_intermediate_model_value_required(self):
|
||||||
|
self.rock.nodefaultsnonulls.get_or_create(name='Test', through_defaults={'nodefaultnonull': 1})
|
||||||
|
self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
|
||||||
|
|
||||||
|
def test_get_or_create_on_m2m_with_intermediate_model_value_required_fails(self):
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
self.rock.nodefaultsnonulls.get_or_create(name='Test')
|
||||||
|
|
||||||
|
def test_update_or_create_on_m2m_with_intermediate_model_value_required(self):
|
||||||
|
self.rock.nodefaultsnonulls.update_or_create(name='Test', through_defaults={'nodefaultnonull': 1})
|
||||||
|
self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
|
||||||
|
|
||||||
|
def test_update_or_create_on_m2m_with_intermediate_model_value_required_fails(self):
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
self.rock.nodefaultsnonulls.update_or_create(name='Test')
|
||||||
|
|
||||||
|
def test_remove_on_m2m_with_intermediate_model(self):
|
||||||
Membership.objects.create(person=self.jim, group=self.rock)
|
Membership.objects.create(person=self.jim, group=self.rock)
|
||||||
msg = 'Cannot use remove() on a ManyToManyField which specifies an intermediary model'
|
self.rock.members.remove(self.jim)
|
||||||
|
self.assertSequenceEqual(self.rock.members.all(), [])
|
||||||
|
|
||||||
with self.assertRaisesMessage(AttributeError, msg):
|
def test_remove_on_m2m_with_intermediate_model_multiple(self):
|
||||||
self.rock.members.remove(self.jim)
|
Membership.objects.create(person=self.jim, group=self.rock, invite_reason='1')
|
||||||
|
Membership.objects.create(person=self.jim, group=self.rock, invite_reason='2')
|
||||||
|
self.assertSequenceEqual(self.rock.members.all(), [self.jim, self.jim])
|
||||||
|
self.rock.members.remove(self.jim)
|
||||||
|
self.assertSequenceEqual(self.rock.members.all(), [])
|
||||||
|
|
||||||
self.assertQuerysetEqual(
|
def test_set_on_m2m_with_intermediate_model(self):
|
||||||
self.rock.members.all(),
|
|
||||||
['Jim'],
|
|
||||||
attrgetter("name")
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_cannot_use_setattr_on_m2m_with_intermediary_model(self):
|
|
||||||
msg = 'Cannot set values on a ManyToManyField which specifies an intermediary model'
|
|
||||||
members = list(Person.objects.filter(name__in=['Bob', 'Jim']))
|
members = list(Person.objects.filter(name__in=['Bob', 'Jim']))
|
||||||
|
self.rock.members.set(members)
|
||||||
|
self.assertSequenceEqual(self.rock.members.all(), [self.bob, self.jim])
|
||||||
|
|
||||||
with self.assertRaisesMessage(AttributeError, msg):
|
def test_set_on_m2m_with_intermediate_model_value_required(self):
|
||||||
self.rock.members.set(members)
|
self.rock.nodefaultsnonulls.set([self.jim], through_defaults={'nodefaultnonull': 1})
|
||||||
|
self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
|
||||||
|
self.rock.nodefaultsnonulls.set([self.jim], through_defaults={'nodefaultnonull': 2})
|
||||||
|
self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
|
||||||
|
self.rock.nodefaultsnonulls.set([self.jim], through_defaults={'nodefaultnonull': 2}, clear=True)
|
||||||
|
self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 2)
|
||||||
|
|
||||||
self.assertQuerysetEqual(
|
def test_set_on_m2m_with_intermediate_model_value_required_fails(self):
|
||||||
self.rock.members.all(),
|
with self.assertRaises(IntegrityError):
|
||||||
[]
|
self.rock.nodefaultsnonulls.set([self.jim])
|
||||||
)
|
|
||||||
|
|
||||||
def test_clear_removes_all_the_m2m_relationships(self):
|
def test_clear_removes_all_the_m2m_relationships(self):
|
||||||
Membership.objects.create(person=self.jim, group=self.rock)
|
Membership.objects.create(person=self.jim, group=self.rock)
|
||||||
|
@ -125,52 +150,23 @@ class M2mThroughTests(TestCase):
|
||||||
attrgetter("name")
|
attrgetter("name")
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_cannot_use_add_on_reverse_m2m_with_intermediary_model(self):
|
def test_add_on_reverse_m2m_with_intermediate_model(self):
|
||||||
msg = 'Cannot use add() on a ManyToManyField which specifies an intermediary model'
|
self.bob.group_set.add(self.rock)
|
||||||
|
self.assertSequenceEqual(self.bob.group_set.all(), [self.rock])
|
||||||
|
|
||||||
with self.assertRaisesMessage(AttributeError, msg):
|
def test_create_on_reverse_m2m_with_intermediate_model(self):
|
||||||
self.bob.group_set.add(self.bob)
|
funk = self.bob.group_set.create(name='Funk')
|
||||||
|
self.assertSequenceEqual(self.bob.group_set.all(), [funk])
|
||||||
|
|
||||||
self.assertQuerysetEqual(
|
def test_remove_on_reverse_m2m_with_intermediate_model(self):
|
||||||
self.bob.group_set.all(),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_cannot_use_create_on_reverse_m2m_with_intermediary_model(self):
|
|
||||||
msg = 'Cannot use create() on a ManyToManyField which specifies an intermediary model'
|
|
||||||
|
|
||||||
with self.assertRaisesMessage(AttributeError, msg):
|
|
||||||
self.bob.group_set.create(name='Funk')
|
|
||||||
|
|
||||||
self.assertQuerysetEqual(
|
|
||||||
self.bob.group_set.all(),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_cannot_use_remove_on_reverse_m2m_with_intermediary_model(self):
|
|
||||||
Membership.objects.create(person=self.bob, group=self.rock)
|
Membership.objects.create(person=self.bob, group=self.rock)
|
||||||
msg = 'Cannot use remove() on a ManyToManyField which specifies an intermediary model'
|
self.bob.group_set.remove(self.rock)
|
||||||
|
self.assertSequenceEqual(self.bob.group_set.all(), [])
|
||||||
|
|
||||||
with self.assertRaisesMessage(AttributeError, msg):
|
def test_set_on_reverse_m2m_with_intermediate_model(self):
|
||||||
self.bob.group_set.remove(self.rock)
|
|
||||||
|
|
||||||
self.assertQuerysetEqual(
|
|
||||||
self.bob.group_set.all(),
|
|
||||||
['Rock'],
|
|
||||||
attrgetter('name')
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_cannot_use_setattr_on_reverse_m2m_with_intermediary_model(self):
|
|
||||||
msg = 'Cannot set values on a ManyToManyField which specifies an intermediary model'
|
|
||||||
members = list(Group.objects.filter(name__in=['Rock', 'Roll']))
|
members = list(Group.objects.filter(name__in=['Rock', 'Roll']))
|
||||||
|
self.bob.group_set.set(members)
|
||||||
with self.assertRaisesMessage(AttributeError, msg):
|
self.assertSequenceEqual(self.bob.group_set.all(), [self.rock, self.roll])
|
||||||
self.bob.group_set.set(members)
|
|
||||||
|
|
||||||
self.assertQuerysetEqual(
|
|
||||||
self.bob.group_set.all(),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_clear_on_reverse_removes_all_the_m2m_relationships(self):
|
def test_clear_on_reverse_removes_all_the_m2m_relationships(self):
|
||||||
Membership.objects.create(person=self.jim, group=self.rock)
|
Membership.objects.create(person=self.jim, group=self.rock)
|
||||||
|
|
|
@ -47,42 +47,6 @@ class M2MThroughTestCase(TestCase):
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_cannot_use_setattr_on_reverse_m2m_with_intermediary_model(self):
|
|
||||||
msg = (
|
|
||||||
"Cannot set values on a ManyToManyField which specifies an "
|
|
||||||
"intermediary model. Use m2m_through_regress.Membership's Manager "
|
|
||||||
"instead."
|
|
||||||
)
|
|
||||||
with self.assertRaisesMessage(AttributeError, msg):
|
|
||||||
self.bob.group_set.set([])
|
|
||||||
|
|
||||||
def test_cannot_use_setattr_on_forward_m2m_with_intermediary_model(self):
|
|
||||||
msg = (
|
|
||||||
"Cannot set values on a ManyToManyField which specifies an "
|
|
||||||
"intermediary model. Use m2m_through_regress.Membership's Manager "
|
|
||||||
"instead."
|
|
||||||
)
|
|
||||||
with self.assertRaisesMessage(AttributeError, msg):
|
|
||||||
self.roll.members.set([])
|
|
||||||
|
|
||||||
def test_cannot_use_create_on_m2m_with_intermediary_model(self):
|
|
||||||
msg = (
|
|
||||||
"Cannot use create() on a ManyToManyField which specifies an "
|
|
||||||
"intermediary model. Use m2m_through_regress.Membership's "
|
|
||||||
"Manager instead."
|
|
||||||
)
|
|
||||||
with self.assertRaisesMessage(AttributeError, msg):
|
|
||||||
self.rock.members.create(name="Anne")
|
|
||||||
|
|
||||||
def test_cannot_use_create_on_reverse_m2m_with_intermediary_model(self):
|
|
||||||
msg = (
|
|
||||||
"Cannot use create() on a ManyToManyField which specifies an "
|
|
||||||
"intermediary model. Use m2m_through_regress.Membership's "
|
|
||||||
"Manager instead."
|
|
||||||
)
|
|
||||||
with self.assertRaisesMessage(AttributeError, msg):
|
|
||||||
self.bob.group_set.create(name="Funk")
|
|
||||||
|
|
||||||
def test_retrieve_reverse_m2m_items_via_custom_id_intermediary(self):
|
def test_retrieve_reverse_m2m_items_via_custom_id_intermediary(self):
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
self.frank.group_set.all(), [
|
self.frank.group_set.all(), [
|
||||||
|
|
Loading…
Reference in New Issue