Django reusable app for revision-based undo functionality with django-simple-history
Project description
django-undo-revision
Atomic undo / rollback for Django models — a reusable Django app that adds a "Ctrl+Z" undo button to your application data. Uses django-simple-history as its change-tracking engine.
pip install django-undo-revision
What problem does it solve?
django-simple-history tracks every change to your models, but it doesn't give you a way to group multiple changes together and roll them all back at once. django-undo-revision adds that layer:
- A user edits a document, renames a tag, and reorders items — all in one request. One
undo_last_revision()call rolls back all three changes atomically. - You're building a collaborative editor, a project management tool, or any app where users need a reliable undo button.
- You want per-scope undo history (per user, per project, per session) rather than a single global history.
How it works
Every mutation is wrapped in a revision — a named unit of work. A revision automatically captures all django-simple-history records (creates, updates, deletes) made within its context and groups them under a single Revision object. Calling undo_last_revision(scope) fetches the latest revision for a scope and replays all changes in reverse order inside a transaction.
open_revision(scope_id=...)
└── saves Document → Version(revision=R, object=doc_history_record)
└── deletes Tag → Version(revision=R, object=tag_history_record)
└── updates Item.order → Version(revision=R, object=item_history_record)
undo_last_revision(scope)
└── restores Item.order (reverse chronological)
└── restores Tag
└── restores Document
└── deletes Revision R
Requirements
| Dependency | Version |
|---|---|
| Python | ≥ 3.11 |
| Django | ≥ 4.2 (tested on 4.2, 5.0, 5.1) |
| django-simple-history | ≥ 3.11.0 |
Installation
pip install django-undo-revision
Add to INSTALLED_APPS (order matters — contenttypes and simple_history must come first):
INSTALLED_APPS = [
...
"django.contrib.contenttypes",
"simple_history",
"undo_revision",
]
Run migrations:
python manage.py migrate
Configuration
In settings.py, set the scope model — the entity that revisions are grouped under. This is typically your top-level container: a project, workspace, document, user, or session.
# Required: the model that owns revisions
UNDO_REVISION_SCOPE_MODEL = "myapp.Project"
# URL kwarg used by UndoRevisionMixin to extract the scope id from the request
UNDO_REVISION_SCOPE_URL_KWARG = "project_id"
# HTTP methods that should automatically open a revision (default: all mutating methods)
UNDO_REVISION_HTTP_METHODS = ["post", "put", "patch", "delete"]
Usage
1. Inherit models from HistoricalModel
from django.db import models
from undo_revision.models import HistoricalModel
class Document(HistoricalModel):
title = models.CharField(max_length=255)
body = models.TextField()
class Tag(HistoricalModel):
name = models.CharField(max_length=100)
document = models.ForeignKey(Document, on_delete=models.CASCADE)
HistoricalModel wires up RevisionHistoricalRecords (an extended HistoricalRecords) and RevisionQuerySet as the default manager.
2. Wrap mutations in a revision
from undo_revision.revision.context import open_revision
with open_revision(scope_id=project.id):
document.title = "New title"
document.save()
tag.delete()
Item.objects.bulk_update_with_history(items, fields=["order"])
# All changes are grouped into one revision.
# If nothing was saved inside the block, the revision is deleted automatically.
You can also attach to an existing revision by id (useful when a single logical action spans multiple functions):
with open_revision(revision_id=existing_revision.id):
...
3. Undo the last revision
from undo_revision.revision.undo import undo_last_revision, RevisionNotFoundError
try:
undo_last_revision(scope=project)
except RevisionNotFoundError:
# No revisions left — nothing to undo
return Response({"detail": "Nothing to undo."}, status=400)
undo_last_revision runs inside a transaction. It rolls back all changes in reverse chronological order, then deletes the revision record.
4. Expose an undo endpoint (DRF example)
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from undo_revision.revision.undo import undo_last_revision, RevisionNotFoundError
class ProjectViewSet(GenericViewSet):
@action(detail=True, methods=["post"], url_path="undo")
def undo(self, request, pk=None):
project = self.get_object()
try:
undo_last_revision(scope=project)
except RevisionNotFoundError:
return Response({"detail": "Nothing to undo."}, status=400)
return Response(status=204)
5. Auto-open revisions via mixin (DRF / CBV)
UndoRevisionMixin automatically wraps all mutating methods with open_revision, so you don't have to add the context manager to every view.
from undo_revision.revision.mixins import UndoRevisionMixin
from rest_framework.viewsets import ModelViewSet
class DocumentViewSet(UndoRevisionMixin, ModelViewSet):
queryset = Document.objects.all()
serializer_class = DocumentSerializer
scope_url_kwarg = "project_id" # URL kwarg carrying the scope id
Every POST, PUT, PATCH, and DELETE request to this viewset will automatically open a revision scoped to project_id.
6. QuerySet update()
QuerySet.update() bypasses Django's post_save signal, so HistoricalModel overrides it to capture history automatically:
# History is saved automatically — no extra call needed
Document.objects.filter(project=project).update(title="New title")
# Skip history tracking explicitly
Document.objects.filter(project=project).update_without_history(title="New title")
7. Bulk operations
RevisionQuerySet exposes helpers for bulk mutations that integrate with revision tracking:
# Tracked — changes will be included in the current revision
MyModel.objects.bulk_create_with_history(objs)
MyModel.objects.bulk_update_with_history(objs, fields=["title", "order"])
# Untracked — changes are invisible to revision/undo
MyModel.objects.bulk_create_without_history(objs)
MyModel.objects.bulk_update_without_history(objs, fields=["title"])
MyModel.objects.create_without_history(title="...")
MyModel.objects.filter(...).delete_without_history()
Use the _without_history variants for seed data, migrations, or internal bookkeeping that shouldn't be undoable.
Data model
Revision
id UUID (PK)
scope FK → your scope model
created_at DateTimeField
Version
id UUID (PK)
revision FK → Revision
content_type FK → ContentType
object_id TextField
content_object GenericForeignKey → historical record (django-simple-history)
Each Version points to a django-simple-history historical record snapshot. On undo, the library reads back the pre-change field values from the snapshot and restores them.
Key features
| Feature | Description |
|---|---|
| Atomic multi-model rollback | Group changes across many models into one revision and revert them all at once |
| Scoped undo history | Each scope (project, user, session) has its own independent undo stack |
QuerySet update() tracking |
filter(...).update(...) saves history automatically, no extra call needed |
| Bulk operation tracking | bulk_create and bulk_update are revision-aware out of the box |
| Zero-change revision cleanup | Revisions with no recorded changes are deleted automatically |
| DRF / CBV integration | Drop-in mixin auto-opens a revision per request |
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 django_undo_revision-3.0.0.tar.gz.
File metadata
- Download URL: django_undo_revision-3.0.0.tar.gz
- Upload date:
- Size: 28.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc397183e9bce09e0edef5804e7d4b650ff5fcc9f228adeb27effa0322ffb570
|
|
| MD5 |
f8177cd14c4e79dc66678b5fcc38ec1a
|
|
| BLAKE2b-256 |
95a6f9f3a7c28d2c259938bb1147871156a6071cab12e7b6d35926aacd807add
|
File details
Details for the file django_undo_revision-3.0.0-py3-none-any.whl.
File metadata
- Download URL: django_undo_revision-3.0.0-py3-none-any.whl
- Upload date:
- Size: 11.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
712b5f7ca6948eebb684742b3fb89ec018232ae6546e150166261b291f8c55fa
|
|
| MD5 |
854ad98edf553e4b1ffc0629b8ec9acb
|
|
| BLAKE2b-256 |
0050884535abf99cce3aab70aa52843c67bb089fff60141cbff209f0f188bd7f
|