Code improvements

This commit is contained in:
Jan-Jelle Kester 2020-08-31 14:14:32 +02:00
parent 228c5949fb
commit 469fe362de
6 changed files with 51 additions and 99 deletions

View file

@ -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

View file

@ -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 = {}

View file

@ -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)

View file

@ -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'<a href="{}">{}</a>', 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'<a href="{}">{}</a>', 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 = '<table><tr><th>#</th><th>Field</th><th>From</th><th>To</th></tr>'
@ -69,4 +69,5 @@ class LogEntryAdminMixin(object):
msg += '</table>'
return mark_safe(msg)
msg.short_description = 'Changes'

View file

@ -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()

View file

@ -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):