Fixed #13552 -- Added a 'using' parameter to database signals. Thanks to gmandx for the suggestion, and Andrew Godwin for the patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@13538 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2010-08-07 06:27:52 +00:00
parent 458e3a3ff9
commit 7e06065d8b
7 changed files with 157 additions and 22 deletions

View File

@ -190,6 +190,7 @@ answer newbie questions, and generally made Django that much better:
martin.glueck@gmail.com martin.glueck@gmail.com
Artyom Gnilov <boobsd@gmail.com> Artyom Gnilov <boobsd@gmail.com>
Ben Godfrey <http://aftnn.org> Ben Godfrey <http://aftnn.org>
Andrew Godwin <andrew@aeracode.org>
GomoX <gomo@datafull.com> GomoX <gomo@datafull.com>
Guilherme Mesquita Gondim <semente@taurinus.org> Guilherme Mesquita Gondim <semente@taurinus.org>
Mario Gonzalez <gonzalemario@gmail.com> Mario Gonzalez <gonzalemario@gmail.com>

View File

@ -456,7 +456,7 @@ class Model(object):
meta = cls._meta meta = cls._meta
if origin and not meta.auto_created: if origin and not meta.auto_created:
signals.pre_save.send(sender=origin, instance=self, raw=raw) signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using)
# If we are in a raw save, save the object exactly as presented. # If we are in a raw save, save the object exactly as presented.
# That means that we don't try to be smart about saving attributes # That means that we don't try to be smart about saving attributes
@ -540,7 +540,7 @@ class Model(object):
# Signal that the save is complete # Signal that the save is complete
if origin and not meta.auto_created: if origin and not meta.auto_created:
signals.post_save.send(sender=origin, instance=self, signals.post_save.send(sender=origin, instance=self,
created=(not record_exists), raw=raw) created=(not record_exists), raw=raw, using=using)
save_base.alters_data = True save_base.alters_data = True

View File

