[Django]-Prevent m2m_changed from firing when creating an object

3👍

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 🙂

👤ZZY

0👍

@ZZY’s answer basically helped me realise that this wasn’t possible without storing additional fields. Fortunately, I’m using django-model-utils which includes a TimeStampedModel which includes a created field.

Providing a small enough delta, it was relatively easy to check against the created time when catching the signal.

@receiver(m2m_changed,sender=MyModel.somerelation.though)
def my_signal(sender, instance, created,**kwargs):
    if action in ['post_add','post_remove','post_clear']:
        created = instance.created >= timezone.now() - datetime.timedelta(seconds=5)
        if created:
            logger.INFO("The item changed - %s"%(instance) )

0👍

For an easier and short way of checking in the object is created or not is using the _state.adding attribute:

def m2m_change_method(sender, **kwargs):
    instance = kwargs.pop('instance', None)
    if instance:
       if instance.adding: #created object
          pk_set = list(kwargs.pop('pk_set')) #ids of object added to m2m relation
       else:
          # do something if the instance not newly created or changed
    
  # if you want to check if the m2m objects is new use pk_set query if exists()
m2m_change.connect(m2m_change_method, sender=YourModel.many_to_many_field.through)

Leave a comment