A model's save() never gets called on ManyToManyField changes, m2m_changed-signal is sent.
    sender = dynamically generated model in m2m-table
    instance = parent
    related_instance = instance being m2m'ed

def m2m_changed_handler(sender, *args, **kwargs):
    """
    A model's save() never gets called on ManyToManyField changes, m2m_changed-signal is sent.
    sender = dynamically generated model in m2m-table
    instance = parent
    related_instance = instance being m2m'ed
    """
    action = kwargs['action']
    instance = kwargs['instance']
    logger.debug("m2m_changed: %s (%s) {%s}"%(sender, args, kwargs))

    bulk = []
    if is_df(instance) and (action in ['post_add', 'post_remove']):
        pk_set = list(kwargs.get('pk_set') or [])
        relation_name = sender._meta.db_table.replace(sender._meta.app_label + '_' + instance.__class__.__name__.lower() + '_', '')
        relations = {k.name:k for k in m2m_relations(instance)}
        field = relations[relation_name]
        for pk in pk_set:
            related_instance = get_relation(relations[relation_name]).objects.get(pk=pk)
            changes = {field.name: {'changed': [pk],
                                    'changed_to_string': six.text_type(related_instance)}}
            # reflect change
            bulk.append(History.objects.add(
                    action=action,
                    changes=changes,
                    model=instance,
                    commit=False,))
            # m2m to reflect on changes
            field = get_field(field)
            changes = {field.name: {'changed': [instance.pk],
                                    'changed_to_string': six.text_type(instance),
                                    'm2mpg': True,}}
            bulk.append(History.objects.add(
                    action='add' if action in ['post_add'] else 'rem',
                    changes=changes,
                    model=related_instance,
                    commit=False,))
    if is_df(instance) and (action in ['pre_clear']):
        # "For the pre_clear and post_clear actions, this is None."
        # TODO: should defer this until post_clear is done to be sure it happened
        # TODO: background job, optional execution
        relations = instance._meta.get_all_related_many_to_many_objects()
        for relation in relations:
            instances = get_m2m_reverse_instances(instance, relation)
            field = get_model_relation_by_instance(kwargs['model'], instance)
            changes = {field.name: {'changed': [instance.pk],
                                    'changed_to_string': six.text_type(instance)}}
            for k in instances:
                bulk.append(History.objects.add(
                        action=action,
                        changes=changes,
                        model=k,
                        commit=False,))

    if bulk:
        History.objects.bulk_create(bulk)