A Django app for auditing field changes on database models.
pip install django-field-audit
To enable the app, add it to your Django INSTALLED_APPS
configuration and run
migrations. Settings example:
INSTALLED_APPS = [
# ...
"field_audit",
]
The "auditor chain" (see FIELD_AUDIT_AUDITORS
in the Custom settings table
below) is configured out of the box with the default auditors. If
change_context
auditing is desired for authenticated Django requests, add the
app middleware to your Django MIDDLEWARE
configuration. For example:
MIDDLEWARE = [
# ...
"field_audit.middleware.FieldAuditMiddleware",
]
The audit chain can be updated to use custom auditors (subclasses of
field_audit.auditors.BaseAuditor
). If change_context
auditing is not
desired, the audit chain can be cleared to avoid extra processing:
FIELD_AUDIT_AUDITORS = []
Name | Description | Default value when unset |
---|---|---|
FIELD_AUDIT_AUDITEVENT_MANAGER |
A custom manager to use for the AuditEvent Model. |
field_audit.models.DefaultAuditEventManager |
FIELD_AUDIT_AUDITORS |
A custom list of auditors for acquiring change_context info. |
["field_audit.auditors.RequestAuditor", "field_audit.auditors.SystemUserAuditor"] |
To begin auditing Django models, import the field_audit.audit_fields
decorator
and decorate models specifying which fields should be audited for changes.
Example code:
# flight/models.py
from django.db import models
from field_audit import audit_fields
@audit_fields("tail_number", "make_model", "operated_by")
class Aircraft(models.Model):
id = AutoField(primary_key=True)
tail_number = models.CharField(max_length=32, unique=True)
make_model = models.CharField(max_length=64)
operated_by = models.CharField(max_length=64)
By default, Model and QuerySet methods are audited, with the exception of four "special" QuerySet methods:
DB Write Method | Audited |
---|---|
Model.delete() |
Yes |
Model.save() |
Yes |
QuerySet.bulk_create() |
No |
QuerySet.bulk_update() |
No |
QuerySet.create() |
Yes (via Model.save() ) |
QuerySet.delete() |
No |
QuerySet.get_or_create() |
Yes (via QuerySet.create() ) |
QuerySet.update() |
No |
QuerySet.update_or_create() |
Yes (via QuerySet.get_or_create() and Model.save() ) |
Auditing for the four "special" QuerySet methods that perform DB writes (labeled No in the table above) can be enabled. This requires three extra usage details:
Warning Enabling auditing on these QuerySet methods might have significant performance implications, especially on large datasets, since audit events are constructed in memory and bulk written to the database.
- Enable the feature by calling the audit decorator specifying
@audit_fields(..., audit_special_queryset_writes=True)
. - Configure the model class so its default manager is an instance of
field_audit.models.AuditingManager
. - All calls to the four "special" QuerySet write methods require an extra
audit_action
keyword argument whose value is one of:field_audit.models.AuditAction.AUDIT
field_audit.models.AuditAction.IGNORE
- Specifying
audit_special_queryset_writes=True
(step 1 above) without setting the default manager to an instance ofAuditingManager
(step 2 above) will raise an exception when the model class is evaluated. - At this time,
QuerySet.delete()
,QuerySet.update()
, andQuerySet.bulk_create()
"special" write methods can actually perform change auditing when called withaudit_action=AuditAction.AUDIT
.QuerySet.bulk_update()
is not currently implemented and will raiseNotImplementedError
if called with that action. Implementing this remaining method remains a task for the future, see TODO below. All four methods do supportaudit_action=AuditAction.IGNORE
usage, however. - All audited methods use transactions to ensure changes to audited models are only committed to the database if audit events are successfully created and saved as well.
In the scenario where auditing is enabled for a model with existing data, it can be valuable to generate "bootstrap" audit events for all of the existing model records in order to ensure that there is at least one audit event record for every model instance that currently exists. There is a migration utility for performing this bootstrap operation. Example code:
# flight/migrations/0002_bootstrap_aircarft_auditing.py
from django.db import migrations, models
from field_audit.utils import run_bootstrap
from flight.models import Aircraft
class Migration(migrations.Migration):
dependencies = [
('flight', '0001_initial'),
]
operations = [
run_bootstrap(Aircraft, ["tail_number", "make_model", "operated_by"])
]
If bootstrapping is not suitable during migrations, there is a management command for
performing the same operation. The management command does not accept arbitrary
field names for bootstrap records, and uses the fields configured by the
existing audit_fields(...)
decorator on the model. Example (analogous to
migration action shown above):
manage.py bootstrap_field_audit_events init Aircraft
Additionally, if a post-migration bootstrap "top up" action is needed, the
the management command can also perform this action. A "top up" operation
creates bootstrap audit events for any existing model records which do not have
a "create" or "bootstrap" AuditEvent
record. Note that the management command
is currently the only way to "top up" bootstrap audit events. Example:
manage.py bootstrap_field_audit_events top-up Aircraft
This app uses Django's JSONField
which means if you intend to use the app with
a SQLite database, the SQLite JSON1
extension is required. If your system's
Python sqlite3
library doesn't ship with this extension enabled, see
this article for details
on how to enable it.
All feature and bug contributions are expected to be covered by tests.
Create/activate a python virtualenv and install the required dependencies.
cd django-field-audit
mkvirtualenv django-field-audit # or however you choose to setup your environment
pip install django pynose flake8 coverage
Note: By default, local tests use an in-memory SQLite database. Ensure that
your local Python's sqlite3
library ships with the JSON1
extension enabled
(see Using with SQLite).
-
Tests
nosetests
-
Style check
flake8 --config=setup.cfg
-
Coverage
coverage run -m nose coverage report -m
The example manage.py
is available for making new migrations.
python example/manage.py makemigrations field_audit
First bump the package version in the field_audit/__init__.py
file. Then create a changelog entry in the CHANGELOG.md
file. After these changes are merged, you should tag the main branch with the new version. Then, package and upload the generated files to PyPI.
pip install -r pkg-requires.txt
python setup.py sdist bdist_wheel
twine upload dist/*
- Implement auditing for the remaining "special" QuerySet write operations:
bulk_update()
- Write full library documentation using github.io.
- Switch to
pytest
to support Python 3.10.
- Add to optimization for
instance.save(save_fields=[...])
[maybe]. - Support adding new audit fields on the same model at different times (instead
of raising
AlreadyAudited
) [maybe].