diff --git a/MANIFEST b/MANIFEST index fcb8944..f2ed177 100644 --- a/MANIFEST +++ b/MANIFEST @@ -3,6 +3,7 @@ setup.py src/auditlog/__init__.py src/auditlog/admin.py src/auditlog/apps.py +src/auditlog/compat.py src/auditlog/diff.py src/auditlog/filters.py src/auditlog/middleware.py @@ -10,6 +11,9 @@ src/auditlog/mixins.py src/auditlog/models.py src/auditlog/receivers.py src/auditlog/registry.py +src/auditlog/management/__init__.py +src/auditlog/management/commands/__init__.py +src/auditlog/management/commands/auditlogflush.py src/auditlog/migrations/0001_initial.py src/auditlog/migrations/0002_auto_support_long_primary_keys.py src/auditlog/migrations/0003_logentry_remote_addr.py diff --git a/src/auditlog/compat.py b/src/auditlog/compat.py new file mode 100644 index 0000000..086b346 --- /dev/null +++ b/src/auditlog/compat.py @@ -0,0 +1,20 @@ +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/src/auditlog/middleware.py b/src/auditlog/middleware.py index 7780bae..a266fee 100644 --- a/src/auditlog/middleware.py +++ b/src/auditlog/middleware.py @@ -8,6 +8,7 @@ from django.db.models.signals import pre_save from django.utils.functional import curry from django.apps import apps from auditlog.models import LogEntry +from auditlog.compat import is_authenticated # Use MiddlewareMixin when present (Django >= 1.10) try: @@ -41,7 +42,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 hasattr(request.user, 'is_authenticated') and request.user.is_authenticated(): + if hasattr(request, 'user') and is_authenticated(request.user): set_actor = curry(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/src/auditlog/migrations/0001_initial.py b/src/auditlog/migrations/0001_initial.py index b973bb3..5a96248 100644 --- a/src/auditlog/migrations/0001_initial.py +++ b/src/auditlog/migrations/0001_initial.py @@ -25,7 +25,7 @@ class Migration(migrations.Migration): ('changes', models.TextField(verbose_name='change message', blank=True)), ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='timestamp')), ('actor', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, verbose_name='actor', blank=True, to=settings.AUTH_USER_MODEL, null=True)), - ('content_type', models.ForeignKey(related_name='+', verbose_name='content type', to='contenttypes.ContentType')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', verbose_name='content type', to='contenttypes.ContentType')), ], options={ 'ordering': ['-timestamp'], diff --git a/src/auditlog/mixins.py b/src/auditlog/mixins.py index 3022b6d..3bea200 100644 --- a/src/auditlog/mixins.py +++ b/src/auditlog/mixins.py @@ -1,7 +1,10 @@ import json from django.conf import settings -from django.core import urlresolvers +try: + from django.core import urlresolvers +except ImportError: + from django import urls as urlresolvers try: from django.urls.exceptions import NoReverseMatch except ImportError: diff --git a/src/auditlog/models.py b/src/auditlog/models.py index d06b408..04a14ef 100644 --- a/src/auditlog/models.py +++ b/src/auditlog/models.py @@ -171,13 +171,13 @@ class LogEntry(models.Model): (DELETE, _("delete")), ) - content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE, related_name='+', verbose_name=_("content type")) + content_type = models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE, related_name='+', verbose_name=_("content type")) object_pk = models.CharField(db_index=True, max_length=255, verbose_name=_("object pk")) object_id = models.BigIntegerField(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")) - actor = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_("actor")) + actor = models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, blank=True, null=True, related_name='+', verbose_name=_("actor")) remote_addr = models.GenericIPAddressField(blank=True, null=True, verbose_name=_("remote address")) timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp")) additional_data = JSONField(blank=True, null=True, verbose_name=_("additional data")) diff --git a/src/auditlog_tests/models.py b/src/auditlog_tests/models.py index 2311b80..d938bfe 100644 --- a/src/auditlog_tests/models.py +++ b/src/auditlog_tests/models.py @@ -66,7 +66,7 @@ class RelatedModel(models.Model): A model with a foreign key. """ - related = models.ForeignKey('self') + related = models.ForeignKey(to='self', on_delete=models.CASCADE) history = AuditlogHistoryField() @@ -124,7 +124,7 @@ class AdditionalDataIncludedModel(models.Model): label = models.CharField(max_length=100) text = models.TextField(blank=True) - related = models.ForeignKey(SimpleModel) + related = models.ForeignKey(to=SimpleModel, on_delete=models.CASCADE) history = AuditlogHistoryField() diff --git a/src/auditlog_tests/test_settings.py b/src/auditlog_tests/test_settings.py index 891b5a8..59541a4 100644 --- a/src/auditlog_tests/test_settings.py +++ b/src/auditlog_tests/test_settings.py @@ -8,6 +8,7 @@ SECRET_KEY = 'test' INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', + 'django.contrib.sessions', 'auditlog', 'auditlog_tests', 'multiselectfield', @@ -15,6 +16,7 @@ INSTALLED_APPS = [ MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware' 'auditlog.middleware.AuditlogMiddleware', ) diff --git a/src/auditlog_tests/tests.py b/src/auditlog_tests/tests.py index f323cc0..01dc852 100644 --- a/src/auditlog_tests/tests.py +++ b/src/auditlog_tests/tests.py @@ -1,5 +1,7 @@ import datetime +import django from django.conf import settings +from django.contrib import auth from django.contrib.auth.models import User, AnonymousUser from django.core.exceptions import ValidationError from django.db.models.signals import pre_save @@ -15,6 +17,7 @@ from auditlog_tests.models import SimpleModel, AltPrimaryKeyModel, UUIDPrimaryKe ProxyModel, SimpleIncludeModel, SimpleExcludeModel, SimpleMappingModel, RelatedModel, \ ManyRelatedModel, AdditionalDataIncludedModel, DateTimeFieldModel, ChoicesFieldModel, \ CharfieldTextfieldModel, PostgresArrayFieldModel +from auditlog import compat class SimpleModelTest(TestCase): @@ -581,3 +584,41 @@ class PostgresArrayFieldModelTest(TestCase): self.obj.save() self.assertTrue(self.obj.history.latest().changes_display_dict["arrayfield"][1] == "Green", 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) diff --git a/tox.ini b/tox.ini index c39cb70..1fa28c2 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = {py27,py34,py35,py36}-django-18 {py27,py34,py35,py36}-django-110 {py27,py34,py35,py36}-django-111 + {py34,py35,py36}-django-20 [testenv] setenv = @@ -11,7 +12,8 @@ commands = coverage run --source src/auditlog src/runtests.py deps = django-18: Django>=1.8,<1.9 django-110: Django>=1.10,<1.11 - django-111: Django>=1.11 + django-111: Django>=1.11,<2.0 + django-20: Django>=2.0 -r{toxinidir}/requirements-test.txt basepython = py36: python3.6