2013-11-28 04:04:49 +00:00
|
|
|
import json
|
|
|
|
|
|
2013-10-20 17:24:33 +00:00
|
|
|
from django.conf import settings
|
2013-10-20 13:25:48 +00:00
|
|
|
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.
|
|
|
|
|
"""
|
2013-10-21 20:27:35 +00:00
|
|
|
changes = kwargs.get('changes', None)
|
|
|
|
|
|
|
|
|
|
if changes is not None:
|
|
|
|
|
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_repr' in kwargs:
|
|
|
|
|
kwargs['object_repr'] = str(instance)
|
|
|
|
|
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
|
|
|
|
|
|
2013-11-06 19:48:16 +00:00
|
|
|
# Delete log entries with the same pk as a newly created model. This should only be necessary when an pk is
|
|
|
|
|
# used twice.
|
2013-10-21 20:27:35 +00:00
|
|
|
if kwargs.get('action', None) is LogEntry.Action.CREATE:
|
2013-10-21 20:41:11 +00:00
|
|
|
if kwargs.get('object_id', None) is not None and self.filter(content_type=kwargs.get('content_type'), object_id=kwargs.get('object_id')).exists():
|
|
|
|
|
self.filter(content_type=kwargs.get('content_type'), object_id=kwargs.get('object_id')).delete()
|
2013-10-21 20:27:35 +00:00
|
|
|
else:
|
2013-10-21 20:41:11 +00:00
|
|
|
self.filter(content_type=kwargs.get('content_type'), object_pk=kwargs.get('object_pk', '')).delete()
|
2013-10-21 20:27:35 +00:00
|
|
|
|
|
|
|
|
return self.create(**kwargs)
|
|
|
|
|
return None
|
2013-11-28 04:04:49 +00:00
|
|
|
|
2013-12-18 16:17:38 +00:00
|
|
|
def get_for_object(self, obj):
|
|
|
|
|
"""
|
|
|
|
|
Get log entries for the specified object.
|
|
|
|
|
"""
|
|
|
|
|
content_type = ContentType.objects.get_for_model(obj.__class__)
|
|
|
|
|
|
|
|
|
|
if isinstance(obj.pk, int):
|
|
|
|
|
return self.filter(content_type=content_type, object_id=obj.pk)
|
|
|
|
|
else:
|
|
|
|
|
return self.filter(content_type=content_type, object_pk=obj.pk)
|
|
|
|
|
|
|
|
|
|
def get_for_model(self, model):
|
|
|
|
|
"""
|
|
|
|
|
Get log entries for all objects of a specified type.
|
|
|
|
|
"""
|
2013-11-28 04:04:49 +00:00
|
|
|
content_type = ContentType.objects.get_for_model(model)
|
2013-12-18 16:17:38 +00:00
|
|
|
return self.filter(content_type=content_type)
|
2013-10-20 13:25:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class LogEntry(models.Model):
|
|
|
|
|
"""
|
2013-10-23 15:23:52 +00:00
|
|
|
Represents an entry in the audit log. The content type is saved along with the textual and numeric (if available)
|
|
|
|
|
primary key, as well as the textual representation of the object when it was saved. It holds the action performed
|
|
|
|
|
and the fields that were changed in the transaction.
|
|
|
|
|
|
2013-11-07 16:02:31 +00:00
|
|
|
If AuditlogMiddleware is used, the actor will be set automatically. Keep in mind that editing / re-saving LogEntry
|
2013-10-23 15:23:52 +00:00
|
|
|
instances may set the actor to a wrong value - editing LogEntry instances is not recommended (and it should not be
|
|
|
|
|
necessary).
|
2013-10-20 13:25:48 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
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"))
|
2013-10-20 17:24:33 +00:00
|
|
|
actor = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_("actor"))
|
2013-10-20 13:25:48 +00:00
|
|
|
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp"))
|
|
|
|
|
|
2013-10-21 19:46:31 +00:00
|
|
|
objects = LogEntryManager()
|
|
|
|
|
|
2013-10-20 13:25:48 +00:00
|
|
|
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:
|
2013-10-21 19:46:31 +00:00
|
|
|
fstring = _("Created {repr:s}")
|
2013-10-20 13:25:48 +00:00
|
|
|
elif self.action == self.Action.UPDATE:
|
2013-10-21 19:46:31 +00:00
|
|
|
fstring = _("Updated {repr:s}")
|
2013-10-20 13:25:48 +00:00
|
|
|
elif self.action == self.Action.DELETE:
|
2013-10-21 19:46:31 +00:00
|
|
|
fstring = _("Deleted {repr:s}")
|
2013-10-20 13:25:48 +00:00
|
|
|
else:
|
2013-10-21 19:46:31 +00:00
|
|
|
fstring = _("Logged {repr:s}")
|
|
|
|
|
|
|
|
|
|
return fstring.format(repr=self.object_repr)
|
2013-12-11 16:14:07 +00:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def changes_dict(self):
|
|
|
|
|
try:
|
|
|
|
|
return json.loads(self.changes)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def changes_str(self, colon=': ', arrow=u' \u2192 ', separator='; '):
|
|
|
|
|
substrings = []
|
|
|
|
|
|
|
|
|
|
for field, values in self.changes_dict.iteritems():
|
2013-12-18 16:17:38 +00:00
|
|
|
substring = u'{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}'.format(
|
|
|
|
|
field_name=field,
|
2013-12-11 16:14:07 +00:00
|
|
|
colon=colon,
|
|
|
|
|
old=values[0],
|
|
|
|
|
arrow=arrow,
|
|
|
|
|
new=values[1],
|
|
|
|
|
)
|
|
|
|
|
substrings.append(substring)
|
|
|
|
|
|
|
|
|
|
return separator.join(substrings)
|
2013-11-28 04:04:49 +00:00
|
|
|
|
2013-10-20 13:25:48 +00:00
|
|
|
|
2013-11-07 16:02:31 +00:00
|
|
|
class AuditlogHistoryField(generic.GenericRelation):
|
2013-10-20 13:25:48 +00:00
|
|
|
"""
|
|
|
|
|
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.
|
2013-10-23 15:23:52 +00:00
|
|
|
|
|
|
|
|
By default this field will assume that your primary keys are numeric, simply because this is the most common case.
|
|
|
|
|
However, if you have a non-integer primary key, you can simply pass pk_indexable=False to the constructor, and
|
|
|
|
|
Auditlog will fall back to using a non-indexed text based field for this model.
|
2013-10-20 13:25:48 +00:00
|
|
|
"""
|
|
|
|
|
|
2013-10-21 19:46:31 +00:00
|
|
|
def __init__(self, pk_indexable=True, **kwargs):
|
2013-10-20 13:25:48 +00:00
|
|
|
kwargs['to'] = LogEntry
|
2013-10-21 19:46:31 +00:00
|
|
|
|
|
|
|
|
if pk_indexable:
|
|
|
|
|
kwargs['object_id_field'] = 'object_id'
|
|
|
|
|
else:
|
|
|
|
|
kwargs['object_id_field'] = 'object_pk'
|
|
|
|
|
|
2013-10-20 13:25:48 +00:00
|
|
|
kwargs['content_type_field'] = 'content_type'
|
2013-11-07 16:02:31 +00:00
|
|
|
super(AuditlogHistoryField, self).__init__(**kwargs)
|