First implementation

This commit is contained in:
Jan-Jelle Kester 2013-10-20 15:25:48 +02:00
parent 15d2bb6295
commit 9a13a5966f
6 changed files with 274 additions and 0 deletions

0
auditlog/__init__.py Normal file
View file

35
auditlog/diff.py Normal file
View file

@ -0,0 +1,35 @@
from django.db.models import Model
def model_instance_diff(old, new):
"""
Calculate the differences between two model instances. One of the instances may be None (i.e., a newly
created model or deleted model). This will cause all fields with a value to have changed (from None).
"""
if not(old is None or isinstance(old, Model)):
raise TypeError('The supplied old instance is not a valid model instance.')
if not(new is None or isinstance(new, Model)):
raise TypeError('The supplied new instance is not a valid model instance.')
diff = {}
if old is not None and new is not None:
fields = set(old._meta.fields + new._meta.fields)
elif old is not None:
fields = set(old._meta.fields)
elif new is not None:
fields = set(new._meta.fields)
else:
fields = set()
for field in fields:
old_value = getattr(old, field.name, None)
new_value = getattr(new, field.name, None)
if old_value != new_value:
diff[field.name] = (old_value, new_value)
if len(diff) == 0:
diff = None
return diff

27
auditlog/middleware.py Normal file
View file

@ -0,0 +1,27 @@
from django.db.models.signals import pre_save
from django.utils.functional import curry
from auditlog.models import LogEntry
class AuditLogMiddleware(object):
"""
Middleware to couple the request's user to log items. This is accomplished by currying the signal receiver with the
user from the request (or None if the user is not authenticated).
"""
def process_request(self, request):
if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated():
user = request.user
else:
user = None
insert_user = curry(self.insert_user, user)
pre_save.connect(insert_user, sender=LogEntry, dispatch_uid=(self.__class__, request), weak=False)
def process_response(self, request, response):
pre_save.disconnect(dispatch_uid=(self.__class__, request))
return response
def insert_user(self, user, sender, instance, **kwargs):
if sender == LogEntry and isinstance(instance, sender):
instance.actor = user

82
auditlog/models.py Normal file
View file

@ -0,0 +1,82 @@
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.translation import ugettext_lazy as _
class LogEntryManager(models.Manager):
"""
Custom manager for the LogEntry model.
"""
def log_create(self, instance, **kwargs):
"""
Helper method to create a new log entry. This method automatically fills in some data when it is left out. It
was created to keep things DRY.
"""
if not 'content_type' in kwargs:
kwargs['content_type'] = ContentType.objects.get_for_model(instance)
if not 'object_pk' in kwargs:
kwargs['object_pk'] = instance.pk
if not 'object_id' in kwargs:
pk_field = instance._meta.pk.name
pk = getattr(instance, pk_field, None)
if isinstance(pk, int):
kwargs['object_id'] = pk
self.create(**kwargs)
class LogEntry(models.Model):
"""
Represents an entry in the audit log, containing data.
"""
class Action:
CREATE = 0
UPDATE = 1
DELETE = 2
choices = (
(CREATE, _("create")),
(UPDATE, _("update")),
(DELETE, _("delete")),
)
content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE, related_name='+', verbose_name=_("content type"))
object_pk = models.TextField(verbose_name=_("object pk"))
object_id = models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name=_("object id"))
object_repr = models.TextField(verbose_name=_("object representation"))
action = models.PositiveSmallIntegerField(choices=Action.choices, verbose_name=_("action"))
changes = models.TextField(blank=True, verbose_name=_("change message"))
actor = models.ForeignKey('auth.User', blank=True, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_("actor"))
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp"))
class Meta:
get_latest_by = 'timestamp'
ordering = ['-timestamp']
verbose_name = _("log entry")
verbose_name_plural = _("log entries")
def __unicode__(self):
if self.action == self.Action.CREATE:
return _("Created {repr:s}").format(self.object_repr)
elif self.action == self.Action.UPDATE:
return _("Updated {repr:s}").format(self.object_repr)
elif self.action == self.Action.DELETE:
return _("Deleted {repr:s}").format(self.object_repr)
else:
return u'{verbose_name:s} #{id:s}'.format(verbose_name=self._meta.verbose_name.capitalize(), id=self.id)
class AuditLogHistoryField(generic.GenericRelation):
"""
A subclass of django.contrib.contenttypes.generic.GenericRelation that sets some default variables. This makes it
easier to implement the audit log in models, and makes future changes easier.
"""
def __init__(self, **kwargs):
kwargs['to'] = LogEntry
kwargs['object_id_field'] = 'object_id'
kwargs['content_type_field'] = 'content_type'
super(AuditLogHistoryField, self).__init__(**kwargs)

