pythondjangodjango-modelsdjango-signals

Django - deleting users with on_delete=models.CASCADE causes problems with signals.post_delete


We are using Django 3.1 for our websites. We have a model, User, which is related to two SiteProfile models for two websites, and is also related to other models such as Friend and UserEmailAddress. All the relations are with on_delete=models.CASCADE. We also have models.signals.post_delete signals for models such as Friend, to update the number of friends a user has, which is saved in one of the SiteProfile models. But the problem is, when deleting a user, the signal is called and then the SiteProfile object is saved, and then I get an exception for django.db.utils.IntegrityError - violations of foreign keys. We used to have the same problem with the UserEmailAddress model (a user's email address), which I fixed with the following code:

def delete(self, *args, **kwargs):
    if ((self.is_staff) or (self.is_superuser)):
        warnings.warn('Can’t delete staff user.')
        return False
    else:
        with transaction.atomic():
            for user_email_address in self.email_addresses.all():
                user_email_address.delete()
            return_value = super().delete(*args, **kwargs)
        return return_value

But I would like to know, is there a better way to delete a user with all its related objects which have on_delete=models.CASCADE, but without saving the counters in models.signals.post_delete to the database? Can I check something in models.signals.post_delete to know if the user is being deleted and then don't save the counters? Since I don't want to specifically delete any related object in the def delete method - there are at least 5 such models not including the two SiteProfile models.


Solution

  • Django 4.1 and later

    Django 4.1 introduced a feature where post_delete signals now dispatch origin of deletion.

    @receiver(signal=models.signals.post_delete, sender=Friend)
    def update_all_friends_count_on_unfriend(sender, instance: Friend, **kwargs):
        user = instance.to_user
        # Check origin for not cascade delete User -> Friend
        if (user != kwargs.get('origin')):
            user.speedy_net_profile.all_friends_count = user.friends.count()
            user.speedy_net_profile.save()
    

    Before Django 4.1

    @receiver(signal=models.signals.post_delete, sender=Friend)
    def update_all_friends_count_on_unfriend(sender, instance: Friend, **kwargs):
        from speedy.net.accounts.models import SiteProfile as SpeedyNetSiteProfile
        user = instance.to_user
        # Do .filter(...).update(...) because for cascade delete User -> Friend, accessing user.speedy_net_profile will re-create deleted SpeedyNetSiteProfile
        SpeedyNetSiteProfile.objects.filter(user=user).update(all_friends_count=user.friends.count())