In the Django admin I sometimes add or delete users to or from (existing) groups. When this happens I'd like to be able to run a function.
I'm just using the standard User and Group models.
I have looked at doing it with signals, through m2m_changed, but it seems to need a Through class - and I don't think there is one in this case.
From the django doc:
sender - The intermediate model class describing the ManyToManyField. This class is automatically created when a many-to-many field is defined; you can access it using the through attribute on the many-to-many field.
When subscribing to m2m_changed like so:
@receiver(m2m_changed)
def my_receiver(**kwargs):
from pprint import pprint
pprint(kwargs)
You will receive a bunch of signals like this (shortened):
{'sender': <class 'django.contrib.auth.models.User_groups'>,
'action': 'post_add',
'instance': <User: bouke>,
'model': <class 'django.contrib.auth.models.Group'>,
'pk_set': set([1]),
'reverse': False,
'signal': <django.dispatch.dispatcher.Signal object at 0x101840210>,
'using': 'default'}
So the user bouke
has been added to pk_set
groups: [1]
. However I noted that the admin layout clears all groups and then adds the selected groups back in. The signals you will receive are pre_clear
, post_clear
, pre_add
, post_add
. Using a combination of these signals you could store the pre and post groups. Doing a diff over these lists, you have the deleted and added groups for the user.
Note that the signals are the other way around (pk_set
and instance
) when editing a group instead of a user.
You need to create a signal using m2m_changed
as a receiver. According to the official Django documentation:
A signal is sent when a ManyToManyField
is changed on a model instance. Strictly speaking, this is not a model signal since it is sent by the ManyToManyField
.
So, the simplest implementation is as follows:
@receiver(m2m_changed)
def signal_handler(**kwargs):
from pprint import pprint
pprint(kwargs)
In your case, you want to perform something when a user is added or removed from a group, so you can take advantage of the action
parameter when it takes the values 'pre_add'
, 'post_add'
, 'pre_remove'
, and 'post_remove'
. You can also take advantage of pk_set
parameter which contains primary key values that have been added to or removed from the relation.
@receiver(m2m_changed)
def signal_handler_when_user_is_added_or_removed_from_group(action, instance, pk_set, model, **kwargs):
if model == Group:
if action == 'pre_add':
# TODO: add logic
pass
elif action == 'post_add':
# TODO: add logic
pass
# handle as many actions as one needs
# The following for loop prints every group that were
# added/removed.
for pk in pk_set:
group = Group.objects.get(id=pk)
print(group)
You'll see in the Django documentation (v1.11) that your desired sender should be the intermediate through
field belonging to the ManyToMany
field, wherever that's defined. If you register that as your sender, then you'll be listening to eg Users adding Groups to themselves, as well as Groups adding Users to themselves.
self.walrus.groups.remove(self.peon_group)
@receiver(signal=m2m_changed, sender=User.groups.through)
def adjust_group_notifications(instance, action, reverse, model, pk_set, using, *args, **kwargs):
if model == Group and not reverse:
logger.info("User %s is modifying their relation to groups «%s»", instance.username, pk_set)
…
# Walrus example fits here
else:
logger.info("Group %s is modifying its relation to users «%s»", instance, pk_set)
…
return
It might be better to try and achieve this with django-celery
, that way you can write custom tasks, and based on a certain criteria (such as removal or addition) you can fire of a certain task.