2015-02-16 21:17:22 +00:00
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
|
2013-11-28 04:04:49 +00:00
|
|
|
import json
|
2017-09-13 14:57:47 +00:00
|
|
|
import ast
|
2013-11-28 04:04:49 +00:00
|
|
|
|
2013-10-20 17:24:33 +00:00
|
|
|
from django.conf import settings
|
2015-08-20 17:53:59 +00:00
|
|
|
from django.contrib.contenttypes.fields import GenericRelation
|
2013-10-20 13:25:48 +00:00
|
|
|
from django.contrib.contenttypes.models import ContentType
|
2017-09-13 14:57:47 +00:00
|
|
|
from django.core.exceptions import FieldDoesNotExist
|
2013-10-20 13:25:48 +00:00
|
|
|
from django.db import models
|
2015-05-15 13:14:57 +00:00
|
|
|
from django.db.models import QuerySet, Q
|
2017-09-13 14:57:47 +00:00
|
|
|
from django.utils import formats
|
2015-02-16 21:17:22 +00:00
|
|
|
from django.utils.encoding import python_2_unicode_compatible, smart_text
|
|
|
|
|
from django.utils.six import iteritems, integer_types
|
2013-10-20 13:25:48 +00:00
|
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
|
|
2018-01-02 17:24:49 +00:00
|
|
|
import pytz
|
2016-05-11 11:03:06 +00:00
|
|
|
from jsonfield.fields import JSONField
|
2017-09-13 14:57:47 +00:00
|
|
|
from dateutil import parser
|
2018-01-02 17:24:49 +00:00
|
|
|
from dateutil.tz import gettz
|
2015-04-16 18:33:59 +00:00
|
|
|
|
2013-10-20 13:25:48 +00:00
|
|
|
|
|
|
|
|
class LogEntryManager(models.Manager):
|
|
|
|
|
"""
|
2015-05-31 13:06:06 +00:00
|
|
|
Custom manager for the :py:class:`LogEntry` model.
|
2013-10-20 13:25:48 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def log_create(self, instance, **kwargs):
|
|
|
|
|
"""
|
2015-05-31 13:06:06 +00:00
|
|
|
Helper method to create a new log entry. This method automatically populates some fields when no explicit value
|
|
|
|
|
is given.
|
|
|
|
|
|
|
|
|
|
:param instance: The model instance to log a change for.
|
|
|
|
|
:type instance: Model
|
|
|
|
|
:param kwargs: Field overrides for the :py:class:`LogEntry` object.
|
|
|
|
|
:return: The new log entry or `None` if there were no changes.
|
|
|
|
|
:rtype: LogEntry
|
2013-10-20 13:25:48 +00:00
|
|
|
"""
|
2013-10-21 20:27:35 +00:00
|
|
|
changes = kwargs.get('changes', None)
|
2013-12-18 16:24:41 +00:00
|
|
|
pk = self._get_pk_value(instance)
|
2013-10-21 20:27:35 +00:00
|
|
|
|
|
|
|
|
if changes is not None:
|
2013-12-18 16:24:41 +00:00
|
|
|
kwargs.setdefault('content_type', ContentType.objects.get_for_model(instance))
|
|
|
|
|
kwargs.setdefault('object_pk', pk)
|
2015-02-16 21:17:22 +00:00
|
|
|
kwargs.setdefault('object_repr', smart_text(instance))
|
2013-12-18 16:24:41 +00:00
|
|
|
|
2015-02-16 21:17:22 +00:00
|
|
|
if isinstance(pk, integer_types):
|
2013-12-18 16:24:41 +00:00
|
|
|
kwargs.setdefault('object_id', pk)
|
2013-10-21 20:27:35 +00:00
|
|
|
|
2015-06-01 16:24:13 +00:00
|
|
|
get_additional_data = getattr(instance, 'get_additional_data', None)
|
|
|
|
|
if callable(get_additional_data):
|
2015-06-03 15:47:50 +00:00
|
|
|
kwargs.setdefault('additional_data', get_additional_data())
|
2015-04-16 18:33:59 +00:00
|
|
|
|
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()
|
2017-09-13 14:57:47 +00:00
|
|
|
# save LogEntry to same database instance is using
|
|
|
|
|
db = instance._state.db
|
|
|
|
|
return self.create(**kwargs) if db is None or db == '' else self.using(db).create(**kwargs)
|
2013-10-21 20:27:35 +00:00
|
|
|
return None
|
2014-10-03 13:13:03 +00:00
|
|
|
|
2013-12-18 16:24:41 +00:00
|
|
|
def get_for_object(self, instance):
|
2013-12-18 16:17:38 +00:00
|
|
|
"""
|
2015-02-16 16:06:17 +00:00
|
|
|
Get log entries for the specified model instance.
|
2015-05-31 13:06:06 +00:00
|
|
|
|
|
|
|
|
:param instance: The model instance to get log entries for.
|
|
|
|
|
:type instance: Model
|
|
|
|
|
:return: QuerySet of log entries for the given model instance.
|
|
|
|
|
:rtype: QuerySet
|
2013-12-18 16:17:38 +00:00
|
|
|
"""
|
2015-02-16 16:06:17 +00:00
|
|
|
# Return empty queryset if the given model instance is not a model instance.
|
|
|
|
|
if not isinstance(instance, models.Model):
|
|
|
|
|
return self.none()
|
|
|
|
|
|
2013-12-18 16:24:41 +00:00
|
|
|
content_type = ContentType.objects.get_for_model(instance.__class__)
|
|
|
|
|
pk = self._get_pk_value(instance)
|
2013-12-18 16:17:38 +00:00
|
|
|
|
2015-02-16 21:17:22 +00:00
|
|
|
if isinstance(pk, integer_types):
|
2013-12-18 16:24:41 +00:00
|
|
|
return self.filter(content_type=content_type, object_id=pk)
|
2013-12-18 16:17:38 +00:00
|
|
|
else:
|
2017-06-29 14:19:33 +00:00
|
|
|
return self.filter(content_type=content_type, object_pk=smart_text(pk))
|
2013-12-18 16:17:38 +00:00
|
|
|
|
2015-05-15 13:14:57 +00:00
|
|
|
def get_for_objects(self, queryset):
|
|
|
|
|
"""
|
|
|
|
|
Get log entries for the objects in the specified queryset.
|
|
|
|
|
|
|
|
|
|
:param queryset: The queryset to get the log entries for.
|
|
|
|
|
:type queryset: QuerySet
|
|
|
|
|
:return: The LogEntry objects for the objects in the given queryset.
|
|
|
|
|
:rtype: QuerySet
|
|
|
|
|
"""
|
|
|
|
|
if not isinstance(queryset, QuerySet) or queryset.count() == 0:
|
|
|
|
|
return self.none()
|
|
|
|
|
|
|
|
|
|
content_type = ContentType.objects.get_for_model(queryset.model)
|
2017-06-29 14:19:33 +00:00
|
|
|
primary_keys = list(queryset.values_list(queryset.model._meta.pk.name, flat=True))
|
2015-05-15 13:14:57 +00:00
|
|
|
|
2016-01-05 16:59:43 +00:00
|
|
|
if isinstance(primary_keys[0], integer_types):
|
|
|
|
|
return self.filter(content_type=content_type).filter(Q(object_id__in=primary_keys)).distinct()
|
2017-06-29 14:19:33 +00:00
|
|
|
elif isinstance(queryset.model._meta.pk, models.UUIDField):
|
|
|
|
|
primary_keys = [smart_text(pk) for pk in primary_keys]
|
|
|
|
|
return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct()
|
2016-01-05 16:59:43 +00:00
|
|
|
else:
|
|
|
|
|
return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct()
|
|
|
|
|
|
2013-12-18 16:17:38 +00:00
|
|
|
def get_for_model(self, model):
|
|
|
|
|
"""
|
|
|
|
|
Get log entries for all objects of a specified type.
|
2015-05-31 13:06:06 +00:00
|
|
|
|
|
|
|
|
:param model: The model to get log entries for.
|
|
|
|
|
:type model: class
|
|
|
|
|
:return: QuerySet of log entries for the given model.
|
|
|
|
|
:rtype: QuerySet
|
2013-12-18 16:17:38 +00:00
|
|
|
"""
|
2015-02-16 16:06:17 +00:00
|
|
|
# Return empty queryset if the given object is not valid.
|
|
|
|
|
if not issubclass(model, models.Model):
|
|
|
|
|
return self.none()
|
|
|
|
|
|
2013-11-28 04:04:49 +00:00
|
|
|
content_type = ContentType.objects.get_for_model(model)
|
2015-02-16 16:06:17 +00:00
|
|
|
|
2013-12-18 16:17:38 +00:00
|
|
|
return self.filter(content_type=content_type)
|
2013-10-20 13:25:48 +00:00
|
|
|
|
2013-12-18 16:24:41 +00:00
|
|
|
def _get_pk_value(self, instance):
|
|
|
|
|
"""
|
|
|
|
|
Get the primary key field value for a model instance.
|
2015-05-31 13:06:06 +00:00
|
|
|
|
|
|
|
|
:param instance: The model instance to get the primary key for.
|
|
|
|
|
:type instance: Model
|
|
|
|
|
:return: The primary key value of the given model instance.
|
2013-12-18 16:24:41 +00:00
|
|
|
"""
|
|
|
|
|
pk_field = instance._meta.pk.name
|
2015-02-12 20:54:59 +00:00
|
|
|
pk = getattr(instance, pk_field, None)
|
|
|
|
|
|
2015-02-19 14:03:39 +00:00
|
|
|
# Check to make sure that we got an pk not a model object.
|
|
|
|
|
if isinstance(pk, models.Model):
|
|
|
|
|
pk = self._get_pk_value(pk)
|
2015-02-12 20:54:59 +00:00
|
|
|
return pk
|
2013-12-18 16:24:41 +00:00
|
|
|
|
2013-10-20 13:25:48 +00:00
|
|
|
|
2015-02-16 21:17:22 +00:00
|
|
|
@python_2_unicode_compatible
|
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:
|
2014-03-14 16:15:31 +00:00
|
|
|
"""
|
|
|
|
|
The actions that Auditlog distinguishes: creating, updating and deleting objects. Viewing objects is not logged.
|
|
|
|
|
The values of the actions are numeric, a higher integer value means a more intrusive action. This may be useful
|
2015-05-31 13:06:06 +00:00
|
|
|
in some cases when comparing actions because the ``__lt``, ``__lte``, ``__gt``, ``__gte`` lookup filters can be
|
|
|
|
|
used in queries.
|
|
|
|
|
|
|
|
|
|
The valid actions are :py:attr:`Action.CREATE`, :py:attr:`Action.UPDATE` and :py:attr:`Action.DELETE`.
|
2014-03-14 16:15:31 +00:00
|
|
|
"""
|
2013-10-20 13:25:48 +00:00
|
|
|
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"))
|
2016-01-23 21:38:02 +00:00
|
|
|
object_pk = models.CharField(db_index=True, max_length=255, verbose_name=_("object pk"))
|
2015-02-16 20:46:35 +00:00
|
|
|
object_id = models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name=_("object id"))
|
2013-10-20 13:25:48 +00:00
|
|
|
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"))
|
2015-06-03 14:45:51 +00:00
|
|
|
remote_addr = models.GenericIPAddressField(blank=True, null=True, verbose_name=_("remote address"))
|
2013-10-20 13:25:48 +00:00
|
|
|
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp"))
|
2015-06-03 15:47:50 +00:00
|
|
|
additional_data = JSONField(blank=True, null=True, verbose_name=_("additional data"))
|
2013-10-20 13:25:48 +00:00
|
|
|
|
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")
|
|
|
|
|
|
2015-02-16 21:17:22 +00:00
|
|
|
def __str__(self):
|
2013-10-20 13:25:48 +00:00
|
|
|
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):
|
2013-12-18 16:27:11 +00:00
|
|
|
"""
|
2015-05-14 23:25:44 +00:00
|
|
|
:return: The changes recorded in this log entry as a dictionary object.
|
2013-12-18 16:27:11 +00:00
|
|
|
"""
|
2013-12-11 16:14:07 +00:00
|
|
|
try:
|
|
|
|
|
return json.loads(self.changes)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
@property
|
2015-02-16 21:17:22 +00:00
|
|
|
def changes_str(self, colon=': ', arrow=smart_text(' \u2192 '), separator='; '):
|
2013-12-18 16:27:11 +00:00
|
|
|
"""
|
|
|
|
|
Return the changes recorded in this log entry as a string. The formatting of the string can be customized by
|
|
|
|
|
setting alternate values for colon, arrow and separator. If the formatting is still not satisfying, please use
|
2015-05-31 13:06:06 +00:00
|
|
|
:py:func:`LogEntry.changes_dict` and format the string yourself.
|
2015-05-14 23:25:44 +00:00
|
|
|
|
|
|
|
|
:param colon: The string to place between the field name and the values.
|
|
|
|
|
:param arrow: The string to place between each old and new value.
|
|
|
|
|
:param separator: The string to place between each field.
|
|
|
|
|
:return: A readable string of the changes in this log entry.
|
2013-12-18 16:27:11 +00:00
|
|
|
"""
|
2013-12-11 16:14:07 +00:00
|
|
|
substrings = []
|
|
|
|
|
|
2015-02-16 21:17:22 +00:00
|
|
|
for field, values in iteritems(self.changes_dict):
|
|
|
|
|
substring = smart_text('{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}').format(
|
2013-12-18 16:17:38 +00:00
|
|
|
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)
|
2014-10-03 13:13:03 +00:00
|
|
|
|
2017-09-13 14:57:47 +00:00
|
|
|
@property
|
|
|
|
|
def changes_display_dict(self):
|
|
|
|
|
"""
|
|
|
|
|
:return: The changes recorded in this log entry intended for display to users as a dictionary object.
|
|
|
|
|
"""
|
|
|
|
|
# Get the model and model_fields
|
|
|
|
|
from auditlog.registry import auditlog
|
|
|
|
|
model = self.content_type.model_class()
|
|
|
|
|
model_fields = auditlog.get_model_fields(model._meta.model)
|
|
|
|
|
changes_display_dict = {}
|
|
|
|
|
# grab the changes_dict and iterate through
|
|
|
|
|
for field_name, values in iteritems(self.changes_dict):
|
|
|
|
|
# try to get the field attribute on the model
|
|
|
|
|
try:
|
|
|
|
|
field = model._meta.get_field(field_name)
|
|
|
|
|
except FieldDoesNotExist:
|
|
|
|
|
changes_display_dict[field_name] = values
|
|
|
|
|
continue
|
|
|
|
|
values_display = []
|
|
|
|
|
# handle choices fields and Postgres ArrayField to get human readable version
|
2018-01-02 17:28:02 +00:00
|
|
|
choices_dict = None
|
|
|
|
|
if hasattr(field, 'choices'):
|
|
|
|
|
choices_dict = dict(field.choices)
|
|
|
|
|
elif hasattr(field, 'base_field') and getattr(field.base_field, 'choices', False):
|
|
|
|
|
choices_dict = dict(field.base_field.choices)
|
|
|
|
|
|
|
|
|
|
if choices_dict:
|
2017-09-13 14:57:47 +00:00
|
|
|
for value in values:
|
|
|
|
|
try:
|
|
|
|
|
value = ast.literal_eval(value)
|
|
|
|
|
if type(value) is [].__class__:
|
|
|
|
|
values_display.append(', '.join([choices_dict.get(val, 'None') for val in value]))
|
|
|
|
|
else:
|
|
|
|
|
values_display.append(choices_dict.get(value, 'None'))
|
|
|
|
|
except ValueError:
|
|
|
|
|
values_display.append(choices_dict.get(value, 'None'))
|
|
|
|
|
except:
|
|
|
|
|
values_display.append(choices_dict.get(value, 'None'))
|
|
|
|
|
else:
|
2018-01-02 17:28:37 +00:00
|
|
|
try:
|
|
|
|
|
field_type = field.get_internal_type()
|
|
|
|
|
except AttributeError:
|
|
|
|
|
# if the field is a relationship it has no internal type and exclude it
|
|
|
|
|
continue
|
2017-09-13 14:57:47 +00:00
|
|
|
for value in values:
|
|
|
|
|
# handle case where field is a datetime, date, or time type
|
|
|
|
|
if field_type in ["DateTimeField", "DateField", "TimeField"]:
|
|
|
|
|
try:
|
|
|
|
|
value = parser.parse(value)
|
|
|
|
|
if field_type == "DateField":
|
|
|
|
|
value = value.date()
|
|
|
|
|
elif field_type == "TimeField":
|
|
|
|
|
value = value.time()
|
2018-01-02 17:24:49 +00:00
|
|
|
elif field_type == "DateTimeField":
|
|
|
|
|
value = value.replace(tzinfo=pytz.utc)
|
|
|
|
|
value = value.astimezone(gettz(settings.TIME_ZONE))
|
2017-09-13 14:57:47 +00:00
|
|
|
value = formats.localize(value)
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
# check if length is longer than 140 and truncate with ellipsis
|
|
|
|
|
if len(value) > 140:
|
|
|
|
|
value = "{}...".format(value[:140])
|
|
|
|
|
|
|
|
|
|
values_display.append(value)
|
|
|
|
|
verbose_name = model_fields['mapping_fields'].get(field.name, field.verbose_name)
|
|
|
|
|
changes_display_dict[verbose_name] = values_display
|
|
|
|
|
return changes_display_dict
|
|
|
|
|
|
2013-10-20 13:25:48 +00:00
|
|
|
|
2015-08-20 17:53:59 +00:00
|
|
|
class AuditlogHistoryField(GenericRelation):
|
2013-10-20 13:25:48 +00:00
|
|
|
"""
|
2015-08-20 17:53:59 +00:00
|
|
|
A subclass of py:class:`django.contrib.contenttypes.fields.GenericRelation` that sets some default variables. This
|
2015-05-31 13:06:06 +00:00
|
|
|
makes it easier to access Auditlog's log entries, for example in templates.
|
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.
|
2015-05-31 13:06:06 +00:00
|
|
|
However, if you have a non-integer primary key, you can simply pass ``pk_indexable=False`` to the constructor, and
|
2013-10-23 15:23:52 +00:00
|
|
|
Auditlog will fall back to using a non-indexed text based field for this model.
|
2014-03-14 16:15:31 +00:00
|
|
|
|
|
|
|
|
Using this field will not automatically register the model for automatic logging. This is done so you can be more
|
|
|
|
|
flexible with how you use this field.
|
2015-05-31 13:06:06 +00:00
|
|
|
|
|
|
|
|
:param pk_indexable: Whether the primary key for this model is not an :py:class:`int` or :py:class:`long`.
|
|
|
|
|
:type pk_indexable: bool
|
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)
|
2014-07-08 13:57:45 +00:00
|
|
|
|
|
|
|
|
# South compatibility for AuditlogHistoryField
|
|
|
|
|
try:
|
|
|
|
|
from south.modelsinspector import add_introspection_rules
|
2015-02-16 15:34:22 +00:00
|
|
|
add_introspection_rules([], ["^auditlog\.models\.AuditlogHistoryField"])
|
2015-02-16 21:17:22 +00:00
|
|
|
raise DeprecationWarning("South support will be dropped in django-auditlog 0.4.0 or later.")
|
2014-07-08 13:57:45 +00:00
|
|
|
except ImportError:
|
|
|
|
|
pass
|