Audited Models

Overview

django-audit provides a single class AuditedModel which is a subclass of django.db.models.Model to handle the auditing for you. The only thing you’ll need to define is which fields to log and the rest is taken care for you!

Creating your model

To get started with auditing your model, you’ll need to subclass AuditedModel and define the log_fields attribute, e.g.:

from django.db import models

from djangoaudit.models import AuditedModel

class Pilot(AuditedModel):
    """A dummy model to test the functionality of the AuditedModel"""

    log_fields = ('first_name', 'last_name', 'call_sign', 'age', 'last_flight',
                  'is_cylon', 'fastest_landing')

    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)
    call_sign = models.CharField(max_length=30)
    age = models.IntegerField()
    last_flight = models.DateTimeField(null=True, blank=True)
    craft = models.IntegerField(choices=CRAFT_CHOICES)
    is_cylon = models.BooleanField(default=False)
    fastest_landing = models.DecimalField(max_digits=5, decimal_places=2)

    def __unicode__(self):
        return self.call_sign

log_fields can be any sequence provided the values are all names of the fields you have declared on your models.

Warning

If you specify a field which is not declared on the models a ValueError will be raised when you start your application.

In the example above we want to log most of the fields on the Pilot model. When new instances of Pilot are created, modified or deleted, the changes to these fields will be recorded.

Auditing data

Auditing happens at the following times:

Model creation

When you create an instance of your model and save it for the first time the initial values of all the log_fields will be recorded:

>>> from datetime import datetime
>>> from decimal import Decimal
>>> hot_dog = Pilot(
            first_name="Brendan",
            last_name="Costanza",
            call_sign="Hot Dog",
            age=25,
            last_flight=datetime(2000, 6, 4, 23, 01),
            craft=1,
            is_cylon=False,
            fastest_landing=Decimal("101.67")
        )
>>> hot_dog.save()
>>> hot_dog.get_creation_log()
{u'fastest_landing': Decimal("101.67"), u'last_flight': datetime.datetime(2000, 6, 4, 23, 1), u'last_name': u'Costanza', u'object_pk': 340, u'call_sign': u'Hot Dog', u'first_name': u'Brendan', u'is_cylon': False, u'_id': ObjectId('4c0f8e8b38b72a6f97000000'), u'audit_date_stamp': datetime.datetime(2010, 6, 9, 12, 52, 27, 717000), u'object_app': u'bsg', u'object_model': u'Pilot', u'age': 25}

For more information on retrieving the creation log, see retrieving the creation log.

We see that when we read back the log entry for creation all the fields specified in log_fields appear in the returned dictionary as well as some identifiers that relate the data to the particular model instance and the ObjectId from the MongoDB collection.

Model modification

When you make any changes to a model and called the save method the delta of the values will be recorded. That is to say only those fields in log_fields which have changed since the model was last saved will be logged.

Warning

Only the save() will trigger the auditing. If you call update() on a queryset, the save method won’t be triggered and no data will be audited.

However, any process which implicitly trigger save() will cause the changes to be recorded.

We can see this in process by picking up where we left off above. First if we change the age we see this in the last log entry:

>>> hot_dog.age = 26
>>> hot_dog.save()
>>> list(hot_dog.get_audit_log())[-1]
{u'object_app': u'bsg', 'audit_changes': {u'age': (25, 26)}, u'object_pk': 340, u'_id': ObjectId('4c0f91d038b72a6f97000001'), u'audit_date_stamp': datetime.datetime(2010, 6, 9, 13, 6, 24, 557000), u'object_model': u'Pilot'}

This time the age is returned as a tuple of the value before the change and the value after. (For more information on retrieving the audit log, see retrieving the audit log).

Conversely if we change craft and save, no changes will be recorded as craft is not one of the log fields:

>>> hot_dog.craft = 45
>>> hot_dog.save()
>>> list(hot_dog.get_audit_log())[-1]
{u'object_app': u'bsg', 'audit_changes': {u'age': (25, 26)}, u'object_pk': 340, u'_id': ObjectId('4c0f91d038b72a6f97000001'), u'audit_date_stamp': datetime.datetime(2010, 6, 9, 13, 6, 24, 557000), u'object_model': u'Pilot'}

In this case, when we retrieve the last log entry, we see that we have the log for the previous change with no mention of the craft.

If we want to change the age back to its original value, we can do this:

>>> hot_dog.age = 25
>>> hot_dog.save()
>>> list(hot_dog.get_audit_log())[-1]
{u'object_app': u'bsg', 'audit_changes': {u'age': (26, 25)}, u'object_pk': 340, u'_id': ObjectId('4c0f931638b72a6f97000002'), u'audit_date_stamp': datetime.datetime(2010, 6, 9, 13, 11, 50, 17000), u'object_model': u'Pilot'}

The reversal of the age in now recorded.

Model deletion

When we delete a model instance from the database, we want to know how it looked just before it was deleted. We can access information for a specific model or for a particular instance (see retrieving the deletion log):

>>> pk = hot_dog.pk
>>> hot_dog.delete()
>>> list(Pilot.get_deleted_log(pk))
[{u'fastest_landing': Decimal("101.67"), u'last_flight': datetime.datetime(2000, 6, 4, 23, 1), u'last_name': u'Costanza', u'age': 25, u'call_sign': u'Hot Dog', u'first_name': u'Brendan', u'is_cylon': False, u'_id': ObjectId('4c0f93a538b72a6f97000003'), u'audit_is_delete': True, u'audit_notes': u'Object deleted. These are the attributes at delete time.', u'audit_date_stamp': datetime.datetime(2010, 6, 9, 13, 14, 13, 313000), u'object_app': u'bsg', u'object_model': u'Pilot', u'object_pk': 340}]

