Django accessing ManyToMany fields from post_save signal
Asked Answered
R

4

52

I have a Django model and I want to modify the object permissions on or just after save. I have tried a few solutions and the post_save signal seemed the best candidate for what I want to do:

    class Project(models.Model):
        title = models.CharField(max_length=755, default='default')
        assigned_to = models.ManyToManyField(
            User, default=None, blank=True, null=True
        )
        created_by = models.ForeignKey(
            User,
            related_name="%(app_label)s_%(class)s_related"
        )


    @receiver(post_save, sender=Project)
    def assign_project_perms(sender, instance, **kwargs):
        print("instance title: "+str(instance.title))
        print("instance assigned_to: "+str(instance.assigned_to.all()))

In this case, when a Project is created, the signal fires and I see the title, but an empty list for the assigned_to field.

How can I access the saved assigned_to data following save?

Regain answered 22/5, 2014 at 0:8 Comment(2)
You should use Concern as the sender object, or change the model name to Project instead Concern.Infusible
Oops - typo. Concern was actually an abstract base class. Edited now.Regain
H
73

You're not going to. M2Ms are saved after instances are saved and thus there won't be any record at all of the m2m updates. Further issues (even if you solve that) are that you're still in a transaction and querying the DB won't get you m2m with proper states anyways.

The solution is to hook into the m2m_changed signal instead of post_save.

https://docs.djangoproject.com/en/dev/ref/signals/#m2m-changed

Your sender then would be Project.assigned_to.through

Hypotonic answered 22/5, 2014 at 1:52 Comment(3)
I have the same issue with a ForeignKey, but apparently there's no signal for that?Sassenach
+1 for identifying the sender (many-to-many_field.through). It is handy in case the m2m relation is not defined with an explicit model.Luftwaffe
It seems that m2m_changed is not fired in the admin. Is that correct? Perhaps relevant: code.djangoproject.com/ticket/16073Loesceke
T
33

If your m2m can be empty (blank=True) you are in a little trouble with m2m_changed, because m2m_changed doesn't fire if m2m wasn't set. You can solve this issue by using post_save and m2m_changed at the same time. But there is one big disadvantage with this method - your code will be executed twice if m2m field isn't empty.

So, you can use transaction's on_commit (Django 1.9+)

Django provides the on_commit() function to register callback functions that should be executed after a transaction is successfully committed.

from django.db import transaction

def on_transaction_commit(func):
    def inner(*args, **kwargs):
        transaction.on_commit(lambda: func(*args, **kwargs))

    return inner

@receiver(post_save, sender=SomeModel)
@on_transaction_commit
def my_ultimate_func(sender, **kwargs):
    # Do things here

Important note: this approach works only if your code calls save(). post_save signal doesn't fire at all in cases when you call only instance.m2m.add() or instance.m2m.set().

Translation answered 29/12, 2017 at 19:37 Comment(2)
I tried using on_commit with post save (the last code you wrote), and the changes in the m2m models still aren't applied.Maraud
@Maraud probably your code doesn't call save() at all and that's why post_save signal doesn't work.Translation
B
2

Use transaction on commit!

from django.db import transaction

@receiver(post_save, sender=Project)
def assign_project_perms(sender, instance, **kwargs):
    transaction.on_commit(lambda: print("instance assigned_to: "+str(instance.assigned_to.all())))
Bothy answered 29/5, 2022 at 12:34 Comment(0)
P
1

here is an example about how to use signal with many to many field (post like and post comments models),

and in my example i have :

  • like model (Intermediary table for User and Post tables) : the user can add 1 record only in Intermediary table for each post , which means (unique_together = ['user_like', 'post_like']) for this type of many to many relations you can use 'm2m_changed' signals ,

  • comment model (Intermediary table for User and Post tables): the user can add many records in Intermediary table for each post , (without unique_together ), for this i just use 'post_save, post_delete' signals , but you can use also 'pre_save, pre_delete' if you like ,

and here is both usage example :

from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver

class Post(models.Model):
    post_user = models.ForeignKey(User,related_name='post_user_related', on_delete=models.CASCADE)
    post_title = models.CharField(max_length=100)
    post_description = models.TextField()
    post_image = models.ImageField(upload_to='post_dir', null=True, blank=True)
    post_created_date = models.DateTimeField(auto_now_add=True)
    post_updated_date = models.DateTimeField(auto_now=True)


    post_comments = models.ManyToManyField(
        User,
        through="Comments",
        related_name="post_comments"
        )

    p_like = models.ManyToManyField(
        User, blank=True,
        through="LikeIntermediary",
        related_name="post_like_rel"
        )

class LikeIntermediary(models.Model):
    user_like = models.ForeignKey(User ,related_name="related_user_like", on_delete=models.CASCADE)
    post_like = models.ForeignKey(Post ,related_name="related_post_like", on_delete=models.CASCADE)
    created =  models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.user_like} - {self.post_like} "

    class Meta:
        unique_together = ['user_like', 'post_like']

@receiver(m2m_changed, sender=LikeIntermediary)
def like_updated_channels(sender, instance, **kwargs):
    print('this m2m_changed receiver is called, the instance is post id', instance.id)



class Comments(models.Model):
    cmt_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="related_comments_user") 
    cmt_post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="related_comments_post")
    cmt_created_date = models.DateTimeField(auto_now_add=True)
    cmt_comment_body = models.TextField()
    cmt_created =  models.DateTimeField(auto_now_add=True)
    cmt_updated =  models.DateTimeField(auto_now=True)

@receiver(post_save, sender=Comments)
def comments_updated_channels(sender, instance, created, **kwargs):
   print('this post_save receiver is called, the instance post id', instance.cmt_post.id)

@receiver(post_delete, sender=Comments)
def comments_deleted_channels(sender, instance, **kwargs):
   print('this post_save receiver is called, the instance post id', instance.cmt_post.id)

notes :

  • the instance with 'm2m_changed' it is a post object .
  • the instance with 'post_save and post_delete' it is a comment object

this is just an example , and change it based on your case/requirements.

i hope this helpful

Plebeian answered 7/6, 2022 at 13:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.