Skip to content

meteozond/django-permanent

 
 

Repository files navigation

Django Permanent

Yet another approach to provide soft (logical) delete or masking (thrashing) django models instead of deleting them physically from db.

Tests

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+

Setup:

Add django_permanent to your INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    'django_permanent',
]

This enables Django System Checks that warn about problematic configurations.

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:

  1. First try to find a matching object (including soft-deleted ones)
  2. If found and deleted: restore it and update with new kwargs
  3. If found and not deleted: return the existing object
  4. 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

  1. Inherit your query set from PermanentQuerySet:

    class ServerFileQuerySet(PermanentQuerySet)
        pass
  2. Wrap PermanentQuerySet or DeletedQuerySet in 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

  1. Check for existence of the object.
  2. Restore it if it was deleted.
  3. 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

About

Yet another approach to creation of non-deletable and non-removable from db restorable django models.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • Python 100.0%