You we're on the right track when using the m2m_changed signal.
Your problem is that when responding to the post_clear
signal the tags have already been deleted so you won't be able to access them like that.
You actually need to dispatch your method before the tags are deleted, which means handling the pre_clear
signal.
Something like this:
@receiver(m2m_changed, sender=Files.tags.through)
def handle_tags(sender, **kwargs):
action = kwargs['action']
if action == "pre_clear":
tags_pk_set = kwargs['instance'].tags.values_list('pk')
elif action == "pre_remove":
tags_pk_set = kwargs.get('pk_set')
else:
return
# I'm using Count() just so I don't have to iterate over the tag objects
annotated_tags = Tags.objects.annotate(n_files=Count('files'))
unreferenced = annotated_tags.filter(pk__in=tags_pk_set).filter(n_files=1)
unreferenced.delete()
I've also added the handling of the pre_remove
signal in which you can use the pk_set
argument to get the actual tags that will be removed.
UPDATE
Of course the previous listener won't delete the unreferenced tags when deleting the files, since it's only handling the pre_clear
and pre_remove
signals from the Tags model. In order to do what you want, you should also handle the pre_delete
signal of the Files model.
In the code below I've added an utility function remove_tags_if_orphan
, a slightly modified version of handle_tags
and a new handler called handle_file_deletion
to remove the tags which will become unreferenced once the File is deleted.
def remove_tags_if_orphan(tags_pk_set):
"""Removes tags in tags_pk_set if they're associated with only 1 File."""
annotated_tags = Tags.objects.annotate(n_files=Count('files'))
unreferenced = annotated_tags.filter(pk__in=tags_pk_set).filter(n_files=1)
unreferenced.delete()
# This will clean unassociated Tags when clearing or removing Tags from a File
@receiver(m2m_changed, sender=Files.tags.through)
def handle_tags(sender, **kwargs):
action = kwargs['action']
if action == "pre_clear":
tags_pk_set = kwargs['instance'].tags.values_list('pk')
elif action == "pre_remove":
tags_pk_set = kwargs.get('pk_set')
else:
return
remove_tags_if_orphan(tags_pk_set)
# This will clean unassociated Tags when deleting/bulk-deleting File objects
@receiver(pre_delete, sender=Files)
def handle_file_deletion(sender, **kwargs):
associated_tags = kwargs['instance'].tags.values_list('pk')
remove_tags_if_orphan(associated_tags)
Hope this clears things up.