Here, we see that the log contains a single item (as only one object was deleted) containing all the last known values for the object.

Recording extra information

If you need to record extra information with a change to the model, AuditedModel provides set_audit_info(). This method accepts any keyword arguments and will log these as such. The keys can be other fields from the model or simply anything else you wish to record.

The audit process does support two special keys in set_audit_info():

  • Operator
  • Notes

These are designed so that they will always avoid a name clash with any fields on the models and in the log will be prefixed with audit_. For most uses these should be sufficient for recording any extra information, but you may also wish to provide other data. Any other data will not be prefixed:

>>> hot_dog.set_audit_info(operator="Someone", notes="Quick update", hyperspace=True)
>>> hot_dog.save()
>>> list(hot_dog.get_audit_log())[-1]['operator']
------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython console>", line 1, in <module>
KeyError: 'operator'

>>> list(hot_dog.get_audit_log())[-1]['audit_operator']
u'Someone'
>>> list(hot_dog.get_audit_log())[-1]['audit_notes']
u'Quick update'
>>> list(hot_dog.get_audit_log())[-1]['hyperspace']
True

Reading from the logs

There are three sets of logs read-back options available:

Retrieving the creation log

To get the creation log for your audited model, get_creation_log() on the model instance:

>>> hot_dog.get_creation_log()
{u'fastest_landing': Decimal("101.67"), u'last_flight': datetime.datetime(2000, 6, 4, 23, 1), u'last_name': u'Costanza', u'object_pk': 340, u'call_sign': u'Hot Dog', u'first_name': u'Brendan', u'is_cylon': False, u'_id': ObjectId('4c0f8e8b38b72a6f97000000'), u'audit_date_stamp': datetime.datetime(2010, 6, 9, 12, 52, 27, 717000), u'object_app': u'bsg', u'object_model': u'Pilot', u'age': 25}

This will return a dictionary of all the values at creation time of the model.

Retrieving the audit log

To see all the entries over the history of the audited model instance call get_audit_log(). This will return a generator that you can iterate over:

>>> hot_dog.get_audit_log()
<generator object at 0x949a2ec>
>>> for entry in hot_dog.get_audit_log():
...     print entry['audit_changes']
...
{u'fastest_landing': (None, Decimal("101.67")), u'last_flight': (None, datetime.datetime(2000, 6, 4, 23, 1)), u'last_name': (None, u'Costanza'), u'age': (None, 25), u'first_name': (None, u'Brendan'), u'is_cylon': (None, False), u'call_sign': (None, u'Hot Dog')}
{u'age': (25, 26)}
{u'age': (26, 25)}
{u'last_name': (u'Costanza', u'New last name')}

In this case a dictionary will be available with the key audit_changes, which contains a tuple for each logged field that has been changed to indicate the before and after value.

For the first entry (the creation), the before value will always be None.

For all fields which are not specified in log_fields, the value in the log at the time will reported. We can see this if we log the operator with another save:

>>> hot_dog.first_name = "New first name"
>>> hot_dog.set_audit_info(operator="Me")
>>> hot_dog.save()
>>> list(hot_dog.get_audit_log())[-1]
{u'object_model': u'Pilot', 'audit_changes': {u'first_name': (u'Brendan', u'New first name')}, u'object_pk': 340, u'audit_operator': u'Me', u'_id': ObjectId('4c0faa0b38b72a6f97000006'), u'audit_date_stamp': datetime.datetime(2010, 6, 9, 14, 49, 47, 140000), u'object_app': u'bsg'}
>>> list(hot_dog.get_audit_log())[-1]['audit_operator']
u'Me'
>>> list(hot_dog.get_audit_log())[-1]['audit_changes']
{u'first_name': (u'Brendan', u'New first name')}

For an explanation of logging extra information with a change, see recording extra information.

Note

In the case of get_audit_log(), the first entry (i.e. the creation log) is not reported in the same manner as in get_creation_log().

Retrieving the deletion log

Once an audited model instance has been deleted, it may be desirable to view the object at the time of deletion. To facilitate this there is a class method provided on AuditedModel subclasses: get_deleted_log().

If this is called with no arguments, all logs for this model will be retrieved. If a primary key is specified as the argument, only the deletion logs for that object will be retrieved.

Note

This class method is a generator so you will need to iterate over it to retrieve the logs.

API Documentation

class djangoaudit.models.AuditedModel(*args, **kwargs)

A version of django.db.models.Model that will audit any values specified in the log_fields class attribute

delete(*args, **kwargs)

The delete method performs auditing on the model to record the state of the model prior to deletion.

get_audit_log()

Construct a generator of all the items in the audit log for this object.

All data in the object will be converted into the correct type. Each log entry is returned a dictionary of directly set data with an additional key called audit_changes. This value is a dictionary of the fields which have changed on the model. e.g.:

{'operator' : 'power_user',
 'extra': 'Extra info',
 'audit_changes': {'seats': (10, 20),
                   'premium': (False, True)
                  }
}
get_creation_log()

Get the values logged on the creation of this instance or None if not available

Returns:The document from MongoDB associated with the creation of this record
Return type:<pending_xref py:class=”AuditedModel” py:module=”djangoaudit.models” refdoc=”models” refdomain=”py” refexplicit=”False” reftarget=”dict” reftype=”class”><literal classes=”xref py py-class”>dict</literal></pending_xref>
classmethod get_deleted_log(pk=None)

Construct a generator of all items which have been deleted for this model. If pk is specified, then the results will be filtered for that primary key.

Parameters:
  • pk – The primary key of the instance to consider (optional)
save(*args, **kwargs)

The save method performs auditing on the model to record the differences before and after the commit to the DB.

set_audit_info(**kwargs)

Set extra audit information on this instance.