3👍
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.
0👍
Just to sum up with hopefully a cleaner answer:
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
class Tags(models.Model):
name = models.CharField(max_length=100)
user = models.ForeignKey(User)
class Files(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=100)
tags = models.ManyToManyField(Tags, null=True, blank=True)
@receiver(m2m_changed, sender=Files.tags.through)
def delete_orphean_dateranges(sender, **kwargs):
if kwargs['action'] == 'post_remove':
Tags.objects.filter(pk__in=kwargs['pk_set'], files_set=None).delete()
post_remove
ensure that the callback is fired when a Tag was disassociated from a File
- [Answered ]-How to allow for complex URLs on a Django-rest-framework viewset
- [Answered ]-Empty QueryDict in ajax POST query – Django
- [Answered ]-Sphinx fails when generating documentation for Django project
- [Answered ]-Download uploaded files from Django Admin
- [Answered ]-Function for every a django-model
-1👍
I think you go deeper than it required. Just define related_name for Tag, and process post_save signal from File.
class Files(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=100)
tags = models.ManyToManyField(Tags, null=True, blank=True, related_name='files')
def clean_empty_tags(sender, instance, *args, **kwargs):
Tags.objects.filter(user=instance.user, files=None).delete()
post_save.connect(clean_empty_tags, sender=Files)
- [Answered ]-Django(-CMS) Userwarning: No registered apphook
- [Answered ]-'User' object has no attribute '__getitem__' error
- [Answered ]-Django-autocomplete-url invalid URL
- [Answered ]-Google Maps Geolocate API call returns invalidRequest on web server but not locally