Skip to main content

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

django_undo_revision-3.0.0.tar.gz (28.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_undo_revision-3.0.0-py3-none-any.whl (11.9 kB view details)

Uploaded Python 3

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

Hashes for django_undo_revision-3.0.0.tar.gz
Algorithm Hash digest
SHA256 cc397183e9bce09e0edef5804e7d4b650ff5fcc9f228adeb27effa0322ffb570
MD5 f8177cd14c4e79dc66678b5fcc38ec1a
BLAKE2b-256 95a6f9f3a7c28d2c259938bb1147871156a6071cab12e7b6d35926aacd807add

See more details on using hashes here.

File details

Details for the file django_undo_revision-3.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_undo_revision-3.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 712b5f7ca6948eebb684742b3fb89ec018232ae6546e150166261b291f8c55fa
MD5 854ad98edf553e4b1ffc0629b8ec9acb
BLAKE2b-256 0050884535abf99cce3aab70aa52843c67bb089fff60141cbff209f0f188bd7f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page