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 = '' @@ -69,4 +69,5 @@ class LogEntryAdminMixin(object): msg += '
#FieldFromTo
' 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):