Skip to main content

A Django framework that provides soft delete functionality with automatic query filtering, admin integration, and safe relation prefetching for models using a deleted_at timestamp.

Project description

DuckDI Logo

duck-django-soft-delete

Pragmatic soft delete framework for Django.
It provides a safe and transparent way to "delete" records by marking them with a timestamp instead of permanently removing them from the database.

This approach is useful when you need:

  • Data recovery after accidental deletions;
  • Historical/audit tracking;
  • To keep database integrity while still hiding records from normal queries;
  • To allow reusing unique keys (with conditional uniqueness).

Features

  • deleted_at field automatically added to your models;
  • Ready-to-use managers:
    • objects → filters out deleted records;
    • everything → returns all records (including deleted);
  • soft_delete() and restore() methods;
  • Admin integration: filter by deletion status + actions for Soft Delete and Restore;
  • Helper build_prefetches() to automatically prefetch only alive related records;
  • Supports partial unique constraints in Postgres (unique only among alive records).

Installation

pip install duck-django-soft-delete

Add to your project and import:

# settings.py
INSTALLED_APPS = [
  # ...
  "duck_django_soft_delete",
]

Concept

Abstract base

from duck_django_soft_delete.table.soft_delete_table import SoftDeleteTable

class MyModel(SoftDeleteTable):
    # your fields...

SoftDeleteTable defines:

  • Model time stamp fields: created_at, updated_at
  • Soft delete model field: deleted_at
  • Django, soft delete and restore model methods: soft_delete() / restore()
  • Django prefetch_related method for soft delete build_prefetches(relations: list[str]) -> list[Prefetch|str]

Managers

  • objectsalive only (deleted_at IS NULL)
  • everythingall (alive and deleted)

Real example

Model with conditional uniqueness (Postgres)

from duck_django_soft_delete.table.soft_delete_table import SoftDeleteTable
from django.db import models
from uuid import uuid4

class Item(SoftDeleteTable):
    uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True, null=True, default="")
    is_public = models.BooleanField(default=True)
    
    foreign_key_exemple = models.ForeignKey(
		"foreign_key_exemple.ForeignKeyExemple",
		on_delete=models.CASCADE,
		db_column="foreign_key_exemple_id",
		related_name="foreign_key_exemples",
	)
	
	class Meta(SoftDeleteTable.Meta):
		db_table = "items"

Admin

from duck_django_soft_delete.admin.soft_delete_admin import SoftDeleteAdmin
from django.contrib import admin
from item import Item

@admin.register(Item)
class ItemAdmin(SoftDeleteAdmin):
    list_display = (
        "uuid", 
        "name",
        "is_public",
        "created_at", 
        "updated_at", 
        "deleted_at",
    )
    list_filter = [
	    *SoftDeleteAdmin.list_filter, 
	    "is_public", 
	    "foreign_key_exemple",
	    "updated_at",
	]
    search_fields = ("name", "is_public", "foreign_key_exemple__id", )

    # ... your @admin.display helpers

SoftDeleteAdmin provides:

  • filter SoftDeletedAtFilter (Not Deleted / Deleted);
  • actions “Soft delete selected” and “Restore selected”;
  • get_queryset using everything so admin sees all records.

Usage

obj = Item.objects.create(name="X")
obj.soft_delete()           # sets deleted_at = now()
obj.restore()               # clears deleted_at
Item.objects.all()      # only alive
Item.everything.all()   # all

Prefetch alive-only relations

If you want to prefetch only alive children when the related model also inherits SoftDeleteTable:

qs = Item.objects.all().prefetch_related(
    *Item.build_prefetches(["foreign_key_exemples", ])
)
  • If the relation inherits SoftDeleteTable, prefetch uses its alive manager.
  • If it does not, falls back to standard Django prefetch.

Best practices

  • Postgres: use UniqueConstraint(..., condition=Q(deleted_at__isnull=True)) for uniqueness only on alive records.
	class Meta(SoftDeleteTable.Meta):
		db_table = "items"
		constraints = [
			models.UniqueConstraint(
				fields=["foreign_key_exemple", "name"],
				condition=Q(deleted_at__isnull=True),
				name="unique_item_name_per_foreign_key_exemple_not_deleted",
			)
		]
  • Document whether you override Model.delete() to do soft delete by default (this lib does not enforce — you can choose to keep delete() as hard delete).

Compatibility

  • Django: 4.2+
  • Python: 3.10+

Admin — details

  • Filter: “Soft Delete” → Not Deleted / Deleted
  • Actions:
    • Soft delete selected: calls soft_delete() on alive items;
    • Restore selected: calls restore() on deleted items;
  • get_queryset uses model.everything.all() so admin shows all records.

Tip: you can remove Django’s default delete_selected action to avoid accidental hard deletes.


Tests

See the tests/ folder in this repo (pytest + pytest-django), covering:

  • Managers (objects vs everything);
  • soft_delete() and restore();
  • Partial unique (Postgres);
  • Admin actions;
  • build_prefetches().

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

duck_django_soft_delete-0.1.7.tar.gz (4.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

duck_django_soft_delete-0.1.7-py3-none-any.whl (7.0 kB view details)

Uploaded Python 3

File details

Details for the file duck_django_soft_delete-0.1.7.tar.gz.

File metadata

  • Download URL: duck_django_soft_delete-0.1.7.tar.gz
  • Upload date:
  • Size: 4.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.1.3 CPython/3.13.4 Darwin/24.0.0

File hashes

Hashes for duck_django_soft_delete-0.1.7.tar.gz
Algorithm Hash digest
SHA256 9a71db0d2c6325a329e6860bc42bfe0edcd85bfad898148e079e30c0a4fc11d4
MD5 c32bd31324d50debf94afca1ca9128c3
BLAKE2b-256 8eb50c5d713951ebb36e03bdea48da686fccbbc1e6ad99e25866254cb5ec343a

See more details on using hashes here.

File details

Details for the file duck_django_soft_delete-0.1.7-py3-none-any.whl.

File metadata

File hashes

Hashes for duck_django_soft_delete-0.1.7-py3-none-any.whl
Algorithm Hash digest
SHA256 e5d913b6be623433a01cf8bcfcd00ab8fa36a1a565b64b48e1f9292f626e5472
MD5 5227ea5067f7a3fe0367bd74ee09e126
BLAKE2b-256 4695a8a0abb0c11cee3e88c20b17385c0a13aded4d14b55d4fb5313579c2ea5f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page