Fixed #31046 -- Allowed RelatedManager.add()/create()/set() to accept callable values in through_defaults.
This commit is contained in:
parent
c50839fccf
commit
26cab4e8c1
|
@ -68,6 +68,7 @@ from django.db import connections, router, transaction
|
||||||
from django.db.models import Q, signals
|
from django.db.models import Q, signals
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.db.models.query_utils import DeferredAttribute
|
from django.db.models.query_utils import DeferredAttribute
|
||||||
|
from django.db.models.utils import resolve_callables
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
|
||||||
|
@ -1116,7 +1117,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
||||||
if not objs:
|
if not objs:
|
||||||
return
|
return
|
||||||
|
|
||||||
through_defaults = through_defaults or {}
|
through_defaults = dict(resolve_callables(through_defaults or {}))
|
||||||
target_ids = self._get_target_ids(target_field_name, objs)
|
target_ids = self._get_target_ids(target_field_name, objs)
|
||||||
db = router.db_for_write(self.through, instance=self.instance)
|
db = router.db_for_write(self.through, instance=self.instance)
|
||||||
can_ignore_conflicts, must_send_signals, can_fast_add = self._get_add_plan(db, source_field_name)
|
can_ignore_conflicts, must_send_signals, can_fast_add = self._get_add_plan(db, source_field_name)
|
||||||
|
|
|
@ -71,7 +71,13 @@ Related objects reference
|
||||||
|
|
||||||
Use the ``through_defaults`` argument to specify values for the new
|
Use the ``through_defaults`` argument to specify values for the new
|
||||||
:ref:`intermediate model <intermediary-manytomany>` instance(s), if
|
:ref:`intermediate model <intermediary-manytomany>` instance(s), if
|
||||||
needed.
|
needed. You can use callables as values in the ``through_defaults``
|
||||||
|
dictionary and they will be evaluated once before creating any
|
||||||
|
intermediate instance(s).
|
||||||
|
|
||||||
|
.. versionchanged:: 3.1
|
||||||
|
|
||||||
|
``through_defaults`` values can now be callables.
|
||||||
|
|
||||||
.. method:: create(through_defaults=None, **kwargs)
|
.. method:: create(through_defaults=None, **kwargs)
|
||||||
|
|
||||||
|
@ -105,7 +111,12 @@ Related objects reference
|
||||||
|
|
||||||
Use the ``through_defaults`` argument to specify values for the new
|
Use the ``through_defaults`` argument to specify values for the new
|
||||||
:ref:`intermediate model <intermediary-manytomany>` instance, if
|
:ref:`intermediate model <intermediary-manytomany>` instance, if
|
||||||
needed.
|
needed. You can use callables as values in the ``through_defaults``
|
||||||
|
dictionary.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.1
|
||||||
|
|
||||||
|
``through_defaults`` values can now be callables.
|
||||||
|
|
||||||
.. method:: remove(*objs, bulk=True)
|
.. method:: remove(*objs, bulk=True)
|
||||||
|
|
||||||
|
@ -193,7 +204,13 @@ Related objects reference
|
||||||
|
|
||||||
Use the ``through_defaults`` argument to specify values for the new
|
Use the ``through_defaults`` argument to specify values for the new
|
||||||
:ref:`intermediate model <intermediary-manytomany>` instance(s), if
|
:ref:`intermediate model <intermediary-manytomany>` instance(s), if
|
||||||
needed.
|
needed. You can use callables as values in the ``through_defaults``
|
||||||
|
dictionary and they will be evaluated once before creating any
|
||||||
|
intermediate instance(s).
|
||||||
|
|
||||||
|
.. versionchanged:: 3.1
|
||||||
|
|
||||||
|
``through_defaults`` values can now be callables.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
|
|
@ -209,6 +209,10 @@ Models
|
||||||
|
|
||||||
* :attr:`.CheckConstraint.check` now supports boolean expressions.
|
* :attr:`.CheckConstraint.check` now supports boolean expressions.
|
||||||
|
|
||||||
|
* The :meth:`.RelatedManager.add`, :meth:`~.RelatedManager.create`, and
|
||||||
|
:meth:`~.RelatedManager.set` methods now accept callables as values in the
|
||||||
|
``through_defaults`` argument.
|
||||||
|
|
||||||
Pagination
|
Pagination
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,40 @@ class M2mThroughTests(TestCase):
|
||||||
self.assertSequenceEqual(self.rock.members.all(), [self.bob])
|
self.assertSequenceEqual(self.rock.members.all(), [self.bob])
|
||||||
self.assertEqual(self.rock.membership_set.get().invite_reason, 'He is good.')
|
self.assertEqual(self.rock.membership_set.get().invite_reason, 'He is good.')
|
||||||
|
|
||||||
|
def test_add_on_m2m_with_intermediate_model_callable_through_default(self):
|
||||||
|
def invite_reason_callable():
|
||||||
|
return 'They were good at %s' % datetime.now()
|
||||||
|
|
||||||
|
self.rock.members.add(
|
||||||
|
self.bob, self.jane,
|
||||||
|
through_defaults={'invite_reason': invite_reason_callable},
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(self.rock.members.all(), [self.bob, self.jane])
|
||||||
|
self.assertEqual(
|
||||||
|
self.rock.membership_set.filter(
|
||||||
|
invite_reason__startswith='They were good at ',
|
||||||
|
).count(),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
# invite_reason_callable() is called once.
|
||||||
|
self.assertEqual(
|
||||||
|
self.bob.membership_set.get().invite_reason,
|
||||||
|
self.jane.membership_set.get().invite_reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_on_m2m_with_intermediate_model_callable_through_default(self):
|
||||||
|
self.rock.members.set(
|
||||||
|
[self.bob, self.jane],
|
||||||
|
through_defaults={'invite_reason': lambda: 'Why not?'},
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(self.rock.members.all(), [self.bob, self.jane])
|
||||||
|
self.assertEqual(
|
||||||
|
self.rock.membership_set.filter(
|
||||||
|
invite_reason__startswith='Why not?',
|
||||||
|
).count(),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
def test_add_on_m2m_with_intermediate_model_value_required(self):
|
def test_add_on_m2m_with_intermediate_model_value_required(self):
|
||||||
self.rock.nodefaultsnonulls.add(self.jim, through_defaults={'nodefaultnonull': 1})
|
self.rock.nodefaultsnonulls.add(self.jim, through_defaults={'nodefaultnonull': 1})
|
||||||
self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
|
self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
|
||||||
|
@ -75,6 +109,17 @@ class M2mThroughTests(TestCase):
|
||||||
self.assertSequenceEqual(self.rock.members.all(), [annie])
|
self.assertSequenceEqual(self.rock.members.all(), [annie])
|
||||||
self.assertEqual(self.rock.membership_set.get().invite_reason, 'She was just awesome.')
|
self.assertEqual(self.rock.membership_set.get().invite_reason, 'She was just awesome.')
|
||||||
|
|
||||||
|
def test_create_on_m2m_with_intermediate_model_callable_through_default(self):
|
||||||
|
annie = self.rock.members.create(
|
||||||
|
name='Annie',
|
||||||
|
through_defaults={'invite_reason': lambda: 'She was just awesome.'},
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(self.rock.members.all(), [annie])
|
||||||
|
self.assertEqual(
|
||||||
|
self.rock.membership_set.get().invite_reason,
|
||||||
|
'She was just awesome.',
|
||||||
|
)
|
||||||
|
|
||||||
def test_create_on_m2m_with_intermediate_model_value_required(self):
|
def test_create_on_m2m_with_intermediate_model_value_required(self):
|
||||||
self.rock.nodefaultsnonulls.create(name='Test', through_defaults={'nodefaultnonull': 1})
|
self.rock.nodefaultsnonulls.create(name='Test', through_defaults={'nodefaultnonull': 1})
|
||||||
self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
|
self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
|
||||||
|
|
Loading…
Reference in New Issue