mirror of
https://github.com/jazzband/django-auditlog.git
synced 2026-03-16 22:20:26 +00:00
First implementation
This commit is contained in:
parent
15d2bb6295
commit
9a13a5966f
6 changed files with 274 additions and 0 deletions
0
auditlog/__init__.py
Normal file
0
auditlog/__init__.py
Normal file
35
auditlog/diff.py
Normal file
35
auditlog/diff.py
Normal 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
27
auditlog/middleware.py
Normal 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
82
auditlog/models.py
Normal 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
57
auditlog/receivers.py
Normal 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
73
auditlog/registry.py
Normal 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()
|
||||
Loading…
Reference in a new issue