diff --git a/.travis.yml b/.travis.yml index 3b9e431..2707a70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,40 +11,23 @@ addons: matrix: include: - - python: 2.7 - env: TOXENV=py27-django-111 - - - python: 3.4 - env: TOXENV=py34-django-111 - - python: 3.4 - env: TOXENV=py34-django-20 - - - python: 3.5 - env: TOXENV=py35-django-111 - - python: 3.5 - env: TOXENV=py35-django-20 - - python: 3.5 - env: TOXENV=py35-django-21 - python: 3.5 env: TOXENV=py35-django-22 - - python: 3.6 - env: TOXENV=py36-django-111 - - python: 3.6 - env: TOXENV=py36-django-20 - - python: 3.6 - env: TOXENV=py36-django-21 - python: 3.6 env: TOXENV=py36-django-22 + - python: 3.6 + env: TOXENV=py36-django-30 - - python: 3.7 - env: TOXENV=py37-django-111 - - python: 3.7 - env: TOXENV=py37-django-20 - - python: 3.7 - env: TOXENV=py37-django-21 - python: 3.7 env: TOXENV=py37-django-22 + - python: 3.7 + env: TOXENV=py37-django-30 + + - python: 3.8 + env: TOXENV=py38-django-22 + - python: 3.8 + env: TOXENV=py38-django-30 fast_finish: true @@ -61,5 +44,5 @@ deploy: on: repo: jjkester/django-auditlog branch: stable - condition: $TOXENV = py36-django-20 + condition: $TOXENV = py38-django-30 edge: true diff --git a/auditlog/__init__.py b/auditlog/__init__.py index 3dd296c..b1dd7f3 100644 --- a/auditlog/__init__.py +++ b/auditlog/__init__.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - __version__ = '1.0a1' default_app_config = 'auditlog.apps.AuditlogConfig' diff --git a/auditlog/apps.py b/auditlog/apps.py index 9704ed9..d7629f0 100644 --- a/auditlog/apps.py +++ b/auditlog/apps.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.apps import AppConfig diff --git a/auditlog/diff.py b/auditlog/diff.py index 9f996f6..e5c6221 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db.models import Model, NOT_PROVIDED, DateTimeField diff --git a/auditlog/management/commands/auditlogflush.py b/auditlog/management/commands/auditlogflush.py index 575fd39..155ba96 100644 --- a/auditlog/management/commands/auditlogflush.py +++ b/auditlog/management/commands/auditlogflush.py @@ -1,5 +1,4 @@ from django.core.management.base import BaseCommand -from six import moves from auditlog.models import LogEntry @@ -11,7 +10,7 @@ class Command(BaseCommand): answer = None while answer not in ['', 'y', 'n']: - answer = moves.input("Are you sure? [y/N]: ").lower().strip() + answer = input("Are you sure? [y/N]: ").lower().strip() if answer == 'y': count = LogEntry.objects.all().count() diff --git a/auditlog/middleware.py b/auditlog/middleware.py index 176de05..2a105ab 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -1,21 +1,14 @@ -from __future__ import unicode_literals - import threading import time +from functools import partial +from django.apps import apps from django.conf import settings 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 django.utils.deprecation import MiddlewareMixin + from auditlog.compat import is_authenticated - -# Use MiddlewareMixin when present (Django >= 1.10) -try: - from django.utils.deprecation import MiddlewareMixin -except ImportError: - MiddlewareMixin = object - +from auditlog.models import LogEntry threadlocal = threading.local() @@ -43,7 +36,7 @@ class AuditlogMiddleware(MiddlewareMixin): # Connect signal for automatic logging if hasattr(request, 'user') and is_authenticated(request.user): - set_actor = curry(self.set_actor, user=request.user, signal_duid=threadlocal.auditlog['signal_duid']) + 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) def process_response(self, request, response): diff --git a/auditlog/migrations/0001_initial.py b/auditlog/migrations/0001_initial.py index 5a96248..7abec05 100644 --- a/auditlog/migrations/0001_initial.py +++ b/auditlog/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations import django.db.models.deletion from django.conf import settings diff --git a/auditlog/migrations/0002_auto_support_long_primary_keys.py b/auditlog/migrations/0002_auto_support_long_primary_keys.py index 5ea87f6..e34b3c9 100644 --- a/auditlog/migrations/0002_auto_support_long_primary_keys.py +++ b/auditlog/migrations/0002_auto_support_long_primary_keys.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/auditlog/migrations/0003_logentry_remote_addr.py b/auditlog/migrations/0003_logentry_remote_addr.py index 948f63c..adf2c89 100644 --- a/auditlog/migrations/0003_logentry_remote_addr.py +++ b/auditlog/migrations/0003_logentry_remote_addr.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/auditlog/migrations/0004_logentry_detailed_object_repr.py b/auditlog/migrations/0004_logentry_detailed_object_repr.py index 2b9af2c..d6c8ee7 100644 --- a/auditlog/migrations/0004_logentry_detailed_object_repr.py +++ b/auditlog/migrations/0004_logentry_detailed_object_repr.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations import jsonfield.fields diff --git a/auditlog/migrations/0005_logentry_additional_data_verbose_name.py b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py index 7837a7c..6554289 100644 --- a/auditlog/migrations/0005_logentry_additional_data_verbose_name.py +++ b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models import jsonfield.fields diff --git a/auditlog/migrations/0006_object_pk_index.py b/auditlog/migrations/0006_object_pk_index.py index 273e6bd..ac431c0 100644 --- a/auditlog/migrations/0006_object_pk_index.py +++ b/auditlog/migrations/0006_object_pk_index.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/auditlog/migrations/0007_object_pk_type.py b/auditlog/migrations/0007_object_pk_type.py index 3a724e8..275db7e 100644 --- a/auditlog/migrations/0007_object_pk_type.py +++ b/auditlog/migrations/0007_object_pk_type.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/auditlog/models.py b/auditlog/models.py index 0832f2c..4d69387 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -1,8 +1,8 @@ -from __future__ import unicode_literals - -import json import ast +import json +from dateutil import parser +from dateutil.tz import gettz from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType @@ -10,13 +10,9 @@ from django.core.exceptions import FieldDoesNotExist from django.db import models, DEFAULT_DB_ALIAS from django.db.models import QuerySet, Q from django.utils import formats, timezone -from django.utils.encoding import python_2_unicode_compatible, smart_text -from django.utils.six import iteritems, integer_types +from django.utils.encoding import smart_str from django.utils.translation import ugettext_lazy as _ - from jsonfield.fields import JSONField -from dateutil import parser -from dateutil.tz import gettz class LogEntryManager(models.Manager): @@ -41,9 +37,9 @@ class LogEntryManager(models.Manager): if changes is not None: kwargs.setdefault('content_type', ContentType.objects.get_for_model(instance)) kwargs.setdefault('object_pk', pk) - kwargs.setdefault('object_repr', smart_text(instance)) + kwargs.setdefault('object_repr', smart_str(instance)) - if isinstance(pk, integer_types): + if isinstance(pk, int): kwargs.setdefault('object_id', pk) get_additional_data = getattr(instance, 'get_additional_data', None) @@ -53,7 +49,9 @@ class LogEntryManager(models.Manager): # Delete log entries with the same pk as a newly created model. This should only be necessary when an pk is # used twice. if kwargs.get('action', None) is LogEntry.Action.CREATE: - 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(): + 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() else: self.filter(content_type=kwargs.get('content_type'), object_pk=kwargs.get('object_pk', '')).delete() @@ -78,10 +76,10 @@ class LogEntryManager(models.Manager): content_type = ContentType.objects.get_for_model(instance.__class__) pk = self._get_pk_value(instance) - if isinstance(pk, integer_types): + if isinstance(pk, int): return self.filter(content_type=content_type, object_id=pk) else: - return self.filter(content_type=content_type, object_pk=smart_text(pk)) + return self.filter(content_type=content_type, object_pk=smart_str(pk)) def get_for_objects(self, queryset): """ @@ -98,10 +96,10 @@ class LogEntryManager(models.Manager): content_type = ContentType.objects.get_for_model(queryset.model) primary_keys = list(queryset.values_list(queryset.model._meta.pk.name, flat=True)) - if isinstance(primary_keys[0], integer_types): + if isinstance(primary_keys[0], int): return self.filter(content_type=content_type).filter(Q(object_id__in=primary_keys)).distinct() elif isinstance(queryset.model._meta.pk, models.UUIDField): - primary_keys = [smart_text(pk) for pk in primary_keys] + primary_keys = [smart_str(pk) for pk in primary_keys] return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct() else: return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct() @@ -140,7 +138,6 @@ class LogEntryManager(models.Manager): return pk -@python_2_unicode_compatible class LogEntry(models.Model): """ Represents an entry in the audit log. The content type is saved along with the textual and numeric (if available) @@ -171,13 +168,15 @@ class LogEntry(models.Model): (DELETE, _("delete")), ) - content_type = models.ForeignKey(to='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(to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, blank=True, null=True, 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")) @@ -213,7 +212,7 @@ class LogEntry(models.Model): return {} @property - def changes_str(self, colon=': ', arrow=smart_text(' \u2192 '), separator='; '): + def changes_str(self, colon=': ', arrow=' \u2192 ', separator='; '): """ 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 @@ -226,8 +225,8 @@ class LogEntry(models.Model): """ substrings = [] - for field, values in iteritems(self.changes_dict): - substring = smart_text('{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}').format( + for field, values in self.changes_dict.items(): + substring = '{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}'.format( field_name=field, colon=colon, old=values[0], @@ -249,7 +248,7 @@ class LogEntry(models.Model): 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): + for field_name, values in self.changes_dict.items(): # try to get the field attribute on the model try: field = model._meta.get_field(field_name) @@ -355,6 +354,7 @@ class AuditlogHistoryField(GenericRelation): # South compatibility for AuditlogHistoryField try: from south.modelsinspector import add_introspection_rules + add_introspection_rules([], ["^auditlog\.models\.AuditlogHistoryField"]) raise DeprecationWarning("South support will be dropped in django-auditlog 0.4.0 or later.") except ImportError: diff --git a/auditlog/receivers.py b/auditlog/receivers.py index 9ee4cf3..25a6228 100644 --- a/auditlog/receivers.py +++ b/auditlog/receivers.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import json from auditlog.diff import model_instance_diff diff --git a/auditlog/registry.py b/auditlog/registry.py index 124fd3c..e5e34ff 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -1,8 +1,5 @@ -from __future__ import unicode_literals - from django.db.models.signals import pre_save, post_save, post_delete from django.db.models import Model -from django.utils.six import iteritems class AuditlogModelRegistry(object): diff --git a/auditlog_tests/models.py b/auditlog_tests/models.py index 50b5833..4eda784 100644 --- a/auditlog_tests/models.py +++ b/auditlog_tests/models.py @@ -5,8 +5,6 @@ from django.db import models from auditlog.models import AuditlogHistoryField from auditlog.registry import auditlog -from multiselectfield import MultiSelectField - @auditlog.register() class SimpleModel(models.Model): @@ -171,7 +169,6 @@ class ChoicesFieldModel(models.Model): ) status = models.CharField(max_length=1, choices=STATUS_CHOICES) - multiselect = MultiSelectField(max_length=3, choices=STATUS_CHOICES, max_choices=3) multiplechoice = models.CharField(max_length=255, choices=STATUS_CHOICES) history = AuditlogHistoryField() diff --git a/auditlog_tests/test_settings.py b/auditlog_tests/test_settings.py index 7bb0dc6..fdd2f2a 100644 --- a/auditlog_tests/test_settings.py +++ b/auditlog_tests/test_settings.py @@ -1,7 +1,6 @@ """ Settings file for the Auditlog test suite. """ -import django SECRET_KEY = 'test' @@ -13,10 +12,9 @@ INSTALLED_APPS = [ 'django.contrib.admin', 'auditlog', 'auditlog_tests', - 'multiselectfield', ] -middlewares = ( +MIDDLEWARE = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -24,19 +22,9 @@ middlewares = ( 'auditlog.middleware.AuditlogMiddleware', ) -if django.VERSION < (1, 10): - MIDDLEWARE_CLASSES = middlewares -else: - MIDDLEWARE = middlewares - -if django.VERSION <= (1, 9): - POSTGRES_DRIVER = 'django.db.backends.postgresql_psycopg2' -else: - POSTGRES_DRIVER = 'django.db.backends.postgresql' - DATABASES = { 'default': { - 'ENGINE': POSTGRES_DRIVER, + 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'auditlog_tests_db', 'USER': 'postgres', 'PASSWORD': '', diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index d13f72f..7e7b198 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -506,7 +506,6 @@ class ChoicesFieldModelTest(TestCase): def setUp(self): self.obj = ChoicesFieldModel.objects.create( status=ChoicesFieldModel.RED, - multiselect=[ChoicesFieldModel.RED, ChoicesFieldModel.GREEN], multiplechoice=[ChoicesFieldModel.RED, ChoicesFieldModel.YELLOW, ChoicesFieldModel.GREEN], ) @@ -518,22 +517,6 @@ class ChoicesFieldModelTest(TestCase): self.obj.save() self.assertTrue(self.obj.history.latest().changes_display_dict["status"][1] == "Green", msg="The human readable text 'Green' is displayed.") - def test_changes_display_dict_multiselect(self): - self.assertTrue(self.obj.history.latest().changes_display_dict["multiselect"][1] == "Red, Green", - msg="The human readable text for the two choices, 'Red, Green' is displayed.") - self.obj.multiselect = ChoicesFieldModel.GREEN - self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["multiselect"][1] == "Green", - msg="The human readable text 'Green' is displayed.") - self.obj.multiselect = None - self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["multiselect"][1] == "None", - msg="The human readable text 'None' is displayed.") - self.obj.multiselect = ChoicesFieldModel.GREEN - self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["multiselect"][1] == "Green", - msg="The human readable text 'Green' is displayed.") - def test_changes_display_dict_multiplechoice(self): self.assertTrue(self.obj.history.latest().changes_display_dict["multiplechoice"][1] == "Red, Yellow, Green", msg="The human readable text 'Red, Yellow, Green' is displayed.") diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 0db8706..9f7c2a8 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -11,10 +11,10 @@ The repository can be found at https://github.com/jjkester/django-auditlog/. **Requirements** -- Python 2.7, 3.4 or higher -- Django 1.8 or higher +- Python 3.5 or higher +- Django 2.2 or higher -Auditlog is currently tested with Python 2.7 and 3.4 and Django 1.8, 1.9 and 1.10. The latest test report can be found +Auditlog is currently tested with Python 3.5 - 3.8 and Django 2.2 and 3.0. The latest test report can be found at https://travis-ci.org/jjkester/django-auditlog. Adding Auditlog to your Django application diff --git a/requirements.txt b/requirements.txt index 4ad0782..7616965 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # Library requirements -django-jsonfield>=1.0.0 -python-dateutil==2.6.0 +django-jsonfield +python-dateutil # Build requirements setuptools @@ -11,8 +11,7 @@ sphinx sphinx_rtd_theme # Test requirements -coverage==4.3.4 -tox>=1.7.0 -codecov>=2.0.0 -django-multiselectfield==0.1.8 +coverage +tox +codecov psycopg2-binary diff --git a/tox.ini b/tox.ini index 6f2cdd7..16f700f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,23 +1,18 @@ [tox] envlist = - {py27,py34,py35,py36,py37}-django-111 - {py34,py35,py36,py37}-django-20 - {py35,py36,py37}-django-21 - {py35,py36,py37}-django-22 + {py35,py36,py37,py38}-django-22 + {py36,py37,py38}-django-30 [testenv] setenv = PYTHONPATH = {toxinidir}:{toxinidir}/auditlog commands = coverage run --source auditlog runtests.py deps = - django-111: Django>=1.11,<2.0 - django-20: Django>=2.0,<2.1 - django-21: Django>=2.1,<2.2 django-22: Django>=2.2,<2.3 + django-30: Django>=3.0,<3.1 -r{toxinidir}/requirements.txt basepython = + py38: python3.8 py37: python3.7 py36: python3.6 py35: python3.5 - py34: python3.4 - py27: python2.7