Skip to main content

Overview

Django Simple History automatically tracks changes to your Django models, creating a complete audit trail of all modifications. Unfold’s integration provides a beautifully styled history interface that seamlessly integrates with your admin panel.
Every change to a tracked model is recorded with information about who made the change, when it occurred, and what was modified.

Installation

1

Install django-simple-history

Install the package using pip:
pip install django-simple-history
2

Add to INSTALLED_APPS

Add both unfold.contrib.simple_history and simple_history to your settings. Order matters:
settings.py
INSTALLED_APPS = [
    "unfold",
    "unfold.contrib.simple_history",  # After unfold, before simple_history
    
    "django.contrib.admin",
    "django.contrib.auth",
    # ...
    
    "simple_history",
]
3

Run migrations

Create history tables for your models:
python manage.py makemigrations
python manage.py migrate

Basic Usage

Adding History to Models

Add history tracking to your models:
models.py
from django.db import models
from simple_history.models import HistoricalRecords

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
    published = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    # Add history tracking
    history = HistoricalRecords()
    
    def __str__(self):
        return self.title
Adding HistoricalRecords() automatically creates a historical version table that mirrors your model’s structure.

Configuring Admin

Inherit from both ModelAdmin and SimpleHistoryAdmin:
admin.py
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from unfold.admin import ModelAdmin
from .models import Article

@admin.register(Article)
class ArticleAdmin(ModelAdmin, SimpleHistoryAdmin):
    list_display = ["title", "author", "published", "updated_at"]
    list_filter = ["published", "created_at"]
    search_fields = ["title", "content"]
    
    # Optional: customize history display
    history_list_display = ["title", "published"]

Advanced Configuration

Custom History User

Specify custom user field for tracking:
models.py
class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    modified_by = models.ForeignKey("auth.User", on_delete=models.SET_NULL, null=True)
    
    history = HistoricalRecords(
        history_user_id_field="modified_by",
        history_user_setter=lambda instance, user: instance.modified_by
    )

Excluding Fields from History

Omit certain fields from history tracking:
models.py
class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    view_count = models.IntegerField(default=0)  # Don't track this
    last_viewed = models.DateTimeField(null=True)  # Don't track this
    
    history = HistoricalRecords(
        excluded_fields=["view_count", "last_viewed"]
    )

Custom History Table

Specify a custom table name:
models.py
class Article(models.Model):
    title = models.CharField(max_length=200)
    
    history = HistoricalRecords(
        table_name="article_audit_log"
    )

Tracking Foreign Keys

Configure how foreign keys are tracked:
models.py
class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    text = models.TextField()
    author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
    
    history = HistoricalRecords(
        # Store foreign key values instead of IDs
        inherit=True
    )

Querying History

Access Historical Records

Query the history of an object:
# Get an article
article = Article.objects.get(id=1)

# Access all historical records
history = article.history.all()

# Get the most recent change
latest = article.history.first()

# Get history at a specific time
from datetime import datetime
history_at_date = article.history.as_of(datetime(2024, 1, 1))

# Get changes by a specific user
user_changes = article.history.filter(history_user=user)

Comparing Versions

Compare different versions of a record:
# Get two versions
version_1 = article.history.all()[0]
version_2 = article.history.all()[1]

# Get changed fields
delta = version_1.diff_against(version_2)

for change in delta.changes:
    print(f"{change.field} changed from {change.old} to {change.new}")

Reverting Changes

Restore a previous version:
# Get a historical version
old_version = article.history.all()[5]

# Revert to this version
old_version.instance.save()

History Display Features

History Button

Unfold automatically adds a “History” button to change forms for models with history tracking:
History button

History List View

The history page displays:
  • Timestamp of each change
  • User who made the change
  • Type of change (Created, Changed, Deleted)
  • Fields that were modified
  • Ability to view and compare versions
Click on any historical record to view the complete state of the object at that point in time.

Middleware Configuration

Automatically track the current user making changes:
settings.py
MIDDLEWARE = [
    # ...
    "simple_history.middleware.HistoryRequestMiddleware",
]
With this middleware, history records will automatically capture the user from the request:
views.py
# User is automatically tracked
article.title = "Updated Title"
article.save()  # History record includes current user

Admin Customization

Custom History Display

Customize which fields appear in history:
admin.py
@admin.register(Article)
class ArticleAdmin(ModelAdmin, SimpleHistoryAdmin):
    # Show only these fields in history list
    history_list_display = ["title", "published", "author"]
    
    # Add filters to history view
    history_list_filter = ["published"]

History Field Formatting

Format how fields appear in history:
admin.py
class ArticleAdmin(ModelAdmin, SimpleHistoryAdmin):
    def history_view(self, request, object_id, extra_context=None):
        extra_context = extra_context or {}
        extra_context["title"] = "Article Change History"
        return super().history_view(request, object_id, extra_context)

Performance Considerations

Index Historical Tables

Add indexes for better query performance:
models.py
class Article(models.Model):
    title = models.CharField(max_length=200)
    
    history = HistoricalRecords()
    
    class Meta:
        indexes = [
            models.Index(fields=["-updated_at"]),
        ]

Limit History Retention

Clean up old history records:
management/commands/clean_history.py
from django.core.management.base import BaseCommand
from django.utils import timezone
from datetime import timedelta
from myapp.models import Article

class Command(BaseCommand):
    def handle(self, *args, **options):
        # Delete history older than 1 year
        cutoff_date = timezone.now() - timedelta(days=365)
        Article.history.filter(history_date__lt=cutoff_date).delete()

Bulk Operations

Disable history for bulk operations:
from simple_history.utils import bulk_create_with_history, bulk_update_with_history

# Bulk create with history
articles = [Article(title=f"Article {i}") for i in range(100)]
bulk_create_with_history(articles, Article)

# Skip history for bulk operations
Article.history.skip_history = True
Article.objects.filter(published=False).update(published=True)
Article.history.skip_history = False

Use Cases

Maintain complete audit trails for regulatory compliance:
# Track all changes to sensitive data
class PatientRecord(models.Model):
    # ... fields ...
    history = HistoricalRecords()

# Generate audit reports
changes = PatientRecord.history.filter(
    history_date__gte=start_date,
    history_date__lte=end_date
)
Implement version control for content:
# Track document versions
article = Article.objects.get(id=1)
versions = article.history.all()

# Revert to previous version
previous_version = versions[5]
previous_version.instance.save()
Track down when and how bugs were introduced:
# Find when a field changed
changes = article.history.filter(
    content__contains="bug"
)

# See who made the change
for change in changes:
    print(f"Changed by {change.history_user} at {change.history_date}")
Recover accidentally deleted or modified data:
# Find deleted records
deleted = Article.history.filter(history_type="-")

# Restore deleted record
if deleted.exists():
    deleted.first().instance.save()

Live Demo

View History Tracking

Explore the history interface with real data and see change tracking in action

Resources

Simple History Docs

Complete documentation and API reference

GitHub Repository

Source code and community discussions
Historical tables can grow large over time. Implement a retention policy and regular cleanup for production environments.

Build docs developers (and LLMs) love