[Fixed]-Django – How to save m2m data via post_save signal?

7👍

Having had the same problem, my solution is to use the m2m_changed signal. You can use it in two places, as in the following example.

The admin upon saving will proceed to:

  • save the model fields
  • emit the post_save signal
  • for each m2m:
    • emit pre_clear
    • clear the relation
    • emit post_clear
    • emit pre_add
    • populate again
    • emit post_add

Here you have a simple example that changes the content of the saved data before actually saving it.

class MyModel(models.Model):

    m2mfield = ManyToManyField(OtherModel)

    @staticmethod
    def met(sender, instance, action, reverse, model, pk_set, **kwargs):
        if action == 'pre_add':
            # here you can modify things, for instance
            pk_set.intersection_update([1,2,3]) 
            # only save relations to objects 1, 2 and 3, ignoring the others
        elif action == 'post_add':
            print pk_set
            # should contain at most 1, 2 and 3

m2m_changed.connect(receiver=MyModel.met, sender=MyModel.m2mfield.through)

You can also listen to pre_remove, post_remove, pre_clear and post_clear. In my case I am using them to filter one list (‘active things’) within the contents of another (‘enabled things’) independent of the order in which lists are saved:

def clean_services(sender, instance, action, reverse, model, pk_set, **kwargs):
    """ Ensures that the active services are a subset of the enabled ones.
    """
    if action == 'pre_add' and sender == Account.active_services.through:
        # remove from the selection the disabled ones
        pk_set.intersection_update(instance.enabled_services.values_list('id', flat=True))
    elif action == 'pre_clear' and sender == Account.enabled_services.through:
        # clear everything
        instance._cache_active_services = list(instance.active_services.values_list('id', flat=True))
        instance.active_services.clear()
    elif action == 'post_add' and sender == Account.enabled_services.through:
        _cache_active_services = getattr(instance, '_cache_active_services', None)
        if _cache_active_services:
            instance.active_services.add(*list(instance.enabled_services.filter(id__in=_cache_active_services)))
            delattr(instance, '_cache_active_services')
    elif action == 'pre_remove' and sender == Account.enabled_services.through:
        # de-default any service we are disabling
        instance.active_services.remove(*list(instance.active_services.filter(id__in=pk_set)))

If the “enabled” ones are updated (cleared/removed + added back, like in admin) then the “active” ones are cached and cleared in the first pass (‘pre_clear’) and then added back from the cache after the second pass (‘post_add’).

The trick was to update one list on the m2m_changed signals of the other.

4👍

I can’t see anything wrong with your code, but I’m confused as to why you think the admin should work any different from any other app.

However, I must say I think your model structure is wrong. I think you need to get rid of all those ForeignKey fields, and just have a ManyToMany – but use a through table to keep track of the roles.

class Project(models.Model):
    members = models.ManyToManyField(User, through='ProjectRole')

class ProjectRole(models.Model):
    ROLES = (
       ('SR', 'Sales Rep'),
       ('SM', 'Sales Manager'),
       ('PM', 'Project Manager'),
    )
    project = models.ForeignKey(Project)
    user = models.ForeignKey(User)
    role = models.CharField(max_length=2, choices=ROLES)

0👍

I’ve stuck on situation, when I needed to find latest item from set of items, that connected to model via m2m_field.

Following Saverio’s answer, following code solved my issue:

def update_item(sender, instance, action, **kwargs):
    if action == 'post_add':
        instance.related_field = instance.m2m_field.all().order_by('-datetime')[0]
        instance.save()

m2m_changed.connect(update_item, sender=MyCoolModel.m2m_field.through)

Leave a comment