Search code examples
pythondjangomany-to-manydjango-signals

Prevent m2m_changed from firing when creating an object


When using a Django signal like post_save you can prevent it from firing when an object is first created by doing something like:

@receiver(post_save,sender=MyModel)
def my_signal(sender, instance, created,**kwargs):
    if not created:
        pass # Do nothing, as the item is new.
    else:
        logger.INFO("The item changed - %s"%(instance) )

However, ManyToMany relations are applied after an item is initially created, so no such argument is passed in, making it difficult to suppress in these cases.

@receiver(m2m_changed,sender=MyModel.somerelation.though)
def my_signal(sender, instance, created,**kwargs):
    if __something__: # What goes here?
        pass # Do nothing, as the item is new.
    else:
        logger.INFO("The item changed - %s"%(instance) )

Is there an easy way to suppress an m2m_changed signal when its being done on an object that has just been created?


Solution

  • I think there is no easy way to do that.

    As the Django doc says, you can't associate an item with a relation until it's been saved. Example from the doc:

    >>> a1 = Article(headline='...')
    >>> a1.publications.add(p1)
    Traceback (most recent call last):
    ...
    ValueError: 'Article' instance needs to have a primary key value before a many-to-many relationship can be used.
    
    # should save Article first
    >>> a1.save()
    # the below statement never know it's just following a creation or not
    >>> a1.publications.add(p1)
    

    It's logically not possible for a relation record to know whether it is added to "a just created item" or "an item that already exists for some time", without external info.

    Some workarounds I came up with:

    Solution 1. add a DatetimeField in MyModel to indicate creation time. m2m_changed handler uses the creation time to check when is the item created. It work practically in some cases, but cannot guarantee correctness

    Solution 2. add a 'created' attribute in MyModel, either in a post_save handler or in other codes. Example:

    @receiver(post_save, sender=Pizza)
    def pizza_listener(sender, instance, created, **kwargs):
        instance.created = created
    
    @receiver(m2m_changed, sender=Pizza.toppings.through)
    def topping_listener(sender, instance, action, **kwargs):
        if action != 'post_add':
            # as example, only handle post_add action here
            return
        if getattr(instance, 'created', False):
            print 'toppings added to freshly created Pizza'
        else:
            print 'toppings added to modified Pizza'
        instance.created = False
    

    Demo:

    p1 = Pizza.objects.create(name='Pizza1')
    p1.toppings.add(Topping.objects.create())
    >>> toppings added to freshly created Pizza
    p1.toppings.add(Topping.objects.create())
    >>> toppings added to modified Pizza
    
    p2 = Pizza.objects.create(name='Pizza2')
    p2.name = 'Pizza2-1'
    p2.save()
    p2.toppings.add(Topping.objects.create())
    >>> toppings added to modified Pizza
    

    But be careful using this solution. Since 'created' attribute was assigned to Python instance, not saved in DB, things can go wrong as:

    p3 = Pizza.objects.create(name='Pizza3')
    p3_1 = Pizza.objects.get(name='Pizza3')
    p3_1.toppings.add(Topping.objects.create())
    >>> toppings added to modified Pizza
    p3.toppings.add(Topping.objects.create())
    >>> toppings added to freshly created Pizza
    

    That's all about the answer. Then, caught you here! I'm zhang-z from github django-notifications group :)