from django.db import models from django.utils import timezone from django.conf import settings class SoftDeleteQuerySet(models.QuerySet): """QuerySet that filters out soft-deleted rows by default.""" def delete(self): # Prevent accidental hard deletes through queryset.delete() for obj in self: obj.delete() def hard_delete(self): return super().delete() def with_deleted(self): """Return all rows, including soft-deleted.""" return super().all() class SoftDeleteManager(models.Manager): """Manager that hides soft-deleted rows by default.""" def get_queryset(self): return SoftDeleteQuerySet(self.model, using=self._db).filter(is_deleted=False) def with_deleted(self): return SoftDeleteQuerySet(self.model, using=self._db) class SoftDeletableModel(models.Model): """ Abstract mixin for soft-deletion with retention window. Objects are hidden by default via SoftDeleteManager. """ is_deleted = models.BooleanField(default=False, db_index=True) deleted_at = models.DateTimeField(null=True, blank=True, db_index=True) restore_until = models.DateTimeField(null=True, blank=True, db_index=True) delete_reason = models.CharField(max_length=255, null=True, blank=True) deleted_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='+', ) objects = SoftDeleteManager() all_objects = models.Manager() DEFAULT_RETENTION_DAYS = 14 class Meta: abstract = True def soft_delete(self, user=None, reason=None, retention_days=None): """Mark the instance as deleted with a retention window.""" if self.is_deleted: return now = timezone.now() if retention_days is None: retention_days = getattr( getattr(self, 'account', None), 'deletion_retention_days', self.DEFAULT_RETENTION_DAYS, ) self.is_deleted = True self.deleted_at = now self.restore_until = now + timezone.timedelta(days=retention_days) self.deleted_by = user if reason: self.delete_reason = reason self.save(update_fields=['is_deleted', 'deleted_at', 'restore_until', 'deleted_by', 'delete_reason']) def restore(self): """Restore a soft-deleted instance if within the retention window.""" if not self.is_deleted: return self.is_deleted = False self.deleted_at = None self.restore_until = None self.delete_reason = None self.deleted_by = None self.save(update_fields=['is_deleted', 'deleted_at', 'restore_until', 'delete_reason', 'deleted_by']) def delete(self, using=None, keep_parents=False): """Override delete to perform a soft delete.""" self.soft_delete() def hard_delete(self, using=None, keep_parents=False): """Irrevocably delete the row.""" return super().delete(using=using, keep_parents=keep_parents)