@ -566,7 +566,7 @@ def create_many_related_manager(superclass, rel=False):
# duplicate data row for symmetrical reverse entries. # duplicate data row for symmetrical reverse entries.
signals.m2m_changed.send(sender=rel.through, action='pre_add', signals.m2m_changed.send(sender=rel.through, action='pre_add',
instance=self.instance, reverse=self.reverse, instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=new_ids) model=self.model, pk_set=new_ids, using=db)
# Add the ones that aren't there already # Add the ones that aren't there already
for obj_id in new_ids: for obj_id in new_ids:
self.through._default_manager.using(db).create(**{ self.through._default_manager.using(db).create(**{
@ -578,7 +578,7 @@ def create_many_related_manager(superclass, rel=False):
# duplicate data row for symmetrical reverse entries. # duplicate data row for symmetrical reverse entries.
signals.m2m_changed.send(sender=rel.through, action='post_add', signals.m2m_changed.send(sender=rel.through, action='post_add',
instance=self.instance, reverse=self.reverse, instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=new_ids) model=self.model, pk_set=new_ids, using=db)
def _remove_items(self, source_field_name, target_field_name, *objs): def _remove_items(self, source_field_name, target_field_name, *objs):
# source_col_name: the PK colname in join_table for the source object # source_col_name: the PK colname in join_table for the source object
@ -594,14 +594,16 @@ def create_many_related_manager(superclass, rel=False):
old_ids.add(obj.pk) old_ids.add(obj.pk)
else: else:
old_ids.add(obj) old_ids.add(obj)
# Work out what DB we're operating on
db = router.db_for_write(self.through.__class__, instance=self.instance)
# Send a signal to the other end if need be.
if self.reverse or source_field_name == self.source_field_name: if self.reverse or source_field_name == self.source_field_name:
# Don't send the signal when we are deleting the # Don't send the signal when we are deleting the
# duplicate data row for symmetrical reverse entries. # duplicate data row for symmetrical reverse entries.
signals.m2m_changed.send(sender=rel.through, action="pre_remove", signals.m2m_changed.send(sender=rel.through, action="pre_remove",
instance=self.instance, reverse=self.reverse, instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=old_ids) model=self.model, pk_set=old_ids, using=db)
# Remove the specified objects from the join table # Remove the specified objects from the join table
db = router.db_for_write(self.through.__class__, instance=self.instance)
self.through._default_manager.using(db).filter(**{ self.through._default_manager.using(db).filter(**{
source_field_name: self._pk_val, source_field_name: self._pk_val,
'%s__in' % target_field_name: old_ids '%s__in' % target_field_name: old_ids
@ -611,17 +613,17 @@ def create_many_related_manager(superclass, rel=False):
# duplicate data row for symmetrical reverse entries. # duplicate data row for symmetrical reverse entries.
signals.m2m_changed.send(sender=rel.through, action="post_remove", signals.m2m_changed.send(sender=rel.through, action="post_remove",
instance=self.instance, reverse=self.reverse, instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=old_ids) model=self.model, pk_set=old_ids, using=db)
def _clear_items(self, source_field_name): def _clear_items(self, source_field_name):
db = router.db_for_write(self.through.__class__, instance=self.instance)
# source_col_name: the PK colname in join_table for the source object # source_col_name: the PK colname in join_table for the source object
if self.reverse or source_field_name == self.source_field_name: if self.reverse or source_field_name == self.source_field_name:
# Don't send the signal when we are clearing the # Don't send the signal when we are clearing the
# duplicate data rows for symmetrical reverse entries. # duplicate data rows for symmetrical reverse entries.
signals.m2m_changed.send(sender=rel.through, action="pre_clear", signals.m2m_changed.send(sender=rel.through, action="pre_clear",
instance=self.instance, reverse=self.reverse, instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=None) model=self.model, pk_set=None, using=db)
db = router.db_for_write(self.through.__class__, instance=self.instance)
self.through._default_manager.using(db).filter(**{ self.through._default_manager.using(db).filter(**{
source_field_name: self._pk_val source_field_name: self._pk_val
}).delete() }).delete()
@ -630,7 +632,7 @@ def create_many_related_manager(superclass, rel=False):
# duplicate data rows for symmetrical reverse entries. # duplicate data rows for symmetrical reverse entries.
signals.m2m_changed.send(sender=rel.through, action="post_clear", signals.m2m_changed.send(sender=rel.through, action="post_clear",
instance=self.instance, reverse=self.reverse, instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=None) model=self.model, pk_set=None, using=db)
return ManyRelatedManager return ManyRelatedManager

View File

@ -1311,7 +1311,8 @@ def delete_objects(seen_objs, using):
# Pre-notify all instances to be deleted. # Pre-notify all instances to be deleted.
for pk_val, instance in items: for pk_val, instance in items:
if not cls._meta.auto_created: if not cls._meta.auto_created:
signals.pre_delete.send(sender=cls, instance=instance) signals.pre_delete.send(sender=cls, instance=instance,
using=using)
pk_list = [pk for pk,instance in items] pk_list = [pk for pk,instance in items]
@ -1343,7 +1344,7 @@ def delete_objects(seen_objs, using):
setattr(instance, field.attname, None) setattr(instance, field.attname, None)
if not cls._meta.auto_created: if not cls._meta.auto_created:
signals.post_delete.send(sender=cls, instance=instance) signals.post_delete.send(sender=cls, instance=instance, using=using)
setattr(instance, cls._meta.pk.attname, None) setattr(instance, cls._meta.pk.attname, None)
if forced_managed: if forced_managed:

View File

@ -5,12 +5,12 @@ class_prepared = Signal(providing_args=["class"])
pre_init = Signal(providing_args=["instance", "args", "kwargs"]) pre_init = Signal(providing_args=["instance", "args", "kwargs"])
post_init = Signal(providing_args=["instance"]) post_init = Signal(providing_args=["instance"])
pre_save = Signal(providing_args=["instance", "raw"]) pre_save = Signal(providing_args=["instance", "raw", "using"])
post_save = Signal(providing_args=["instance", "raw", "created"]) post_save = Signal(providing_args=["instance", "raw", "created", "using"])
pre_delete = Signal(providing_args=["instance"]) pre_delete = Signal(providing_args=["instance", "using"])
post_delete = Signal(providing_args=["instance"]) post_delete = Signal(providing_args=["instance", "using"])
post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"]) post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"])
m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set"]) m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"])

View File

@ -33,9 +33,9 @@ module system.
If you override these methods on your model, you must call the parent class' If you override these methods on your model, you must call the parent class'
methods for this signals to be sent. methods for this signals to be sent.
Note also that Django stores signal handlers as weak references by default, Note also that Django stores signal handlers as weak references by default,
so if your handler is a local function, it may be garbage collected. To so if your handler is a local function, it may be garbage collected. To
prevent this, pass ``weak=False`` when you call the signal's :meth:`~django.dispatch.Signal.connect`. prevent this, pass ``weak=False`` when you call the signal's :meth:`~django.dispatch.Signal.connect`.
pre_init pre_init
-------- --------
@ -113,6 +113,9 @@ Arguments sent with this signal:
``instance`` ``instance``
The actual instance being saved. The actual instance being saved.
``using``
The database alias being used.
post_save post_save
--------- ---------
@ -133,6 +136,9 @@ Arguments sent with this signal:
``created`` ``created``
A boolean; ``True`` if a new record was created. A boolean; ``True`` if a new record was created.
``using``
The database alias being used.
pre_delete pre_delete
---------- ----------
@ -150,6 +156,9 @@ Arguments sent with this signal:
``instance`` ``instance``
The actual instance being deleted. The actual instance being deleted.
``using``
The database alias being used.
post_delete post_delete
----------- -----------
@ -170,6 +179,9 @@ Arguments sent with this signal:
Note that the object will no longer be in the database, so be very Note that the object will no longer be in the database, so be very
careful what you do with this instance. careful what you do with this instance.
``using``
The database alias being used.
m2m_changed m2m_changed
----------- -----------
@ -215,8 +227,8 @@ Arguments sent with this signal:
Sent *after* the relation is cleared Sent *after* the relation is cleared
``reverse`` ``reverse``
Indicates which side of the relation is updated (i.e., if it is the Indicates which side of the relation is updated (i.e., if it is the
forward or reverse relation that is being modified). forward or reverse relation that is being modified).
``model`` ``model``
The class of the objects that are added to, removed from or cleared The class of the objects that are added to, removed from or cleared
@ -228,6 +240,9 @@ Arguments sent with this signal:
For the ``"clear"`` action, this is ``None``. For the ``"clear"`` action, this is ``None``.
``using``
The database alias being used.
For example, if a ``Pizza`` can have multiple ``Topping`` objects, modeled For example, if a ``Pizza`` can have multiple ``Topping`` objects, modeled
like this: like this:
@ -266,6 +281,8 @@ the arguments sent to a :data:`m2m_changed` handler would be:
``Pizza``) ``Pizza``)
``pk_set`` ``[t.id]`` (since only ``Topping t`` was added to the relation) ``pk_set`` ``[t.id]`` (since only ``Topping t`` was added to the relation)
``using`` ``"default"`` (since the default router sends writes here)
============== ============================================================ ============== ============================================================
And if we would then do something like this: And if we would then do something like this:
@ -293,6 +310,8 @@ the arguments sent to a :data:`m2m_changed` handler would be:
``pk_set`` ``[p.id]`` (since only ``Pizza p`` was removed from the ``pk_set`` ``[p.id]`` (since only ``Pizza p`` was removed from the
relation) relation)
``using`` ``"default"`` (since the default router sends writes here)
============== ============================================================ ============== ============================================================
class_prepared class_prepared

View File

@ -7,6 +7,7 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import management from django.core import management
from django.db import connections, router, DEFAULT_DB_ALIAS from django.db import connections, router, DEFAULT_DB_ALIAS
from django.db.models import signals
from django.db.utils import ConnectionRouter from django.db.utils import ConnectionRouter
from django.test import TestCase from django.test import TestCase
@ -1619,3 +1620,114 @@ class PickleQuerySetTestCase(TestCase):
Book.objects.using(db).create(title='Dive into Python', published=datetime.date(2009, 5, 4)) Book.objects.using(db).create(title='Dive into Python', published=datetime.date(2009, 5, 4))
qs = Book.objects.all() qs = Book.objects.all()
self.assertEqual(qs.db, pickle.loads(pickle.dumps(qs)).db) self.assertEqual(qs.db, pickle.loads(pickle.dumps(qs)).db)
class DatabaseReceiver(object):
"""
Used in the tests for the database argument in signals (#13552)
"""
def __call__(self, signal, sender, **kwargs):
self._database = kwargs['using']
class WriteToOtherRouter(object):
"""
A router that sends all writes to the other database.
"""
def db_for_write(self, model, **hints):
return "other"
class SignalTests(TestCase):
multi_db = True
def setUp(self):
self.old_routers = router.routers
def tearDown(self):
router.routser = self.old_routers
def _write_to_other(self):
"Sends all writes to 'other'."
router.routers = [WriteToOtherRouter()]
def _write_to_default(self):
"Sends all writes to the default DB"
router.routers = self.old_routers
def test_database_arg_save_and_delete(self):
"""
Tests that the pre/post_save signal contains the correct database.
(#13552)
"""
# Make some signal receivers
pre_save_receiver = DatabaseReceiver()
post_save_receiver = DatabaseReceiver()
pre_delete_receiver = DatabaseReceiver()
post_delete_receiver = DatabaseReceiver()
# Make model and connect receivers
signals.pre_save.connect(sender=Person, receiver=pre_save_receiver)
signals.post_save.connect(sender=Person, receiver=post_save_receiver)
signals.pre_delete.connect(sender=Person, receiver=pre_delete_receiver)
signals.post_delete.connect(sender=Person, receiver=post_delete_receiver)
p = Person.objects.create(name='Darth Vader')
# Save and test receivers got calls
p.save()
self.assertEqual(pre_save_receiver._database, DEFAULT_DB_ALIAS)
self.assertEqual(post_save_receiver._database, DEFAULT_DB_ALIAS)
# Delete, and test
p.delete()
self.assertEqual(pre_delete_receiver._database, DEFAULT_DB_ALIAS)
self.assertEqual(post_delete_receiver._database, DEFAULT_DB_ALIAS)
# Save again to a different database
p.save(using="other")
self.assertEqual(pre_save_receiver._database, "other")
self.assertEqual(post_save_receiver._database, "other")
# Delete, and test
p.delete(using="other")
self.assertEqual(pre_delete_receiver._database, "other")
self.assertEqual(post_delete_receiver._database, "other")
def test_database_arg_m2m(self):
"""
Test that the m2m_changed signal has a correct database arg (#13552)
"""
# Make a receiver
receiver = DatabaseReceiver()
# Connect it, and make the models
signals.m2m_changed.connect(receiver=receiver)
b = Book.objects.create(title="Pro Django",
published=datetime.date(2008, 12, 16))
p = Person.objects.create(name="Marty Alchin")
# Test addition
b.authors.add(p)
self.assertEqual(receiver._database, DEFAULT_DB_ALIAS)
self._write_to_other()
b.authors.add(p)
self._write_to_default()
self.assertEqual(receiver._database, "other")
# Test removal
b.authors.remove(p)
self.assertEqual(receiver._database, DEFAULT_DB_ALIAS)
self._write_to_other()
b.authors.remove(p)
self._write_to_default()
self.assertEqual(receiver._database, "other")
# Test addition in reverse
p.book_set.add(b)
self.assertEqual(receiver._database, DEFAULT_DB_ALIAS)
self._write_to_other()
p.book_set.add(b)
self._write_to_default()
self.assertEqual(receiver._database, "other")
# Test clearing
b.authors.clear()
self.assertEqual(receiver._database, DEFAULT_DB_ALIAS)
self._write_to_other()
b.authors.clear()
self._write_to_default()
self.assertEqual(receiver._database, "other")