Django PG History
django-pghistory is a Django library that provides simple, powerful, and performant history tracking for Django models using PostgreSQL's event triggers. It leverages database-level features for efficiency, making it suitable for auditing and versioning. The current stable version is 3.9.2, and it maintains a regular release cadence with several minor versions and patches throughout the year.
Warnings
- breaking Python 3.9 support was dropped in version 3.9.0. Ensure your project is running Python 3.10 or newer.
- gotcha Installing `pghistory.admin` in `INSTALLED_APPS` registers the admin automatically for all tracked models. If you need custom admin registration for history models, you should not include `pghistory.admin` and register them manually using `pghistory.admin.site.register`.
- gotcha Context tracking in `django-pghistory` can be ignored for certain SQL statements (e.g., `VACUUM`, `SELECT` without `FOR UPDATE`). This is by design to prevent issues or track irrelevant changes, but it means certain database operations will not generate context events.
- gotcha Upgrading existing tracking models to denormalized context requires specific migration steps. Simply changing tracking configuration won't automatically update existing history tables.
- gotcha Statement-level history tracking (introduced in 3.6.0) significantly changes how triggers work, especially for bulk operations. While it offers performance improvements, it may alter the granularity or content of recorded history events compared to row-level tracking.
Install
-
pip install django-pghistory 'psycopg[binary]' django
Imports
- track
import pghistory
- context
import pghistory
- get_event_model
import pghistory
- ProxyField
import pghistory.models
- admin
from django.contrib import admin; admin.site.register(MyHistoryModel)
import pghistory.admin
Quickstart
import os
import django
from django.conf import settings
from django.db import models
settings.configure(
DEBUG=True,
INSTALLED_APPS=[
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'pghistory',
'pghistory.admin',
'your_app_name' # Replace with your actual app name
],
DATABASES={
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('PG_DB_NAME', 'test_db'),
'USER': os.environ.get('PG_DB_USER', 'test_user'),
'HOST': os.environ.get('PG_DB_HOST', 'localhost'),
'PORT': os.environ.get('PG_DB_PORT', '5432'),
'PASSWORD': os.environ.get('PG_DB_PASSWORD', 'password'),
}
},
MIDDLEWARE_CLASSES=(
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
),
TEMPLATES=[
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
],
STATIC_URL = '/static/',
SECRET_KEY = 'a-very-secret-key',
)
django.setup()
import pghistory
# Define your model
@pghistory.track(fields=['name', 'value'])
class MyModel(models.Model):
name = models.CharField(max_length=255)
value = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.name} - {self.value}"
class Meta:
app_label = 'your_app_name'
# After running makemigrations and migrate:
# from your_app_name.models import MyModel
# obj = MyModel.objects.create(name='Test', value=10)
# obj.value = 20
# obj.save()
#
# HistoryModel = pghistory.get_event_model(MyModel)
# for event in HistoryModel.objects.all():
# print(f"ID: {event.pgh_obj_id}, Name: {event.name}, Value: {event.value}, Changed at: {event.pgh_created_at}")