57
auditlog/receivers.py Normal file
View file

@ -0,0 +1,57 @@
import json
from auditlog.diff import model_instance_diff
from auditlog.models import LogEntry
def log_create(sender, instance, created, **kwargs):
"""
Signal receiver that creates a log entry when a model instance is first saved to the database.
Direct use is discouraged, connect your model through auditlog.registry.registry instead.
"""
if created:
changes = model_instance_diff(None, instance)
log_entry = LogEntry.objects.log_create(
instance,
action=LogEntry.Action.CREATE,
changes=json.dumps(changes),
)
log_entry.save()
def log_update(sender, instance, **kwargs):
"""
Signal receiver that creates a log entry when a model instance is changed and saved to the database.
Direct use is discouraged, connect your model through auditlog.registry.registry instead.
"""
if instance.pk is not None:
old = sender.objects.get(pk=instance.pk)
new = instance
changes = model_instance_diff(old, new)
log_entry = LogEntry.objects.log_create(
instance,
action=LogEntry.Action.UPDATE,
changes=json.dumps(changes),
)
log_entry.save()
def log_delete(sender, instance, **kwargs):
"""
Signal receiver that creates a log entry when a model instance is deleted from the database.
Direct use is discouraged, connect your model through auditlog.registry.registry instead.
"""
if instance.pk is not None:
changes = model_instance_diff(instance, None)
log_entry = LogEntry.objects.log_create(
instance,
action=LogEntry.Action.DELETE,
changes=json.dumps(changes),
)
log_entry.save()

73
auditlog/registry.py Normal file
View file

@ -0,0 +1,73 @@
from django.db.models.signals import pre_save, post_save, post_delete
from django.db.models import Model
from auditlog.receivers import log_create, log_update, log_delete
class AuditLogModelRegistry(object):
"""
A registry that keeps track of the models that use auditlog.
"""
def __init__(self, create=True, update=True, delete=True, custom=None):
self._registry = []
self._signals = {}
if create:
self._signals[post_save] = log_create
if update:
self._signals[pre_save] = log_update
if delete:
self._signals[post_delete] = log_delete
if custom is not None:
self._signals.update(custom)
def register(self, model):
"""
Register a model with auditlog. Auditlog will then track mutations on this model's instances.
"""
if issubclass(model, Model):
self._registry.append(model)
self._connect_signals(model)
else:
raise TypeError('Supplied model is not a valid model.')
def contains(self, model):
"""
Check if a model is registered with auditlog.
"""
return model in self._registry
def unregister(self, model):
"""
Unregister a model with auditlog. This will not affect the database.
"""
try:
self._registry.pop(model)
except KeyError:
pass
else:
self._disconnect_signals(model)
def _connect_signals(self, model):
"""
Connect signals for the model.
"""
for signal, receiver in self._signals:
signal.connect(receiver, sender=model, dispatch_uid=self._dispatch_uid(signal, model))
def _disconnect_signals(self, model):
"""
Disconnect signals for the model.
"""
for signal, receiver in self._signals:
signal.disconnect(dispatch_uid=self._dispatch_uid(signal, model))
def _dispatch_uid(self, signal, model):
"""
Generate a dispatch_uid.
"""
return (self.__class__, model, signal)
registry = AuditLogModelRegistry()