Context Navigation

Automating an audit trail

As raised in ​a recent discussion on django-developers, this code is one solution for creating an audit trail for a given model. This is very much incomplete, and is intended to be used as a base to work from. See Caveats below for more information.

Usage

Copy the code at the bottom of this article into a location of your choice. It's just a one-file utility, so it doesn't require an app directory or anything. The examples below assume it's called audit.py and is somewhere on your PYTHONPATH.

In your models file, there are only a couple things to do. First, obviously you'll need to import your audit file, or possibly just get AuditTrail from within it. Then, add an AuditTrail to the model of your choice, assigning it to whatever name you like. That's the only thing necessary to set up the audit trail and get Python-level acecss to it. If you need to view the audit information in the admin interface, simply add show_in_admin=True as an argument to AuditTrail.

This simple addition will do the rest, allowing you to run syncdb and install the audit model. Once it's installed, the following code will work as shown below. As you will see, Person.history becomes a manager that's used to access the audit trail for a particular object.

>>>frommyapp.modelsimport Person
>>> person = Person.objects.create(first_name='John', last_name='Public', salary=50000)>>><Person: John Public>>>> person.history.count()1>>> person.salary =65000>>> person.save()>>> person.history.count()2>>>for item in person.history.all():...print"%s: %s"%(item, item.salary)
John Public as of 2007-08-1420:31:21.852000:65000
John Public as of 2007-08-1420:30:58.959000:50000

As you can see, the audit trail is listed with the most recent state first. Each entry also inclues a timestamp when the edit took place.

Caveats

For one thing, only post_save is used right now, so if you have need for post_delete, you'll need to put that in whatever way you'd like.

Also, in order to copy the fields from the original model to the audit model, it uses some hackery I'm not particularly proud of. It seems to work for all the cases I would have hoped it would, but it relies on the arguments passed to the Field class being named the same as the attributes stored on the Field object after it's created. If there's ever a time that's not the case, it will fail completely on that Field type.

Speaking of which, it fails completely on ForeignKeys and ManyToManyFields, something I've yet to remedy. That's definitely a must-have, but I haven't worked out the best way to go about it. And since this whole things isn't something I'm particularly interested in, I'm probably going to leave that up to somebody else to work out.

It currently copies and overrides the model's __str__ method, so that it can helpfully describe each entry in the audit history. This means, however, that if your __str__ method relies on any other methods (such as get_full_name or similar), it won't work and will need to be adjusted.

Code

Hopefully there are enough comments to make sense of what's going on. More information can be found ​here.