From f5bb5cb1a2b08c596fa9f2e233fc0050030202e3 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Sun, 6 Dec 2020 21:29:24 +0100 Subject: [PATCH] Add black and format files with black. --- auditlog/__init__.py | 2 +- auditlog/admin.py | 18 +- auditlog/apps.py | 2 +- auditlog/diff.py | 30 +- auditlog/filters.py | 8 +- auditlog/management/commands/auditlogflush.py | 21 +- auditlog/middleware.py | 55 +- auditlog/migrations/0001_initial.py | 73 ++- .../0002_auto_support_long_primary_keys.py | 10 +- .../migrations/0003_logentry_remote_addr.py | 10 +- .../0004_logentry_detailed_object_repr.py | 6 +- ...5_logentry_additional_data_verbose_name.py | 10 +- auditlog/migrations/0006_object_pk_index.py | 10 +- auditlog/migrations/0007_object_pk_type.py | 10 +- auditlog/mixins.py | 53 +- auditlog/models.py | 151 ++++-- auditlog/registry.py | 38 +- auditlog_tests/__init__.py | 2 +- auditlog_tests/apps.py | 2 +- auditlog_tests/models.py | 47 +- auditlog_tests/test_settings.py | 62 +-- auditlog_tests/tests.py | 479 +++++++++++++----- pyproject.toml | 2 + runtests.py | 2 +- setup.py | 40 +- tox.ini | 8 + 26 files changed, 778 insertions(+), 373 deletions(-) create mode 100644 pyproject.toml diff --git a/auditlog/__init__.py b/auditlog/__init__.py index e369079..b3d77c3 100644 --- a/auditlog/__init__.py +++ b/auditlog/__init__.py @@ -6,4 +6,4 @@ except DistributionNotFound: # package is not installed pass -default_app_config = 'auditlog.apps.AuditlogConfig' +default_app_config = "auditlog.apps.AuditlogConfig" diff --git a/auditlog/admin.py b/auditlog/admin.py index a4c60fc..2f32ccb 100644 --- a/auditlog/admin.py +++ b/auditlog/admin.py @@ -5,13 +5,19 @@ from .filters import ResourceTypeFilter class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): - list_display = ['created', 'resource_url', 'action', 'msg_short', 'user_url'] - search_fields = ['timestamp', 'object_repr', 'changes', 'actor__first_name', 'actor__last_name'] - list_filter = ['action', ResourceTypeFilter] - readonly_fields = ['created', 'resource_url', 'action', 'user_url', 'msg'] + list_display = ["created", "resource_url", "action", "msg_short", "user_url"] + search_fields = [ + "timestamp", + "object_repr", + "changes", + "actor__first_name", + "actor__last_name", + ] + list_filter = ["action", ResourceTypeFilter] + readonly_fields = ["created", "resource_url", "action", "user_url", "msg"] fieldsets = [ - (None, {'fields': ['created', 'user_url', 'resource_url']}), - ('Changes', {'fields': ['action', 'msg']}), + (None, {"fields": ["created", "user_url", "resource_url"]}), + ("Changes", {"fields": ["action", "msg"]}), ] diff --git a/auditlog/apps.py b/auditlog/apps.py index d7629f0..e592f8d 100644 --- a/auditlog/apps.py +++ b/auditlog/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class AuditlogConfig(AppConfig): - name = 'auditlog' + name = "auditlog" verbose_name = "Audit log" diff --git a/auditlog/diff.py b/auditlog/diff.py index e627149..3774e60 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -17,12 +17,16 @@ def track_field(field): :rtype: bool """ from auditlog.models import LogEntry + # Do not track many to many relations if field.many_to_many: return False # Do not track relations to LogEntry - if getattr(field, 'remote_field', None) is not None and field.remote_field.model == LogEntry: + if ( + getattr(field, "remote_field", None) is not None + and field.remote_field.model == LogEntry + ): return False return True @@ -108,16 +112,26 @@ def model_instance_diff(old, new): model_fields = None # Check if fields must be filtered - if model_fields and (model_fields['include_fields'] or model_fields['exclude_fields']) and fields: + if ( + model_fields + and (model_fields["include_fields"] or model_fields["exclude_fields"]) + and fields + ): filtered_fields = [] - if model_fields['include_fields']: - filtered_fields = [field for field in fields - if field.name in model_fields['include_fields']] + if model_fields["include_fields"]: + filtered_fields = [ + field + for field in fields + if field.name in model_fields["include_fields"] + ] else: filtered_fields = fields - if model_fields['exclude_fields']: - filtered_fields = [field for field in filtered_fields - if field.name not in model_fields['exclude_fields']] + if model_fields["exclude_fields"]: + filtered_fields = [ + field + for field in filtered_fields + if field.name not in model_fields["exclude_fields"] + ] fields = filtered_fields for field in fields: diff --git a/auditlog/filters.py b/auditlog/filters.py index c5b651a..21591ac 100644 --- a/auditlog/filters.py +++ b/auditlog/filters.py @@ -2,13 +2,13 @@ from django.contrib.admin import SimpleListFilter class ResourceTypeFilter(SimpleListFilter): - title = 'Resource Type' - parameter_name = 'resource_type' + title = "Resource Type" + parameter_name = "resource_type" def lookups(self, request, model_admin): qs = model_admin.get_queryset(request) - types = qs.values_list('content_type_id', 'content_type__model') - return list(types.order_by('content_type__model').distinct()) + types = qs.values_list("content_type_id", "content_type__model") + return list(types.order_by("content_type__model").distinct()) def queryset(self, request, queryset): if self.value() is None: diff --git a/auditlog/management/commands/auditlogflush.py b/auditlog/management/commands/auditlogflush.py index ecf4bd7..a2becd4 100644 --- a/auditlog/management/commands/auditlogflush.py +++ b/auditlog/management/commands/auditlogflush.py @@ -7,16 +7,25 @@ class Command(BaseCommand): help = "Deletes all log entries from the database." def add_arguments(self, parser): - parser.add_argument('-y, --yes', action='store_true', default=None, - help="Continue without asking confirmation.", dest='yes') + parser.add_argument( + "-y, --yes", + action="store_true", + default=None, + help="Continue without asking confirmation.", + dest="yes", + ) def handle(self, *args, **options): - answer = options['yes'] + answer = options["yes"] if answer is None: - self.stdout.write("This action will clear all log entries from the database.") - response = input("Are you sure you want to continue? [y/N]: ").lower().strip() - answer = response == 'y' + self.stdout.write( + "This action will clear all log entries from the database." + ) + response = ( + input("Are you sure you want to continue? [y/N]: ").lower().strip() + ) + answer = response == "y" if answer: count, _ = LogEntry.objects.all().delete() diff --git a/auditlog/middleware.py b/auditlog/middleware.py index 4ef07f6..ec1f563 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -25,25 +25,40 @@ class AuditlogMiddleware(MiddlewareMixin): """ # Initialize thread local storage threadlocal.auditlog = { - 'signal_duid': (self.__class__, time.time()), - 'remote_addr': request.META.get('REMOTE_ADDR'), + "signal_duid": (self.__class__, time.time()), + "remote_addr": request.META.get("REMOTE_ADDR"), } # In case of proxy, set 'original' address - if request.META.get('HTTP_X_FORWARDED_FOR'): - threadlocal.auditlog['remote_addr'] = request.META.get('HTTP_X_FORWARDED_FOR').split(',')[0] + if request.META.get("HTTP_X_FORWARDED_FOR"): + threadlocal.auditlog["remote_addr"] = request.META.get( + "HTTP_X_FORWARDED_FOR" + ).split(",")[0] # Connect signal for automatic logging - 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) + 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, + ) def process_response(self, request, response): """ Disconnects the signal receiver to prevent it from staying active. """ - if hasattr(threadlocal, 'auditlog'): - pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid']) + if hasattr(threadlocal, "auditlog"): + pre_save.disconnect( + sender=LogEntry, dispatch_uid=threadlocal.auditlog["signal_duid"] + ) return response @@ -51,8 +66,10 @@ class AuditlogMiddleware(MiddlewareMixin): """ Disconnects the signal receiver to prevent it from staying active in case of an exception. """ - if hasattr(threadlocal, 'auditlog'): - pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid']) + if hasattr(threadlocal, "auditlog"): + pre_save.disconnect( + sender=LogEntry, dispatch_uid=threadlocal.auditlog["signal_duid"] + ) return None @@ -62,15 +79,19 @@ class AuditlogMiddleware(MiddlewareMixin): Signal receiver with an extra, required 'user' kwarg. This method becomes a real (valid) signal receiver when it is curried with the actor. """ - if hasattr(threadlocal, 'auditlog'): - if signal_duid != threadlocal.auditlog['signal_duid']: + if hasattr(threadlocal, "auditlog"): + if signal_duid != threadlocal.auditlog["signal_duid"]: return try: - app_label, model_name = settings.AUTH_USER_MODEL.split('.') + app_label, model_name = settings.AUTH_USER_MODEL.split(".") auth_user_model = apps.get_model(app_label, model_name) except ValueError: - auth_user_model = apps.get_model('auth', 'user') - if sender == LogEntry and isinstance(user, auth_user_model) and instance.actor is None: + auth_user_model = apps.get_model("auth", "user") + if ( + sender == LogEntry + and isinstance(user, auth_user_model) + and instance.actor is None + ): instance.actor = user - instance.remote_addr = threadlocal.auditlog['remote_addr'] + instance.remote_addr = threadlocal.auditlog["remote_addr"] diff --git a/auditlog/migrations/0001_initial.py b/auditlog/migrations/0001_initial.py index 7abec05..0e2bd55 100644 --- a/auditlog/migrations/0001_initial.py +++ b/auditlog/migrations/0001_initial.py @@ -7,28 +7,71 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contenttypes', '0001_initial'), + ("contenttypes", "0001_initial"), ] operations = [ migrations.CreateModel( - name='LogEntry', + name="LogEntry", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('object_pk', models.TextField(verbose_name='object pk')), - ('object_id', models.PositiveIntegerField(db_index=True, null=True, verbose_name='object id', blank=True)), - ('object_repr', models.TextField(verbose_name='object representation')), - ('action', models.PositiveSmallIntegerField(verbose_name='action', choices=[(0, 'create'), (1, 'update'), (2, 'delete')])), - ('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(on_delete=django.db.models.deletion.CASCADE, related_name='+', verbose_name='content type', to='contenttypes.ContentType')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("object_pk", models.TextField(verbose_name="object pk")), + ( + "object_id", + models.PositiveIntegerField( + db_index=True, null=True, verbose_name="object id", blank=True + ), + ), + ("object_repr", models.TextField(verbose_name="object representation")), + ( + "action", + models.PositiveSmallIntegerField( + verbose_name="action", + choices=[(0, "create"), (1, "update"), (2, "delete")], + ), + ), + ( + "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( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + verbose_name="content type", + to="contenttypes.ContentType", + ), + ), ], options={ - 'ordering': ['-timestamp'], - 'get_latest_by': 'timestamp', - 'verbose_name': 'log entry', - 'verbose_name_plural': 'log entries', + "ordering": ["-timestamp"], + "get_latest_by": "timestamp", + "verbose_name": "log entry", + "verbose_name_plural": "log entries", }, bases=(models.Model,), ), diff --git a/auditlog/migrations/0002_auto_support_long_primary_keys.py b/auditlog/migrations/0002_auto_support_long_primary_keys.py index e34b3c9..01c7d6e 100644 --- a/auditlog/migrations/0002_auto_support_long_primary_keys.py +++ b/auditlog/migrations/0002_auto_support_long_primary_keys.py @@ -4,13 +4,15 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0001_initial'), + ("auditlog", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='logentry', - name='object_id', - field=models.BigIntegerField(db_index=True, null=True, verbose_name='object id', blank=True), + model_name="logentry", + name="object_id", + field=models.BigIntegerField( + db_index=True, null=True, verbose_name="object id", blank=True + ), ), ] diff --git a/auditlog/migrations/0003_logentry_remote_addr.py b/auditlog/migrations/0003_logentry_remote_addr.py index adf2c89..706dc4f 100644 --- a/auditlog/migrations/0003_logentry_remote_addr.py +++ b/auditlog/migrations/0003_logentry_remote_addr.py @@ -4,13 +4,15 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0002_auto_support_long_primary_keys'), + ("auditlog", "0002_auto_support_long_primary_keys"), ] operations = [ migrations.AddField( - model_name='logentry', - name='remote_addr', - field=models.GenericIPAddressField(null=True, verbose_name='remote address', blank=True), + model_name="logentry", + name="remote_addr", + field=models.GenericIPAddressField( + null=True, verbose_name="remote address", blank=True + ), ), ] diff --git a/auditlog/migrations/0004_logentry_detailed_object_repr.py b/auditlog/migrations/0004_logentry_detailed_object_repr.py index d6c8ee7..9e9deeb 100644 --- a/auditlog/migrations/0004_logentry_detailed_object_repr.py +++ b/auditlog/migrations/0004_logentry_detailed_object_repr.py @@ -5,13 +5,13 @@ import jsonfield.fields class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0003_logentry_remote_addr'), + ("auditlog", "0003_logentry_remote_addr"), ] operations = [ migrations.AddField( - model_name='logentry', - name='additional_data', + model_name="logentry", + name="additional_data", field=jsonfield.fields.JSONField(null=True, blank=True), ), ] diff --git a/auditlog/migrations/0005_logentry_additional_data_verbose_name.py b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py index 6554289..09e22d1 100644 --- a/auditlog/migrations/0005_logentry_additional_data_verbose_name.py +++ b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py @@ -5,13 +5,15 @@ import jsonfield.fields class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0004_logentry_detailed_object_repr'), + ("auditlog", "0004_logentry_detailed_object_repr"), ] operations = [ migrations.AlterField( - model_name='logentry', - name='additional_data', - field=jsonfield.fields.JSONField(null=True, verbose_name='additional data', blank=True), + model_name="logentry", + name="additional_data", + field=jsonfield.fields.JSONField( + null=True, verbose_name="additional data", blank=True + ), ), ] diff --git a/auditlog/migrations/0006_object_pk_index.py b/auditlog/migrations/0006_object_pk_index.py index ac431c0..729ebe2 100644 --- a/auditlog/migrations/0006_object_pk_index.py +++ b/auditlog/migrations/0006_object_pk_index.py @@ -4,13 +4,15 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0005_logentry_additional_data_verbose_name'), + ("auditlog", "0005_logentry_additional_data_verbose_name"), ] operations = [ migrations.AlterField( - model_name='logentry', - name='object_pk', - field=models.CharField(verbose_name='object pk', max_length=255, db_index=True), + model_name="logentry", + name="object_pk", + field=models.CharField( + verbose_name="object pk", max_length=255, db_index=True + ), ), ] diff --git a/auditlog/migrations/0007_object_pk_type.py b/auditlog/migrations/0007_object_pk_type.py index 275db7e..d6514e4 100644 --- a/auditlog/migrations/0007_object_pk_type.py +++ b/auditlog/migrations/0007_object_pk_type.py @@ -4,13 +4,15 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0006_object_pk_index'), + ("auditlog", "0006_object_pk_index"), ] operations = [ migrations.AlterField( - model_name='logentry', - name='object_pk', - field=models.CharField(verbose_name='object pk', max_length=255, db_index=True), + model_name="logentry", + name="object_pk", + field=models.CharField( + verbose_name="object pk", max_length=255, db_index=True + ), ), ] diff --git a/auditlog/mixins.py b/auditlog/mixins.py index 87225dd..a3f8f2e 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -12,62 +12,65 @@ MAX = 75 class LogEntryAdminMixin(object): - def created(self, obj): - return obj.timestamp.strftime('%Y-%m-%d %H:%M:%S') + return obj.timestamp.strftime("%Y-%m-%d %H:%M:%S") - created.short_description = 'Created' + created.short_description = "Created" def user_url(self, obj): if obj.actor: - app_label, model = settings.AUTH_USER_MODEL.split('.') - viewname = 'admin:%s_%s_change' % (app_label, model.lower()) + app_label, model = settings.AUTH_USER_MODEL.split(".") + viewname = "admin:%s_%s_change" % (app_label, model.lower()) try: link = urlresolvers.reverse(viewname, args=[obj.actor.pk]) except NoReverseMatch: - return u'%s' % (obj.actor) - return format_html(u'{}', link, obj.actor) + return "%s" % (obj.actor) + return format_html('{}', link, obj.actor) - return 'system' + return "system" - user_url.short_description = 'User' + user_url.short_description = "User" def resource_url(self, obj): app_label, model = obj.content_type.app_label, obj.content_type.model - viewname = 'admin:%s_%s_change' % (app_label, model) + viewname = "admin:%s_%s_change" % (app_label, model) try: args = [obj.object_pk] if obj.object_id is None else [obj.object_id] link = urlresolvers.reverse(viewname, args=args) except NoReverseMatch: return obj.object_repr else: - return format_html(u'{}', link, obj.object_repr) + return format_html('{}', link, obj.object_repr) - resource_url.short_description = 'Resource' + resource_url.short_description = "Resource" def msg_short(self, obj): if obj.action == LogEntry.Action.DELETE: - return '' # delete + return "" # delete changes = json.loads(obj.changes) - s = '' if len(changes) == 1 else 's' - fields = ', '.join(changes.keys()) + s = "" if len(changes) == 1 else "s" + fields = ", ".join(changes.keys()) if len(fields) > MAX: - i = fields.rfind(' ', 0, MAX) - fields = fields[:i] + ' ..' - return '%d change%s: %s' % (len(changes), s, fields) + i = fields.rfind(" ", 0, MAX) + fields = fields[:i] + " .." + return "%d change%s: %s" % (len(changes), s, fields) - msg_short.short_description = 'Changes' + msg_short.short_description = "Changes" def msg(self, obj): if obj.action == LogEntry.Action.DELETE: - return '' # delete + return "" # delete changes = json.loads(obj.changes) - msg = '' + msg = "
#FieldFromTo
" for i, field in enumerate(sorted(changes), 1): - value = [i, field] + (['***', '***'] if field == 'password' else changes[field]) - msg += format_html('', *value) + value = [i, field] + ( + ["***", "***"] if field == "password" else changes[field] + ) + msg += format_html( + "", *value + ) - msg += '
#FieldFromTo
{}{}{}{}
{}{}{}{}
' + msg += "" return mark_safe(msg) - msg.short_description = 'Changes' + msg.short_description = "Changes" diff --git a/auditlog/models.py b/auditlog/models.py index f04e562..c479acf 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -31,33 +31,49 @@ class LogEntryManager(models.Manager): :return: The new log entry or `None` if there were no changes. :rtype: LogEntry """ - changes = kwargs.get('changes', None) + changes = kwargs.get("changes", None) pk = self._get_pk_value(instance) 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_str(instance)) + kwargs.setdefault( + "content_type", ContentType.objects.get_for_model(instance) + ) + kwargs.setdefault("object_pk", pk) + kwargs.setdefault("object_repr", smart_str(instance)) if isinstance(pk, int): - kwargs.setdefault('object_id', pk) + kwargs.setdefault("object_id", pk) - get_additional_data = getattr(instance, 'get_additional_data', None) + get_additional_data = getattr(instance, "get_additional_data", None) if callable(get_additional_data): - kwargs.setdefault('additional_data', get_additional_data()) + kwargs.setdefault("additional_data", get_additional_data()) # 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(): - self.filter(content_type=kwargs.get('content_type'), object_id=kwargs.get('object_id')).delete() + 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() + ): + 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() + self.filter( + content_type=kwargs.get("content_type"), + object_pk=kwargs.get("object_pk", ""), + ).delete() # save LogEntry to same database instance is using db = instance._state.db - return self.create(**kwargs) if db is None or db == '' else self.using(db).create(**kwargs) + return ( + self.create(**kwargs) + if db is None or db == "" + else self.using(db).create(**kwargs) + ) return None def get_for_object(self, instance): @@ -94,15 +110,29 @@ class LogEntryManager(models.Manager): return self.none() content_type = ContentType.objects.get_for_model(queryset.model) - primary_keys = list(queryset.values_list(queryset.model._meta.pk.name, flat=True)) + primary_keys = list( + queryset.values_list(queryset.model._meta.pk.name, flat=True) + ) if isinstance(primary_keys[0], int): - return self.filter(content_type=content_type).filter(Q(object_id__in=primary_keys)).distinct() + 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_str(pk) for pk in primary_keys] - return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct() + 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() + return ( + self.filter(content_type=content_type) + .filter(Q(object_pk__in=primary_keys)) + .distinct() + ) def get_for_model(self, model): """ @@ -158,6 +188,7 @@ class LogEntry(models.Model): The valid actions are :py:attr:`Action.CREATE`, :py:attr:`Action.UPDATE` and :py:attr:`Action.DELETE`. """ + CREATE = 0 UPDATE = 1 DELETE = 2 @@ -168,24 +199,44 @@ class LogEntry(models.Model): (DELETE, _("delete")), ) - 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")) + 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")) + 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")) - remote_addr = models.GenericIPAddressField(blank=True, null=True, verbose_name=_("remote address")) + 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")) + additional_data = JSONField( + blank=True, null=True, verbose_name=_("additional data") + ) objects = LogEntryManager() class Meta: - get_latest_by = 'timestamp' - ordering = ['-timestamp'] + get_latest_by = "timestamp" + ordering = ["-timestamp"] verbose_name = _("log entry") verbose_name_plural = _("log entries") @@ -212,7 +263,7 @@ class LogEntry(models.Model): return {} @property - def changes_str(self, colon=': ', arrow=' \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,7 +277,7 @@ class LogEntry(models.Model): substrings = [] for field, values in self.changes_dict.items(): - substring = '{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}'.format( + substring = "{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}".format( field_name=field, colon=colon, old=values[0], @@ -244,6 +295,7 @@ class LogEntry(models.Model): """ # Get the model and model_fields from auditlog.registry import auditlog + model = self.content_type.model_class() model_fields = auditlog.get_model_fields(model._meta.model) changes_display_dict = {} @@ -258,9 +310,14 @@ class LogEntry(models.Model): values_display = [] # handle choices fields and Postgres ArrayField to get human readable version choices_dict = None - if getattr(field, 'choices') and len(field.choices) > 0: + if getattr(field, "choices") and len(field.choices) > 0: choices_dict = dict(field.choices) - if hasattr(field, 'base_field') and isinstance(field.base_field, Field) and getattr(field.base_field, 'choices') and len(field.base_field.choices) > 0: + if ( + hasattr(field, "base_field") + and isinstance(field.base_field, Field) + and getattr(field.base_field, "choices") + and len(field.base_field.choices) > 0 + ): choices_dict = dict(field.base_field.choices) if choices_dict: @@ -268,13 +325,17 @@ class LogEntry(models.Model): try: value = ast.literal_eval(value) if type(value) is [].__class__: - values_display.append(', '.join([choices_dict.get(val, 'None') for val in value])) + values_display.append( + ", ".join( + [choices_dict.get(val, "None") for val in value] + ) + ) else: - values_display.append(choices_dict.get(value, 'None')) + values_display.append(choices_dict.get(value, "None")) except ValueError: - values_display.append(choices_dict.get(value, 'None')) + values_display.append(choices_dict.get(value, "None")) except: - values_display.append(choices_dict.get(value, 'None')) + values_display.append(choices_dict.get(value, "None")) else: try: field_type = field.get_internal_type() @@ -301,7 +362,9 @@ class LogEntry(models.Model): value = "{}...".format(value[:140]) values_display.append(value) - verbose_name = model_fields['mapping_fields'].get(field.name, getattr(field, 'verbose_name', field.name)) + verbose_name = model_fields["mapping_fields"].get( + field.name, getattr(field, "verbose_name", field.name) + ) changes_display_dict[verbose_name] = values_display return changes_display_dict @@ -327,14 +390,14 @@ class AuditlogHistoryField(GenericRelation): """ def __init__(self, pk_indexable=True, delete_related=True, **kwargs): - kwargs['to'] = LogEntry + kwargs["to"] = LogEntry if pk_indexable: - kwargs['object_id_field'] = 'object_id' + kwargs["object_id_field"] = "object_id" else: - kwargs['object_id_field'] = 'object_pk' + kwargs["object_id_field"] = "object_pk" - kwargs['content_type_field'] = 'content_type' + kwargs["content_type_field"] = "content_type" self.delete_related = delete_related super(AuditlogHistoryField, self).__init__(**kwargs) @@ -356,6 +419,8 @@ 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.") + raise DeprecationWarning( + "South support will be dropped in django-auditlog 0.4.0 or later." + ) except ImportError: pass diff --git a/auditlog/registry.py b/auditlog/registry.py index 538f5dd..d4a520a 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -12,8 +12,13 @@ class AuditlogModelRegistry(object): A registry that keeps track of the models that use Auditlog to track changes. """ - def __init__(self, create: bool = True, update: bool = True, delete: bool = True, - custom: Optional[Dict[ModelSignal, Callable]] = 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 = {} @@ -29,8 +34,13 @@ class AuditlogModelRegistry(object): if custom is not None: self._signals.update(custom) - 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): + 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. @@ -54,9 +64,9 @@ class AuditlogModelRegistry(object): raise TypeError("Supplied model is not a valid model.") self._registry[cls] = { - 'include_fields': include_fields, - 'exclude_fields': exclude_fields, - 'mapping_fields': mapping_fields, + "include_fields": include_fields, + "exclude_fields": exclude_fields, + "mapping_fields": mapping_fields, } self._connect_signals(cls) @@ -101,9 +111,9 @@ class AuditlogModelRegistry(object): 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']), + "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): @@ -112,14 +122,18 @@ class AuditlogModelRegistry(object): """ for signal in self._signals: receiver = self._signals[signal] - signal.connect(receiver, sender=model, dispatch_uid=self._dispatch_uid(signal, model)) + signal.connect( + receiver, sender=model, dispatch_uid=self._dispatch_uid(signal, model) + ) def _disconnect_signals(self, model): """ Disconnect signals for the model. """ for signal, receiver in self._signals.items(): - signal.disconnect(sender=model, dispatch_uid=self._dispatch_uid(signal, model)) + signal.disconnect( + sender=model, dispatch_uid=self._dispatch_uid(signal, model) + ) def _dispatch_uid(self, signal, model) -> DispatchUID: """ diff --git a/auditlog_tests/__init__.py b/auditlog_tests/__init__.py index 1f12d80..fa61512 100644 --- a/auditlog_tests/__init__.py +++ b/auditlog_tests/__init__.py @@ -1 +1 @@ -default_app_config = 'auditlog_tests.apps.AuditlogTestConfig' +default_app_config = "auditlog_tests.apps.AuditlogTestConfig" diff --git a/auditlog_tests/apps.py b/auditlog_tests/apps.py index 85148cb..2bcc080 100644 --- a/auditlog_tests/apps.py +++ b/auditlog_tests/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class AuditlogTestConfig(AppConfig): - name = 'auditlog_tests' + name = "auditlog_tests" diff --git a/auditlog_tests/models.py b/auditlog_tests/models.py index 4eda784..a3b56b3 100644 --- a/auditlog_tests/models.py +++ b/auditlog_tests/models.py @@ -64,7 +64,7 @@ class RelatedModel(models.Model): A model with a foreign key. """ - related = models.ForeignKey(to='self', on_delete=models.CASCADE) + related = models.ForeignKey(to="self", on_delete=models.CASCADE) history = AuditlogHistoryField() @@ -74,12 +74,12 @@ class ManyRelatedModel(models.Model): A model with a many to many relation. """ - related = models.ManyToManyField('self') + related = models.ManyToManyField("self") history = AuditlogHistoryField() -@auditlog.register(include_fields=['label']) +@auditlog.register(include_fields=["label"]) class SimpleIncludeModel(models.Model): """ A simple model used for register's include_fields kwarg @@ -108,7 +108,7 @@ class SimpleMappingModel(models.Model): """ sku = models.CharField(max_length=100) - vtxt = models.CharField(verbose_name='Version', max_length=100) + vtxt = models.CharField(verbose_name="Version", max_length=100) not_mapped = models.CharField(max_length=100) history = AuditlogHistoryField() @@ -133,8 +133,8 @@ class AdditionalDataIncludedModel(models.Model): manager and added to each logentry instance on creation. """ object_details = { - 'related_model_id': self.related.id, - 'related_model_text': self.related.text + "related_model_id": self.related.id, + "related_model_text": self.related.text, } return object_details @@ -144,6 +144,7 @@ class DateTimeFieldModel(models.Model): A model with a DateTimeField, used to test DateTimeField changes are detected properly. """ + label = models.CharField(max_length=100) timestamp = models.DateTimeField() date = models.DateField() @@ -158,14 +159,15 @@ class ChoicesFieldModel(models.Model): A model with a CharField restricted to a set of choices. This model is used to test the changes_display_dict method. """ - RED = 'r' - YELLOW = 'y' - GREEN = 'g' + + RED = "r" + YELLOW = "y" + GREEN = "g" STATUS_CHOICES = ( - (RED, 'Red'), - (YELLOW, 'Yellow'), - (GREEN, 'Green'), + (RED, "Red"), + (YELLOW, "Yellow"), + (GREEN, "Green"), ) status = models.CharField(max_length=1, choices=STATUS_CHOICES) @@ -191,17 +193,20 @@ class PostgresArrayFieldModel(models.Model): """ Test auditlog with Postgres's ArrayField """ - RED = 'r' - YELLOW = 'y' - GREEN = 'g' + + RED = "r" + YELLOW = "y" + GREEN = "g" STATUS_CHOICES = ( - (RED, 'Red'), - (YELLOW, 'Yellow'), - (GREEN, 'Green'), + (RED, "Red"), + (YELLOW, "Yellow"), + (GREEN, "Green"), ) - arrayfield = ArrayField(models.CharField(max_length=1, choices=STATUS_CHOICES), size=3) + arrayfield = ArrayField( + models.CharField(max_length=1, choices=STATUS_CHOICES), size=3 + ) history = AuditlogHistoryField() @@ -218,8 +223,8 @@ auditlog.register(ProxyModel) auditlog.register(RelatedModel) auditlog.register(ManyRelatedModel) auditlog.register(ManyRelatedModel.related.through) -auditlog.register(SimpleExcludeModel, exclude_fields=['text']) -auditlog.register(SimpleMappingModel, mapping_fields={'sku': 'Product No.'}) +auditlog.register(SimpleExcludeModel, exclude_fields=["text"]) +auditlog.register(SimpleMappingModel, mapping_fields={"sku": "Product No."}) auditlog.register(AdditionalDataIncludedModel) auditlog.register(DateTimeFieldModel) auditlog.register(ChoicesFieldModel) diff --git a/auditlog_tests/test_settings.py b/auditlog_tests/test_settings.py index eb05b48..e2a036a 100644 --- a/auditlog_tests/test_settings.py +++ b/auditlog_tests/test_settings.py @@ -5,55 +5,55 @@ import os DEBUG = True -SECRET_KEY = 'test' +SECRET_KEY = "test" INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.messages', - 'django.contrib.sessions', - 'django.contrib.admin', - 'django.contrib.staticfiles', - 'auditlog', - 'auditlog_tests', + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.messages", + "django.contrib.sessions", + "django.contrib.admin", + "django.contrib.staticfiles", + "auditlog", + "auditlog_tests", ] MIDDLEWARE = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'auditlog.middleware.AuditlogMiddleware', + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "auditlog.middleware.AuditlogMiddleware", ) DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('TEST_DB_NAME', 'auditlog_tests_db'), - 'USER': os.getenv('TEST_DB_USER', 'postgres'), - 'PASSWORD': os.getenv('TEST_DB_PASS', ''), - 'HOST': os.getenv('TEST_DB_HOST', '127.0.0.1'), - 'PORT': os.getenv('TEST_DB_PORT', '5432'), + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.getenv("TEST_DB_NAME", "auditlog_tests_db"), + "USER": os.getenv("TEST_DB_USER", "postgres"), + "PASSWORD": os.getenv("TEST_DB_PASS", ""), + "HOST": os.getenv("TEST_DB_HOST", "127.0.0.1"), + "PORT": os.getenv("TEST_DB_PORT", "5432"), } } TEMPLATES = [ { - 'APP_DIRS': True, - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "APP_DIRS": True, + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ] }, }, ] -STATIC_URL = '/static/' +STATIC_URL = "/static/" -ROOT_URLCONF = 'auditlog_tests.urls' +ROOT_URLCONF = "auditlog_tests.urls" USE_TZ = True diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 4dd68d7..b4c5c02 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -14,15 +14,28 @@ from dateutil.tz import gettz from auditlog.middleware import AuditlogMiddleware from auditlog.models import LogEntry from auditlog.registry import auditlog -from auditlog_tests.models import SimpleModel, AltPrimaryKeyModel, UUIDPrimaryKeyModel, \ - ProxyModel, SimpleIncludeModel, SimpleExcludeModel, SimpleMappingModel, RelatedModel, \ - ManyRelatedModel, AdditionalDataIncludedModel, DateTimeFieldModel, ChoicesFieldModel, \ - CharfieldTextfieldModel, PostgresArrayFieldModel, NoDeleteHistoryModel +from auditlog_tests.models import ( + SimpleModel, + AltPrimaryKeyModel, + UUIDPrimaryKeyModel, + ProxyModel, + SimpleIncludeModel, + SimpleExcludeModel, + SimpleMappingModel, + RelatedModel, + ManyRelatedModel, + AdditionalDataIncludedModel, + DateTimeFieldModel, + ChoicesFieldModel, + CharfieldTextfieldModel, + PostgresArrayFieldModel, + NoDeleteHistoryModel, +) class SimpleModelTest(TestCase): def setUp(self): - self.obj = SimpleModel.objects.create(text='I am not difficult.') + self.obj = SimpleModel.objects.create(text="I am not difficult.") def test_create(self): """Creation is logged correctly.""" @@ -37,8 +50,12 @@ class SimpleModelTest(TestCase): except obj.history.DoesNotExist: self.assertTrue(False, "Log entry exists") else: - self.assertEqual(history.action, LogEntry.Action.CREATE, msg="Action is 'CREATE'") - self.assertEqual(history.object_repr, str(obj), msg="Representation is equal") + self.assertEqual( + history.action, LogEntry.Action.CREATE, msg="Action is 'CREATE'" + ) + self.assertEqual( + history.object_repr, str(obj), msg="Representation is equal" + ) def test_update(self): """Updates are logged correctly.""" @@ -50,11 +67,18 @@ class SimpleModelTest(TestCase): obj.save() # Check for log entries - self.assertTrue(obj.history.filter(action=LogEntry.Action.UPDATE).count() == 1, msg="There is one log entry for 'UPDATE'") + self.assertTrue( + obj.history.filter(action=LogEntry.Action.UPDATE).count() == 1, + msg="There is one log entry for 'UPDATE'", + ) history = obj.history.get(action=LogEntry.Action.UPDATE) - self.assertJSONEqual(history.changes, '{"boolean": ["False", "True"]}', msg="The change is correctly logged") + self.assertJSONEqual( + history.changes, + '{"boolean": ["False", "True"]}', + msg="The change is correctly logged", + ) def test_delete(self): """Deletion is logged correctly.""" @@ -67,7 +91,15 @@ class SimpleModelTest(TestCase): obj.delete() # Check for log entries - self.assertTrue(LogEntry.objects.filter(content_type=history.content_type, object_pk=history.object_pk, action=LogEntry.Action.DELETE).count() == 1, msg="There is one log entry for 'DELETE'") + self.assertTrue( + LogEntry.objects.filter( + content_type=history.content_type, + object_pk=history.object_pk, + action=LogEntry.Action.DELETE, + ).count() + == 1, + msg="There is one log entry for 'DELETE'", + ) def test_recreate(self): SimpleModel.objects.all().delete() @@ -77,12 +109,14 @@ class SimpleModelTest(TestCase): class AltPrimaryKeyModelTest(SimpleModelTest): def setUp(self): - self.obj = AltPrimaryKeyModel.objects.create(key=str(datetime.datetime.now()), text='I am strange.') + self.obj = AltPrimaryKeyModel.objects.create( + key=str(datetime.datetime.now()), text="I am strange." + ) class UUIDPrimaryKeyModelModelTest(SimpleModelTest): def setUp(self): - self.obj = UUIDPrimaryKeyModel.objects.create(text='I am strange.') + self.obj = UUIDPrimaryKeyModel.objects.create(text="I am strange.") def test_get_for_object(self): self.obj.boolean = True @@ -94,41 +128,54 @@ class UUIDPrimaryKeyModelModelTest(SimpleModelTest): self.obj.boolean = True self.obj.save() - self.assertEqual(LogEntry.objects.get_for_objects(UUIDPrimaryKeyModel.objects.all()).count(), 2) + self.assertEqual( + LogEntry.objects.get_for_objects(UUIDPrimaryKeyModel.objects.all()).count(), + 2, + ) class ProxyModelTest(SimpleModelTest): def setUp(self): - self.obj = ProxyModel.objects.create(text='I am not what you think.') + self.obj = ProxyModel.objects.create(text="I am not what you think.") class ManyRelatedModelTest(TestCase): """ Test the behaviour of a many-to-many relationship. """ + def setUp(self): self.obj = ManyRelatedModel.objects.create() self.rel_obj = ManyRelatedModel.objects.create() self.obj.related.add(self.rel_obj) def test_related(self): - self.assertEqual(LogEntry.objects.get_for_objects(self.obj.related.all()).count(), self.rel_obj.history.count()) - self.assertEqual(LogEntry.objects.get_for_objects(self.obj.related.all()).first(), self.rel_obj.history.first()) + self.assertEqual( + LogEntry.objects.get_for_objects(self.obj.related.all()).count(), + self.rel_obj.history.count(), + ) + self.assertEqual( + LogEntry.objects.get_for_objects(self.obj.related.all()).first(), + self.rel_obj.history.first(), + ) class MiddlewareTest(TestCase): """ Test the middleware responsible for connecting and disconnecting the signals used in automatic logging. """ + def setUp(self): self.middleware = AuditlogMiddleware() self.factory = RequestFactory() - self.user = User.objects.create_user(username='test', email='test@example.com', password='top_secret') + self.user = User.objects.create_user( + username="test", email="test@example.com", password="top_secret" + ) def test_request_anonymous(self): """No actor will be logged when a user is not logged in.""" # Create a request - request = self.factory.get('/') + request = self.factory.get("/") request.user = AnonymousUser() # Run middleware @@ -143,7 +190,7 @@ class MiddlewareTest(TestCase): def test_request(self): """The actor will be logged when a user is logged in.""" # Create a request - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.user # Run middleware self.middleware.process_request(request) @@ -157,12 +204,14 @@ class MiddlewareTest(TestCase): def test_response(self): """The signal will be disconnected when the request is processed.""" # Create a request - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.user # Run middleware self.middleware.process_request(request) - self.assertTrue(pre_save.has_listeners(LogEntry)) # The signal should be present before trying to disconnect it. + self.assertTrue( + pre_save.has_listeners(LogEntry) + ) # The signal should be present before trying to disconnect it. self.middleware.process_response(request, HttpResponse()) # Validate result @@ -171,12 +220,14 @@ class MiddlewareTest(TestCase): def test_exception(self): """The signal will be disconnected when an exception is raised.""" # Create a request - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.user # Run middleware self.middleware.process_request(request) - self.assertTrue(pre_save.has_listeners(LogEntry)) # The signal should be present before trying to disconnect it. + self.assertTrue( + pre_save.has_listeners(LogEntry) + ) # The signal should be present before trying to disconnect it. self.middleware.process_exception(request, ValidationError("Test")) # Validate result @@ -187,17 +238,17 @@ class SimpeIncludeModelTest(TestCase): """Log only changes in include_fields""" def test_register_include_fields(self): - sim = SimpleIncludeModel(label='Include model', text='Looong text') + sim = SimpleIncludeModel(label="Include model", text="Looong text") sim.save() self.assertTrue(sim.history.count() == 1, msg="There is one log entry") # Change label, record - sim.label = 'Changed label' + sim.label = "Changed label" sim.save() self.assertTrue(sim.history.count() == 2, msg="There are two log entries") # Change text, ignore - sim.text = 'Short text' + sim.text = "Short text" sim.save() self.assertTrue(sim.history.count() == 2, msg="There are two log entries") @@ -206,17 +257,17 @@ class SimpeExcludeModelTest(TestCase): """Log only changes that are not in exclude_fields""" def test_register_exclude_fields(self): - sem = SimpleExcludeModel(label='Exclude model', text='Looong text') + sem = SimpleExcludeModel(label="Exclude model", text="Looong text") sem.save() self.assertTrue(sem.history.count() == 1, msg="There is one log entry") # Change label, ignore - sem.label = 'Changed label' + sem.label = "Changed label" sem.save() self.assertTrue(sem.history.count() == 2, msg="There are two log entries") # Change text, record - sem.text = 'Short text' + sem.text = "Short text" sem.save() self.assertTrue(sem.history.count() == 2, msg="There are two log entries") @@ -225,38 +276,58 @@ class SimpleMappingModelTest(TestCase): """Diff displays fields as mapped field names where available through mapping_fields""" def test_register_mapping_fields(self): - smm = SimpleMappingModel(sku='ASD301301A6', vtxt='2.1.5', not_mapped='Not mapped') + smm = SimpleMappingModel( + sku="ASD301301A6", vtxt="2.1.5", not_mapped="Not mapped" + ) smm.save() - self.assertTrue(smm.history.latest().changes_dict['sku'][1] == 'ASD301301A6', - msg="The diff function retains 'sku' and can be retrieved.") - self.assertTrue(smm.history.latest().changes_dict['not_mapped'][1] == 'Not mapped', - msg="The diff function does not map 'not_mapped' and can be retrieved.") - self.assertTrue(smm.history.latest().changes_display_dict['Product No.'][1] == 'ASD301301A6', - msg="The diff function maps 'sku' as 'Product No.' and can be retrieved.") - self.assertTrue(smm.history.latest().changes_display_dict['Version'][1] == '2.1.5', - msg=("The diff function maps 'vtxt' as 'Version' through verbose_name" - " setting on the model field and can be retrieved.")) - self.assertTrue(smm.history.latest().changes_display_dict['not mapped'][1] == 'Not mapped', - msg=("The diff function uses the django default verbose name for 'not_mapped'" - " and can be retrieved.")) + self.assertTrue( + smm.history.latest().changes_dict["sku"][1] == "ASD301301A6", + msg="The diff function retains 'sku' and can be retrieved.", + ) + self.assertTrue( + smm.history.latest().changes_dict["not_mapped"][1] == "Not mapped", + msg="The diff function does not map 'not_mapped' and can be retrieved.", + ) + self.assertTrue( + smm.history.latest().changes_display_dict["Product No."][1] + == "ASD301301A6", + msg="The diff function maps 'sku' as 'Product No.' and can be retrieved.", + ) + self.assertTrue( + smm.history.latest().changes_display_dict["Version"][1] == "2.1.5", + msg=( + "The diff function maps 'vtxt' as 'Version' through verbose_name" + " setting on the model field and can be retrieved." + ), + ) + self.assertTrue( + smm.history.latest().changes_display_dict["not mapped"][1] == "Not mapped", + msg=( + "The diff function uses the django default verbose name for 'not_mapped'" + " and can be retrieved." + ), + ) class AdditionalDataModelTest(TestCase): """Log additional data if get_additional_data is defined in the model""" def test_model_without_additional_data(self): - obj_wo_additional_data = SimpleModel.objects.create(text='No additional ' - 'data') + obj_wo_additional_data = SimpleModel.objects.create( + text="No additional " "data" + ) obj_log_entry = obj_wo_additional_data.history.get() self.assertIsNone(obj_log_entry.additional_data) def test_model_with_additional_data(self): - related_model = SimpleModel.objects.create(text='Log my reference') + related_model = SimpleModel.objects.create(text="Log my reference") obj_with_additional_data = AdditionalDataIncludedModel( - label='Additional data to log entries', related=related_model) + label="Additional data to log entries", related=related_model + ) obj_with_additional_data.save() - self.assertTrue(obj_with_additional_data.history.count() == 1, - msg="There is 1 log entry") + self.assertTrue( + obj_with_additional_data.history.count() == 1, msg="There is 1 log entry" + ) log_entry = obj_with_additional_data.history.get() # FIXME: Work-around for the fact that additional_data isn't working # on Django 3.1 correctly (see https://github.com/jazzband/django-auditlog/issues/266) @@ -265,10 +336,14 @@ class AdditionalDataModelTest(TestCase): else: extra_data = log_entry.additional_data self.assertIsNotNone(extra_data) - self.assertTrue(extra_data['related_model_text'] == related_model.text, - msg="Related model's text is logged") - self.assertTrue(extra_data['related_model_id'] == related_model.id, - msg="Related model's id is logged") + self.assertTrue( + extra_data["related_model_text"] == related_model.text, + msg="Related model's text is logged", + ) + self.assertTrue( + extra_data["related_model_id"] == related_model.id, + msg="Related model's id is logged", + ) class DateTimeFieldModelTest(TestCase): @@ -281,7 +356,13 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") @@ -299,7 +380,13 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") @@ -315,7 +402,13 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") @@ -331,7 +424,13 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") @@ -347,7 +446,13 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") @@ -363,7 +468,13 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") @@ -379,85 +490,144 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 15, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() localized_timestamp = timestamp.astimezone(gettz(settings.TIME_ZONE)) - self.assertTrue(dtm.history.latest().changes_display_dict["timestamp"][1] == \ - dateformat.format(localized_timestamp, settings.DATETIME_FORMAT), - msg=("The datetime should be formatted according to Django's settings for" - " DATETIME_FORMAT")) + self.assertTrue( + dtm.history.latest().changes_display_dict["timestamp"][1] + == dateformat.format(localized_timestamp, settings.DATETIME_FORMAT), + msg=( + "The datetime should be formatted according to Django's settings for" + " DATETIME_FORMAT" + ), + ) timestamp = timezone.now() dtm.timestamp = timestamp dtm.save() localized_timestamp = timestamp.astimezone(gettz(settings.TIME_ZONE)) - self.assertTrue(dtm.history.latest().changes_display_dict["timestamp"][1] == \ - dateformat.format(localized_timestamp, settings.DATETIME_FORMAT), - msg=("The datetime should be formatted according to Django's settings for" - " DATETIME_FORMAT")) + self.assertTrue( + dtm.history.latest().changes_display_dict["timestamp"][1] + == dateformat.format(localized_timestamp, settings.DATETIME_FORMAT), + msg=( + "The datetime should be formatted according to Django's settings for" + " DATETIME_FORMAT" + ), + ) # Change USE_L10N = True - with self.settings(USE_L10N=True, LANGUAGE_CODE='en-GB'): - self.assertTrue(dtm.history.latest().changes_display_dict["timestamp"][1] == \ - formats.localize(localized_timestamp), - msg=("The datetime should be formatted according to Django's settings for" - " USE_L10N is True with a different LANGUAGE_CODE.")) - + with self.settings(USE_L10N=True, LANGUAGE_CODE="en-GB"): + self.assertTrue( + dtm.history.latest().changes_display_dict["timestamp"][1] + == formats.localize(localized_timestamp), + msg=( + "The datetime should be formatted according to Django's settings for" + " USE_L10N is True with a different LANGUAGE_CODE." + ), + ) def test_changes_display_dict_date(self): timestamp = datetime.datetime(2017, 1, 10, 15, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() - self.assertTrue(dtm.history.latest().changes_display_dict["date"][1] == \ - dateformat.format(date, settings.DATE_FORMAT), - msg=("The date should be formatted according to Django's settings for" - " DATE_FORMAT unless USE_L10N is True.")) + self.assertTrue( + dtm.history.latest().changes_display_dict["date"][1] + == dateformat.format(date, settings.DATE_FORMAT), + msg=( + "The date should be formatted according to Django's settings for" + " DATE_FORMAT unless USE_L10N is True." + ), + ) date = datetime.date(2017, 1, 11) dtm.date = date dtm.save() - self.assertTrue(dtm.history.latest().changes_display_dict["date"][1] == \ - dateformat.format(date, settings.DATE_FORMAT), - msg=("The date should be formatted according to Django's settings for" - " DATE_FORMAT unless USE_L10N is True.")) + self.assertTrue( + dtm.history.latest().changes_display_dict["date"][1] + == dateformat.format(date, settings.DATE_FORMAT), + msg=( + "The date should be formatted according to Django's settings for" + " DATE_FORMAT unless USE_L10N is True." + ), + ) # Change USE_L10N = True - with self.settings(USE_L10N=True, LANGUAGE_CODE='en-GB'): - self.assertTrue(dtm.history.latest().changes_display_dict["date"][1] == \ - formats.localize(date), - msg=("The date should be formatted according to Django's settings for" - " USE_L10N is True with a different LANGUAGE_CODE.")) + with self.settings(USE_L10N=True, LANGUAGE_CODE="en-GB"): + self.assertTrue( + dtm.history.latest().changes_display_dict["date"][1] + == formats.localize(date), + msg=( + "The date should be formatted according to Django's settings for" + " USE_L10N is True with a different LANGUAGE_CODE." + ), + ) def test_changes_display_dict_time(self): timestamp = datetime.datetime(2017, 1, 10, 15, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() - self.assertTrue(dtm.history.latest().changes_display_dict["time"][1] == \ - dateformat.format(time, settings.TIME_FORMAT), - msg=("The time should be formatted according to Django's settings for" - " TIME_FORMAT unless USE_L10N is True.")) + self.assertTrue( + dtm.history.latest().changes_display_dict["time"][1] + == dateformat.format(time, settings.TIME_FORMAT), + msg=( + "The time should be formatted according to Django's settings for" + " TIME_FORMAT unless USE_L10N is True." + ), + ) time = datetime.time(6, 0) dtm.time = time dtm.save() - self.assertTrue(dtm.history.latest().changes_display_dict["time"][1] == \ - dateformat.format(time, settings.TIME_FORMAT), - msg=("The time should be formatted according to Django's settings for" - " TIME_FORMAT unless USE_L10N is True.")) + self.assertTrue( + dtm.history.latest().changes_display_dict["time"][1] + == dateformat.format(time, settings.TIME_FORMAT), + msg=( + "The time should be formatted according to Django's settings for" + " TIME_FORMAT unless USE_L10N is True." + ), + ) # Change USE_L10N = True - with self.settings(USE_L10N=True, LANGUAGE_CODE='en-GB'): - self.assertTrue(dtm.history.latest().changes_display_dict["time"][1] == \ - formats.localize(time), - msg=("The time should be formatted according to Django's settings for" - " USE_L10N is True with a different LANGUAGE_CODE.")) + with self.settings(USE_L10N=True, LANGUAGE_CODE="en-GB"): + self.assertTrue( + dtm.history.latest().changes_display_dict["time"][1] + == formats.localize(time), + msg=( + "The time should be formatted according to Django's settings for" + " USE_L10N is True with a different LANGUAGE_CODE." + ), + ) def test_update_naive_dt(self): timestamp = datetime.datetime(2017, 1, 10, 15, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() # Change with naive field doesnt raise error @@ -468,7 +638,7 @@ class DateTimeFieldModelTest(TestCase): class UnregisterTest(TestCase): def setUp(self): auditlog.unregister(SimpleModel) - self.obj = SimpleModel.objects.create(text='No history') + self.obj = SimpleModel.objects.create(text="No history") def tearDown(self): # Re-register for future tests @@ -507,32 +677,45 @@ class UnregisterTest(TestCase): class ChoicesFieldModelTest(TestCase): - def setUp(self): self.obj = ChoicesFieldModel.objects.create( status=ChoicesFieldModel.RED, - multiplechoice=[ChoicesFieldModel.RED, ChoicesFieldModel.YELLOW, ChoicesFieldModel.GREEN], + multiplechoice=[ + ChoicesFieldModel.RED, + ChoicesFieldModel.YELLOW, + ChoicesFieldModel.GREEN, + ], ) def test_changes_display_dict_single_choice(self): - self.assertTrue(self.obj.history.latest().changes_display_dict["status"][1] == "Red", - msg="The human readable text 'Red' is displayed.") + self.assertTrue( + self.obj.history.latest().changes_display_dict["status"][1] == "Red", + msg="The human readable text 'Red' is displayed.", + ) self.obj.status = ChoicesFieldModel.GREEN self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["status"][1] == "Green", msg="The human readable text 'Green' is displayed.") + 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_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.") + self.assertTrue( + self.obj.history.latest().changes_display_dict["multiplechoice"][1] + == "Red, Yellow, Green", + msg="The human readable text 'Red, Yellow, Green' is displayed.", + ) self.obj.multiplechoice = ChoicesFieldModel.RED self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["multiplechoice"][1] == "Red", - msg="The human readable text 'Red' is displayed.") + self.assertTrue( + self.obj.history.latest().changes_display_dict["multiplechoice"][1] + == "Red", + msg="The human readable text 'Red' is displayed.", + ) class CharfieldTextfieldModelTest(TestCase): - def setUp(self): self.PLACEHOLDER_LONGCHAR = "s" * 255 self.PLACEHOLDER_LONGTEXTFIELD = "s" * 1000 @@ -542,28 +725,38 @@ class CharfieldTextfieldModelTest(TestCase): ) def test_changes_display_dict_longchar(self): - self.assertTrue(self.obj.history.latest().changes_display_dict["longchar"][1] == \ - "{}...".format(self.PLACEHOLDER_LONGCHAR[:140]), - msg="The string should be truncated at 140 characters with an ellipsis at the end.") + self.assertTrue( + self.obj.history.latest().changes_display_dict["longchar"][1] + == "{}...".format(self.PLACEHOLDER_LONGCHAR[:140]), + msg="The string should be truncated at 140 characters with an ellipsis at the end.", + ) SHORTENED_PLACEHOLDER = self.PLACEHOLDER_LONGCHAR[:139] self.obj.longchar = SHORTENED_PLACEHOLDER self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["longchar"][1] == SHORTENED_PLACEHOLDER, - msg="The field should display the entire string because it is less than 140 characters") + self.assertTrue( + self.obj.history.latest().changes_display_dict["longchar"][1] + == SHORTENED_PLACEHOLDER, + msg="The field should display the entire string because it is less than 140 characters", + ) def test_changes_display_dict_longtextfield(self): - self.assertTrue(self.obj.history.latest().changes_display_dict["longtextfield"][1] == \ - "{}...".format(self.PLACEHOLDER_LONGTEXTFIELD[:140]), - msg="The string should be truncated at 140 characters with an ellipsis at the end.") + self.assertTrue( + self.obj.history.latest().changes_display_dict["longtextfield"][1] + == "{}...".format(self.PLACEHOLDER_LONGTEXTFIELD[:140]), + msg="The string should be truncated at 140 characters with an ellipsis at the end.", + ) SHORTENED_PLACEHOLDER = self.PLACEHOLDER_LONGTEXTFIELD[:139] self.obj.longtextfield = SHORTENED_PLACEHOLDER self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["longtextfield"][1] == SHORTENED_PLACEHOLDER, - msg="The field should display the entire string because it is less than 140 characters") + self.assertTrue( + self.obj.history.latest().changes_display_dict["longtextfield"][1] + == SHORTENED_PLACEHOLDER, + msg="The field should display the entire string because it is less than 140 characters", + ) class PostgresArrayFieldModelTest(TestCase): - databases = '__all__' + databases = "__all__" def setUp(self): self.obj = PostgresArrayFieldModel.objects.create( @@ -575,20 +768,28 @@ class PostgresArrayFieldModelTest(TestCase): return self.obj.history.latest().changes_display_dict["arrayfield"][1] def test_changes_display_dict_arrayfield(self): - self.assertTrue(self.latest_array_change == "Red, Green", - msg="The human readable text for the two choices, 'Red, Green' is displayed.") + self.assertTrue( + self.latest_array_change == "Red, Green", + msg="The human readable text for the two choices, 'Red, Green' is displayed.", + ) self.obj.arrayfield = [PostgresArrayFieldModel.GREEN] self.obj.save() - self.assertTrue(self.latest_array_change == "Green", - msg="The human readable text 'Green' is displayed.") + self.assertTrue( + self.latest_array_change == "Green", + msg="The human readable text 'Green' is displayed.", + ) self.obj.arrayfield = [] self.obj.save() - self.assertTrue(self.latest_array_change == "", - msg="The human readable text '' is displayed.") + self.assertTrue( + self.latest_array_change == "", + msg="The human readable text '' is displayed.", + ) self.obj.arrayfield = [PostgresArrayFieldModel.GREEN] self.obj.save() - self.assertTrue(self.latest_array_change == "Green", - msg="The human readable text 'Green' is displayed.") + self.assertTrue( + self.latest_array_change == "Green", + msg="The human readable text 'Green' is displayed.", + ) class AdminPanelTest(TestCase): @@ -602,7 +803,7 @@ class AdminPanelTest(TestCase): cls.user.is_superuser = True cls.user.is_active = True cls.user.save() - cls.obj = SimpleModel.objects.create(text='For admin logentry test') + cls.obj = SimpleModel.objects.create(text="For admin logentry test") def test_auditlog_admin(self): self.client.login(username=self.username, password=self.password) @@ -611,7 +812,9 @@ class AdminPanelTest(TestCase): assert res.status_code == 200 res = self.client.get("/admin/auditlog/logentry/add/") assert res.status_code == 200 - res = self.client.get("/admin/auditlog/logentry/{}/".format(log_pk), follow=True) + res = self.client.get( + "/admin/auditlog/logentry/{}/".format(log_pk), follow=True + ) assert res.status_code == 200 res = self.client.get("/admin/auditlog/logentry/{}/delete/".format(log_pk)) assert res.status_code == 200 @@ -628,7 +831,7 @@ class NoDeleteHistoryTest(TestCase): assert LogEntry.objects.all().count() == 2 instance.delete() - entries = LogEntry.objects.order_by('id') + entries = LogEntry.objects.order_by("id") # The "DELETE" record is always retained assert LogEntry.objects.all().count() == 1 @@ -642,9 +845,9 @@ class NoDeleteHistoryTest(TestCase): self.assertEqual(LogEntry.objects.all().count(), 2) instance.delete() - entries = LogEntry.objects.order_by('id') + entries = LogEntry.objects.order_by("id") self.assertEqual(entries.count(), 3) self.assertEqual( - list(entries.values_list('action', flat=True)), - [LogEntry.Action.CREATE, LogEntry.Action.UPDATE, LogEntry.Action.DELETE] + list(entries.values_list("action", flat=True)), + [LogEntry.Action.CREATE, LogEntry.Action.UPDATE, LogEntry.Action.DELETE], ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f040b26 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +target-version = ["py35"] diff --git a/runtests.py b/runtests.py index b7df405..dea36f6 100644 --- a/runtests.py +++ b/runtests.py @@ -7,7 +7,7 @@ from django.conf import settings from django.test.utils import get_runner if __name__ == "__main__": - os.environ['DJANGO_SETTINGS_MODULE'] = 'auditlog_tests.test_settings' + os.environ["DJANGO_SETTINGS_MODULE"] = "auditlog_tests.test_settings" django.setup() TestRunner = get_runner(settings) test_runner = TestRunner() diff --git a/setup.py b/setup.py index 7fc063b..ef3be65 100644 --- a/setup.py +++ b/setup.py @@ -7,29 +7,31 @@ with open(os.path.join(os.path.dirname(__file__), "README.md"), "r") as readme_f long_description = readme_file.read() setup( - name='django-auditlog', + name="django-auditlog", use_scm_version={"version_scheme": "post-release"}, setup_requires=["setuptools_scm"], - packages=['auditlog', 'auditlog.migrations', 'auditlog.management', 'auditlog.management.commands'], - url='https://github.com/jazzband/django-auditlog', - license='MIT', - author='Jan-Jelle Kester', - description='Audit log app for Django', - long_description=long_description, - long_description_content_type='text/markdown', - install_requires=[ - 'django-jsonfield>=1.0.0', - 'python-dateutil>=2.6.0' + packages=[ + "auditlog", + "auditlog.migrations", + "auditlog.management", + "auditlog.management.commands", ], + url="https://github.com/jazzband/django-auditlog", + license="MIT", + author="Jan-Jelle Kester", + description="Audit log app for Django", + long_description=long_description, + long_description_content_type="text/markdown", + install_requires=["django-jsonfield>=1.0.0", "python-dateutil>=2.6.0"], zip_safe=False, classifiers=[ - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'License :: OSI Approved :: MIT License', + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "License :: OSI Approved :: MIT License", ], ) diff --git a/tox.ini b/tox.ini index 2a5eb8c..0ddd609 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = {py35,py36,py37,py38,py39}-django-22 {py36,py37,py38,py39}-django-{30,31} py38-docs + py38-qa [testenv] commands = @@ -35,6 +36,13 @@ changedir = docs/source deps = -rdocs/requirements.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html +[testenv:py38-qa] +basepython = python3.8 +deps = + black +commands = + black --check --diff auditlog auditlog_tests setup.py runtests.py + [gh-actions] python = 3.5: py35