Yet another approach to provide soft (logical) delete or masking (thrashing) django models instead of deleting them physically from db.
Project description
Django Permanent
Yet another approach to provide soft (logical) delete or masking (thrashing) django models instead of deleting them physically from db.
Installation
Install using pip:
pip install django-permanent
Or install from source:
git clone https://github.com/meteozond/django-permanent.git
cd django-permanent
python setup.py install
Requirements:
- Python 3.10+
- Django 4.2+
Quick Start
To create a non-deletable model just inherit it from PermanentModel:
class MyModel(PermanentModel):
pass
It automatically changes delete behaviour to hide objects instead of deleting them:
>>> a = MyModel.objects.create(pk=1)
>>> b = MyModel.objects.create(pk=2)
>>> MyModel.objects.count()
2
>>> a.delete()
>>> MyModel.objects.count()
1
To recover a deleted object just call its restore method:
>>> a.restore()
>>> MyModel.objects.count()
2
Use the force kwarg to enforce physical deletion:
>>> a.delete(force=True) # Will act as the default django delete
>>> MyModel._base_manager.count()
0
Restore on Create
If you want create() to restore deleted objects instead of raising an integrity error on unique constraints, use the restore_on_create option:
class Article(PermanentModel):
title = models.CharField(max_length=100, unique=True)
class Permanent:
restore_on_create = True
How it works:
When restore_on_create = True, calling Model.objects.create(**kwargs) will:
- First try to find a matching object (including soft-deleted ones)
- If found and deleted: restore it and update with new kwargs
- If found and not deleted: return the existing object
- If not found: create a new object
Example:
>>> article = Article.objects.create(title="Django Tips")
>>> article.delete() # Soft delete
>>> Article.objects.count()
0
# Without restore_on_create: would raise IntegrityError
# With restore_on_create: restores the deleted article
>>> article2 = Article.objects.create(title="Django Tips")
>>> article2.pk == article.pk # Same object!
True
>>> Article.objects.count()
1
Note: This feature is most useful for models with unique constraints where you want to "resurrect" deleted objects rather than creating duplicates.
Managers
It changes the default model manager to ignore deleted objects, adding a deleted_objects manager to see them instead:
>>> MyModel.objects.count()
2
>>> a.delete()
>>> MyModel.objects.count()
1
>>> MyModel.deleted_objects.count()
1
>>> MyModel.all_objects.count()
2
>>> MyModel._base_manager.count()
2
Accessing Deleted Related Objects
By default, accessing a foreign key to a deleted object will raise DoesNotExist. Use show_all_context() to access deleted related objects:
from django_permanent.related import show_all_context
# Create models with relationship
parent = ParentModel.objects.create(name="parent")
child = ChildModel.objects.create(parent=parent)
# Soft delete parent
parent.delete()
# Without show_all_context: raises DoesNotExist
child.parent # Raises ParentModel.DoesNotExist
# With show_all_context: can access deleted parent
with show_all_context():
print(child.parent.name) # Works! Returns "parent"
Note: This is useful when you need to access relationships to soft-deleted objects, for example in admin interfaces or audit logs.
QuerySet
The QuerySet.delete method will act as the default django delete, with one exception - objects of models subclassing PermanentModel will be marked as deleted; the rest will be deleted physically:
>>> MyModel.objects.all().delete()
You can still force django query set physical deletion:
>>> MyModel.objects.all().delete(force=True)
Using custom querysets
-
Inherit your query set from
PermanentQuerySet:class ServerFileQuerySet(PermanentQuerySet) pass
-
Wrap
PermanentQuerySetorDeletedQuerySetin you model manager declaration:class MyModel(PermanentModel) objects = MultiPassThroughManager(ServerFileQuerySet, NonDeletedQuerySet) deleted_objects = MultiPassThroughManager(ServerFileQuerySet, DeletedQuerySet) all_objects = MultiPassThroughManager(ServerFileQuerySet, PermanentQuerySet)
Method get_restore_or_create
- Check for existence of the object.
- Restore it if it was deleted.
- Create a new one, if it was never created.
Field name
The default field named is 'removed', but you can override it with the PERMANENT_FIELD variable in settings.py:
PERMANENT_FIELD = 'deleted'
Requirements
- Django 4.2+
- Python 3.10, 3.11, 3.12+
Testing
The project uses GitHub Actions for continuous integration.
Run tests locally using act (GitHub Actions locally):
# Install act (macOS)
brew install act
# Run all tests in parallel
act
# Run specific Python version
act --matrix python-version:3.11
# Run specific Python/Django combination
act --matrix python-version:3.11 --matrix django-version:"Django>=4.2,<5.0"
Run tests directly:
# Install dependencies
pip install "Django>=4.2,<5.0" coverage flake8
# Run linter
flake8 django_permanent
# Run tests with coverage
coverage run runtests.py
coverage report
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file django_permanent-2.0.0.tar.gz.
File metadata
- Download URL: django_permanent-2.0.0.tar.gz
- Upload date:
- Size: 20.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
671fc0b30b07fb0d781014f8173e8191bf26a8bbfb8990b2685e5610c54e899a
|
|
| MD5 |
61d90a42de1c4cea7f1e7092de42cb88
|
|
| BLAKE2b-256 |
5ad252794af389feb0e01cb9f5008c2b7f71d310937073200629727d67978d52
|
Provenance
The following attestation bundles were made for django_permanent-2.0.0.tar.gz:
Publisher:
publish.yml on meteozond/django-permanent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_permanent-2.0.0.tar.gz -
Subject digest:
671fc0b30b07fb0d781014f8173e8191bf26a8bbfb8990b2685e5610c54e899a - Sigstore transparency entry: 927063234
- Sigstore integration time:
-
Permalink:
meteozond/django-permanent@71059fcc79738f7f9fa169f76dd65c81ff58aae7 -
Branch / Tag:
refs/tags/v2.0.0 - Owner: https://github.com/meteozond
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@71059fcc79738f7f9fa169f76dd65c81ff58aae7 -
Trigger Event:
push
-
Statement type: