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
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_atfield automatically added to your models;- Ready-to-use managers:
objects→ filters out deleted records;everything→ returns all records (including deleted);
soft_delete()andrestore()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
objects→ alive only (deleted_at IS NULL)everything→ all (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
SoftDeleteAdminprovides:
- filter
SoftDeletedAtFilter(Not Deleted / Deleted);- actions “Soft delete selected” and “Restore selected”;
get_querysetusingeverythingso 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 keepdelete()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;
- Soft delete selected: calls
get_querysetusesmodel.everything.all()so admin shows all records.
Tip: you can remove Django’s default
delete_selectedaction to avoid accidental hard deletes.
Tests
See the tests/ folder in this repo (pytest + pytest-django), covering:
- Managers (
objectsvseverything); soft_delete()andrestore();- 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9a71db0d2c6325a329e6860bc42bfe0edcd85bfad898148e079e30c0a4fc11d4
|
|
| MD5 |
c32bd31324d50debf94afca1ca9128c3
|
|
| BLAKE2b-256 |
8eb50c5d713951ebb36e03bdea48da686fccbbc1e6ad99e25866254cb5ec343a
|
File details
Details for the file duck_django_soft_delete-0.1.7-py3-none-any.whl.
File metadata
- Download URL: duck_django_soft_delete-0.1.7-py3-none-any.whl
- Upload date:
- Size: 7.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.3 CPython/3.13.4 Darwin/24.0.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e5d913b6be623433a01cf8bcfcd00ab8fa36a1a565b64b48e1f9292f626e5472
|
|
| MD5 |
5227ea5067f7a3fe0367bd74ee09e126
|
|
| BLAKE2b-256 |
4695a8a0abb0c11cee3e88c20b17385c0a13aded4d14b55d4fb5313579c2ea5f
|