diff --git a/auditlog/compat.py b/auditlog/compat.py
deleted file mode 100644
index 086b346..0000000
--- a/auditlog/compat.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import django
-
-def is_authenticated(user):
- """Return whether or not a User is authenticated.
-
- Function provides compatibility following deprecation of method call to
- `is_authenticated()` in Django 2.0.
-
- This is *only* required to support Django < v1.10 (i.e. v1.9 and earlier),
- as `is_authenticated` was introduced as a property in v1.10.s
- """
- if not hasattr(user, 'is_authenticated'):
- return False
- if callable(user.is_authenticated):
- # Will be callable if django.version < 2.0, but is only necessary in
- # v1.9 and earlier due to change introduced in v1.10 making
- # `is_authenticated` a property instead of a callable.
- return user.is_authenticated()
- else:
- return user.is_authenticated
diff --git a/auditlog/diff.py b/auditlog/diff.py
index e5c6221..137d12b 100644
--- a/auditlog/diff.py
+++ b/auditlog/diff.py
@@ -55,6 +55,7 @@ def get_fields_in_model(instance):
def get_field_value(obj, field):
"""
Gets the value of a given model instance field.
+
:param obj: The model instance.
:type obj: Model
:param field: The field you want to find the value of.
@@ -64,7 +65,7 @@ def get_field_value(obj, field):
"""
if isinstance(field, DateTimeField):
# DateTimeFields are timezone-aware, so we need to convert the field
- # to its naive form before we can accuratly compare them for changes.
+ # to its naive form before we can accurately compare them for changes.
try:
value = field.to_python(getattr(obj, field.name, None))
if value is not None and settings.USE_TZ and not timezone.is_naive(value):
@@ -95,9 +96,9 @@ def model_instance_diff(old, new):
"""
from auditlog.registry import auditlog
- if not(old is None or isinstance(old, Model)):
+ 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)):
+ if not (new is None or isinstance(new, Model)):
raise TypeError("The supplied new instance is not a valid model instance.")
diff = {}
diff --git a/auditlog/middleware.py b/auditlog/middleware.py
index 2a105ab..4ef07f6 100644
--- a/auditlog/middleware.py
+++ b/auditlog/middleware.py
@@ -7,7 +7,6 @@ from django.conf import settings
from django.db.models.signals import pre_save
from django.utils.deprecation import MiddlewareMixin
-from auditlog.compat import is_authenticated
from auditlog.models import LogEntry
threadlocal = threading.local()
@@ -35,7 +34,7 @@ class AuditlogMiddleware(MiddlewareMixin):
threadlocal.auditlog['remote_addr'] = request.META.get('HTTP_X_FORWARDED_FOR').split(',')[0]
# Connect signal for automatic logging
- if hasattr(request, 'user') and is_authenticated(request.user):
+ if hasattr(request, 'user') and getattr(request.user, 'is_authenticated', False):
set_actor = partial(self.set_actor, user=request.user, signal_duid=threadlocal.auditlog['signal_duid'])
pre_save.connect(set_actor, sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'], weak=False)
diff --git a/auditlog/mixins.py b/auditlog/mixins.py
index 5a0b829..7d4635d 100644
--- a/auditlog/mixins.py
+++ b/auditlog/mixins.py
@@ -1,17 +1,13 @@
import json
+from django import urls as urlresolvers
from django.conf import settings
-try:
- from django.core import urlresolvers
-except ImportError:
- from django import urls as urlresolvers
-try:
- from django.urls.exceptions import NoReverseMatch
-except ImportError:
- from django.core.urlresolvers import NoReverseMatch
+from django.urls.exceptions import NoReverseMatch
from django.utils.html import format_html
from django.utils.safestring import mark_safe
+from auditlog.models import LogEntry
+
MAX = 75
@@ -19,6 +15,7 @@ class LogEntryAdminMixin(object):
def created(self, obj):
return obj.timestamp.strftime('%Y-%m-%d %H:%M:%S')
+
created.short_description = 'Created'
def user_url(self, obj):
@@ -32,6 +29,7 @@ class LogEntryAdminMixin(object):
return format_html(u'{}', link, obj.actor)
return 'system'
+
user_url.short_description = 'User'
def resource_url(self, obj):
@@ -44,10 +42,11 @@ class LogEntryAdminMixin(object):
return obj.object_repr
else:
return format_html(u'{}', link, obj.object_repr)
+
resource_url.short_description = 'Resource'
def msg_short(self, obj):
- if obj.action == 2:
+ if obj.action == LogEntry.Action.DELETE:
return '' # delete
changes = json.loads(obj.changes)
s = '' if len(changes) == 1 else 's'
@@ -56,10 +55,11 @@ class LogEntryAdminMixin(object):
i = fields.rfind(' ', 0, MAX)
fields = fields[:i] + ' ..'
return '%d change%s: %s' % (len(changes), s, fields)
+
msg_short.short_description = 'Changes'
def msg(self, obj):
- if obj.action == 2:
+ if obj.action == LogEntry.Action.DELETE:
return '' # delete
changes = json.loads(obj.changes)
msg = '
| # | Field | From | To |
'
@@ -69,4 +69,5 @@ class LogEntryAdminMixin(object):
msg += '
'
return mark_safe(msg)
+
msg.short_description = 'Changes'
diff --git a/auditlog/registry.py b/auditlog/registry.py
index e5e34ff..538f5dd 100644
--- a/auditlog/registry.py
+++ b/auditlog/registry.py
@@ -1,12 +1,19 @@
-from django.db.models.signals import pre_save, post_save, post_delete
+from typing import Dict, Callable, Optional, List, Tuple
+
from django.db.models import Model
+from django.db.models.base import ModelBase
+from django.db.models.signals import pre_save, post_save, post_delete, ModelSignal
+
+DispatchUID = Tuple[int, str, int]
class AuditlogModelRegistry(object):
"""
A registry that keeps track of the models that use Auditlog to track changes.
"""
- def __init__(self, create=True, update=True, delete=True, custom=None):
+
+ def __init__(self, create: bool = True, update: bool = True, delete: bool = True,
+ custom: Optional[Dict[ModelSignal, Callable]] = None):
from auditlog.receivers import log_create, log_update, log_delete
self._registry = {}
@@ -22,17 +29,25 @@ class AuditlogModelRegistry(object):
if custom is not None:
self._signals.update(custom)
- def register(self, model=None, include_fields=[], exclude_fields=[], mapping_fields={}):
+ def register(self, model: ModelBase = None, include_fields: Optional[List[str]] = None,
+ exclude_fields: Optional[List[str]] = None, mapping_fields: Optional[Dict[str, str]] = None):
"""
Register a model with auditlog. Auditlog will then track mutations on this model's instances.
:param model: The model to register.
- :type model: Model
:param include_fields: The fields to include. Implicitly excludes all other fields.
- :type include_fields: list
:param exclude_fields: The fields to exclude. Overrides the fields to include.
- :type exclude_fields: list
+ :param mapping_fields: Mapping from field names to strings in diff.
+
"""
+
+ if include_fields is None:
+ include_fields = []
+ if exclude_fields is None:
+ exclude_fields = []
+ if mapping_fields is None:
+ mapping_fields = {}
+
def registrar(cls):
"""Register models for a given class."""
if not issubclass(cls, Model):
@@ -58,23 +73,21 @@ class AuditlogModelRegistry(object):
# Otherwise, just register the model.
registrar(model)
- def contains(self, model):
+ def contains(self, model: ModelBase) -> bool:
"""
Check if a model is registered with auditlog.
:param model: The model to check.
- :type model: Model
:return: Whether the model has been registered.
:rtype: bool
"""
return model in self._registry
- def unregister(self, model):
+ def unregister(self, model: ModelBase) -> None:
"""
Unregister a model with auditlog. This will not affect the database.
:param model: The model to unregister.
- :type model: Model
"""
try:
del self._registry[model]
@@ -83,6 +96,16 @@ class AuditlogModelRegistry(object):
else:
self._disconnect_signals(model)
+ def get_models(self) -> List[ModelBase]:
+ return list(self._registry.keys())
+
+ def get_model_fields(self, model: ModelBase):
+ return {
+ 'include_fields': list(self._registry[model]['include_fields']),
+ 'exclude_fields': list(self._registry[model]['exclude_fields']),
+ 'mapping_fields': dict(self._registry[model]['mapping_fields']),
+ }
+
def _connect_signals(self, model):
"""
Connect signals for the model.
@@ -98,24 +121,11 @@ class AuditlogModelRegistry(object):
for signal, receiver in self._signals.items():
signal.disconnect(sender=model, dispatch_uid=self._dispatch_uid(signal, model))
- def _dispatch_uid(self, signal, model):
+ def _dispatch_uid(self, signal, model) -> DispatchUID:
"""
Generate a dispatch_uid.
"""
- return (self.__class__, model, signal)
-
- def get_model_fields(self, model):
- return {
- 'include_fields': self._registry[model]['include_fields'],
- 'exclude_fields': self._registry[model]['exclude_fields'],
- 'mapping_fields': self._registry[model]['mapping_fields'],
- }
-
-
-class AuditLogModelRegistry(AuditlogModelRegistry):
- def __init__(self, *args, **kwargs):
- super(AuditLogModelRegistry, self).__init__(*args, **kwargs)
- raise DeprecationWarning("Use AuditlogModelRegistry instead of AuditLogModelRegistry, AuditLogModelRegistry will be removed in django-auditlog 0.4.0 or later.")
+ return self.__hash__(), model.__qualname__, signal.__hash__()
auditlog = AuditlogModelRegistry()
diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py
index 7e7b198..9993f76 100644
--- a/auditlog_tests/tests.py
+++ b/auditlog_tests/tests.py
@@ -17,7 +17,6 @@ from auditlog_tests.models import SimpleModel, AltPrimaryKeyModel, UUIDPrimaryKe
ProxyModel, SimpleIncludeModel, SimpleExcludeModel, SimpleMappingModel, RelatedModel, \
ManyRelatedModel, AdditionalDataIncludedModel, DateTimeFieldModel, ChoicesFieldModel, \
CharfieldTextfieldModel, PostgresArrayFieldModel, NoDeleteHistoryModel
-from auditlog import compat
class SimpleModelTest(TestCase):
@@ -586,44 +585,6 @@ class PostgresArrayFieldModelTest(TestCase):
msg="The human readable text 'Green' is displayed.")
-class CompatibilityTest(TestCase):
- """Test case for compatibility functions."""
-
- def test_is_authenticated(self):
- """Test that the 'is_authenticated' compatibility function is working.
-
- Bit of explanation: the `is_authenticated` property on request.user is
- *always* set to 'False' for AnonymousUser, and it is *always* set to
- 'True' for *any* other (i.e. identified/authenticated) user.
-
- So, the logic of this test is to ensure that compat.is_authenticated()
- returns the correct value based on whether or not the User is an
- anonymous user (simulating what goes on in the real request.user).
-
- """
-
- # Test compat.is_authenticated for anonymous users
- self.user = auth.get_user(self.client)
- if django.VERSION < (1, 10):
- assert self.user.is_anonymous()
- else:
- assert self.user.is_anonymous
- assert not compat.is_authenticated(self.user)
-
- # Setup some other user, which is *not* anonymous, and check
- # compat.is_authenticated
- self.user = User.objects.create(
- username="test.user",
- email="test.user@mail.com",
- password="auditlog"
- )
- if django.VERSION < (1, 10):
- assert not self.user.is_anonymous()
- else:
- assert not self.user.is_anonymous
- assert compat.is_authenticated(self.user)
-
-
class AdminPanelTest(TestCase):
@classmethod
def setUpTestData(cls):