django-simple-history FTW!
Eight months! Apparently that is the magic number when it comes to the amount of time it takes for me to talk about work I’ve done and get motivated to polish it up a bit. That said, lets talk about django-simple-history! One of my bigger projects at work is a monstrous django project that I’ve built from the ground up that does a bunch of enterprise type nitty-gritty (yay CRUD). At time of inception, one of the big feature requests was to have tracking of _all_ changes to the database. This request included needs for the following:
- change type (create, update, delete)
- change time
- change user
I looked around and saw some basic implementations of different historical record attempts, including AuditTrail but had a requirement of needing models.OneToOneField & models.ForeignKey fields to work (store that there was a relation and not fail on lookups). I had read about something similar in Marty Alchin’s Pro Django book (highly recommend), so I dug that up and was sad to see that it also didn’t work with the relation fields. I reached out to Marty to see if he had a solution and it seemed that at the time he had not even thought about it too much past his initial work. I asked him if he’d be OK with me expanding upon his initial source and creating a project out of his work with changes to support the relation fields and he agreed. The one aspect that had to be left out for now is the tracking of the user who made the change. This _can_ be done. I do it with an extra field added to the history model. That said, hopefully soon I can release that code with a patch to django-simple-history so you can easily do this. For now please contact me for more details about tracking the current user making changes. That said, everything else was free game so I threw it all together and put it on bitbucket.
Thus django-simple-history was born.
I kinda threw the code up after i got it working and totally forgot about it. I’m sad to admit that I didn’t do it due diligence, even if I never planned to make it a package. That was until someone actually forked my code! *SHOCK* They had added a setup.py, so I pulled it in and expanded on it (it was pretty simple at first). Now there is a fully installable app with example code in the README and an actual package uploaded to make django-simple-history 1.0!
Here’s the basic rundown on how you use it using the django tutorials Poll and Choice models (used django 1.2.3 for these tests):
from django.db import models
# import the HistoricalRecords model
from simple_history.models import HistoricalRecords
class Poll(models.Model):
question = models.CharField(max_length = 200)
pub_date = models.DateTimeField('date published')
# create an instance of HistoricalRecords on any model you want to track
history = HistoricalRecords()
class Choice(models.Model):
poll = models.ForeignKey(Poll)
choice = models.CharField(max_length=200)
votes = models.IntegerField()
history = HistoricalRecords()
That’s it! Once you’ve done this, when you $ ./manage.py syncdb you will get the following:
BEGIN;
CREATE TABLE "test_historicalpoll" (
"id" integer NOT NULL,
"question" varchar(200) NOT NULL,
"pub_date" datetime NOT NULL,
"history_id" integer NOT NULL PRIMARY KEY,
"history_date" datetime NOT NULL,
"history_type" varchar(1) NOT NULL
)
;
CREATE TABLE "test_poll" (
"id" integer NOT NULL PRIMARY KEY,
"question" varchar(200) NOT NULL,
"pub_date" datetime NOT NULL
)
;
CREATE TABLE "test_historicalchoice" (
"id" integer NOT NULL,
"poll_id" integer,
"choice" varchar(200) NOT NULL,
"votes" integer NOT NULL,
"history_id" integer NOT NULL PRIMARY KEY,
"history_date" datetime NOT NULL,
"history_type" varchar(1) NOT NULL
)
;
CREATE TABLE "test_choice" (
"id" integer NOT NULL PRIMARY KEY,
"poll_id" integer NOT NULL REFERENCES "test_poll" ("id"),
"choice" varchar(200) NOT NULL,
"votes" integer NOT NULL
)
;
CREATE INDEX "test_historicalpoll_4a5fc416" ON "test_historicalpoll" ("id");
CREATE INDEX "test_historicalchoice_4a5fc416" ON "test_historicalchoice" ("id");
CREATE INDEX "test_historicalchoice_763e883" ON "test_historicalchoice" ("poll_id");
CREATE INDEX "test_choice_763e883" ON "test_choice" ("poll_id");
COMMIT;
* test was my app name for use in testing… poor choice.
The two historical db’s were created with the extra fields needed to track them historically:
- test_historicalpoll
- test_historicalchoice
Now to use, fire up the django shell $ ./manage.py syncdb and try this!
In [2]: from poll.models import Poll, Choice In [3]: Poll.objects.all() Out[3]: [] In [4]: import datetime In [5]: p = Poll(question="what's up?", pub_date=datetime.datetime.now()) In [6]: p.save() In [7]: p Out[7]: <Poll: Poll object> In [9]: p.history.all() Out[9]: [<HistoricalPoll: Poll object as of 2010-10-25 18:03:29.855689>] In [10]: p.pub_date = datetime.datetime(2007,4,1,0,0) In [11]: p.save() In [13]: p.history.all() Out[13]: [<HistoricalPoll: Poll object as of 2010-10-25 18:04:13.814128>, <HistoricalPoll: Poll object as of 2010-10-25 18:03:29.855689>] In [14]: p.choice_set.create(choice='Not Much', votes=0) Out[14]: <Choice: Choice object> In [15]: p.choice_set.create(choice='The sky', votes=0) Out[15]: <Choice: Choice object> In [16]: c = p.choice_set.create(choice='Just hacking again', votes=0) In [17]: c.poll Out[17]: <Poll: Poll object> In [19]: c.history.all() Out[19]: [<HistoricalChoice: Choice object as of 2010-10-25 18:05:30.160595>] In [20]: Choice.history Out[20]: <simple_history.manager.HistoryManager object at 0x1cc4290> In [21]: Choice.history.all() Out[21]: [<HistoricalChoice: Choice object as of 2010-10-25 18:05:30.160595>, <HistoricalChoice: Choice object as of 2010-10-25 18:05:12.183340>, <HistoricalChoice: Choice object as of 2010-10-25 18:04:59.047351>]
The highlighted lines show how to access the history of each object:
- the first and second being the specific history of an instance (p – Poll, c – Choice)
- the third is the history of an entire model (Choice)
Please leave a comment if you have any questions about this, or drop a bug report on bitbucket. I’m open to suggestions and/or more help to fill out some of the other relational field types (ManyToMany).
tl;dr – Use django-simple-history, it’s epic.