Soft Delete¶
Replace hard deletes with a soft delete flag — keep your data safe and recoverable.
Overview¶
The SoftDeleteViewSetMixin replaces hard deletes with a boolean flag, automatically excludes soft-deleted records from queries, and provides restore and permanent delete endpoints.
What changes when you add the mixin:
| Endpoint | Without Mixin | With Mixin |
|---|---|---|
DELETE /{pk}/ |
Row removed from DB | Sets is_deleted=True |
DELETE /bulk/ |
Rows removed from DB | Sets is_deleted=True on all |
GET / (list) |
All records | Excludes is_deleted=True |
GET /{pk}/ |
Any record | 404 if is_deleted=True |
PATCH /{pk}/ |
Any record | 404 if is_deleted=True |
POST /{pk}/restore |
— | Restores soft-deleted record |
DELETE /{pk}/hard-delete |
— | Permanently removes record |
Step 1: Add the Field to Your Model¶
The mixin requires a BooleanField on your model. Add it yourself — the mixin does not create it for you:
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
is_deleted = models.BooleanField(default=False) # (1)!
- The field name defaults to
is_deletedbut is configurable viasoft_delete_field.
Don't forget to run makemigrations and migrate after adding the field.
Step 2: Add the Mixin to Your ViewSet¶
from ninja_aio.views import APIViewSet
from ninja_aio.views.mixins import SoftDeleteViewSetMixin
class ArticleAPI(SoftDeleteViewSetMixin, APIViewSet):
model = Article
That's it. All delete operations now soft-delete, and soft-deleted records are hidden from list/retrieve/update.
Configuration¶
Custom Field Name¶
If your boolean field is named differently:
class ArticleAPI(SoftDeleteViewSetMixin, APIViewSet):
model = Article
soft_delete_field = "deleted" # (1)!
- Must match the actual
BooleanFieldname on the model.
Admin View (Include Deleted Records)¶
For admin viewsets that need to see and manage soft-deleted records:
class ArticleAdminAPI(SoftDeleteViewSetMixin, APIViewSet):
model = Article
include_deleted = True # (1)!
- Soft-deleted records appear in list, can be retrieved and updated.
Endpoints¶
Soft Delete¶
Sets is_deleted=True on the record. The row stays in the database. Returns 204 No Content.
Soft-deleting an already soft-deleted record is idempotent — no error.
Bulk Soft Delete¶
Uses a single UPDATE ... SET is_deleted=True WHERE pk IN (...) query. Returns the standard BulkResultSchema with partial success semantics.
Restore¶
Sets is_deleted=False and returns the serialized object. Uses patch_auth for authorization.
Hard Delete¶
Permanently removes the record from the database. Returns 204 No Content. Uses delete_auth for authorization.
Composability¶
The mixin works with all other mixins. Order matters — put SoftDeleteViewSetMixin first:
class ArticleAPI(
SoftDeleteViewSetMixin,
PermissionViewSetMixin,
IcontainsFilterViewSetMixin,
APIViewSet,
):
model = Article
query_params = {"title": (str, None)}
async def has_permission(self, request, operation):
if operation in ("hard_delete", "restore"):
return request.auth.is_staff
return True
The hooks chain via super():
- Soft delete filters out
is_deleted=True - Permissions filter by user role
- Filters apply query parameters
Validation¶
The mixin validates at initialization that the model has the configured field. If the field is missing, a clear error is raised immediately:
django.core.exceptions.ImproperlyConfigured:
Article does not have a 'is_deleted' field.
Add a BooleanField or set soft_delete_field to the correct name.
Attributes Reference¶
| Attribute | Type | Default | Description |
|---|---|---|---|
soft_delete_field |
str |
"is_deleted" |
Name of the BooleanField on the model |
include_deleted |
bool |
False |
If True, soft-deleted records are visible in list/retrieve/update |