5

How to do something after a Django user model save, including related changes to m2m fields like django.contrib.auth.models.Group?

Situation

I have a custom Django user model and want to trigger some actions after a user instance is - with related changes such as m2m group memberships - successfully saved to the database. The use case here is a Wagtail CMS where I create ProfilePages for each user instance. Depending of the group memberships of the user instance, I need to do something.

Problem

In a custom model save() method, I'm not able to reference the changed group memberships, as m2m's are saved after the saving of the user instance. Even if running my custom function after the super().save() call, new group memberships are not yet available. But I need to get the new group memberships in order to do something depending of the new groups for that user.

What I've tried

[✘] Custom model save()

# file: users/models.py
class CustomUser(AbstractUser):
    super().save(*args, **kwargs)
    do_something()

[✘] Signal post_save

As the above simple save() method did not do the trick, I tried the post_save signal of the user model:

# file users/signals.py
@receiver(post_save, sender=get_user_model())
def handle_profilepage(sender, instance, created, **kwargs):
    action = 'created' if created else 'updated'
    do_something()

... but even here I always get the "old" values from the group membership back.

[✔] Signal: m2m_changed

I learnt that there is a m2m_changed signal with which I could monitor changes of the Users.groups(.through) table.

My following implementation did what I need:

@receiver(m2m_changed, sender=User.groups.through)
def user_groups_changed_handler(sender, instance, **kwargs):
    USER_GROUPS = instance.groups.values_list('name', flat=True)
    if set(USER_GROUPS) & set(settings.PROFILE_GROUPS):
        do_something_because_some_groups_match()
    else:
        do_something_else()

My whishlist

I would happily stay away from signals if there is a chance to solve this problem in the models save() method - but I'm stuck...

tombreit
  • 1,199
  • 8
  • 27
  • Maybe you have to try `django-model-utils`'s [field-tracker](https://django-model-utils.readthedocs.io/en/latest/utilities.html#field-tracker) – martbln May 13 '19 at 21:21
  • Dear @martbln, I would like to stay away from external packages (and signals...) for this purpose , and I think ``FieldTracker`` from ``django-model-utils`` does not track m2m fields (https://github.com/jazzband/django-model-utils/issues/163#issuecomment-78296098) anyway. – tombreit May 14 '19 at 13:01
  • Great question. I have this very same problem. Any update? – Steve Smith May 29 '19 at 20:43
  • I am facing the same problem but my question is why you do not like to use signals? it is not recommended? – ladhari Jul 23 '20 at 12:55

1 Answers1

0

You asked after the dark magic. Tell me, what are you willing to sacrifice to avoid using signals? Would you embrace a greater evil? What if I were to suggest that you could start a new thread and sleep for a few seconds until you thought that the m2m relations were probably saved before doing anything?

from threading import Thread
from time import sleep

# file: users/models.py
class CustomUser(AbstractUser):
    super().save(*args, **kwargs)

    def do_something(obj):
        sleep(3)
        # stuff

    thread = Thread(target = do_something, args = [self], daemon=True)
    thread.start()
kloddant
  • 1,026
  • 12
  • 19