From 6a7908f2ca0671a4ebc9e9e154beb7d6e55bf17d Mon Sep 17 00:00:00 2001 From: Ann Paul Date: Thu, 16 Apr 2015 11:33:59 -0700 Subject: [PATCH 01/26] Add detailed_object_repr JSONField to logentry model --- src/auditlog/models.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/auditlog/models.py b/src/auditlog/models.py index 1223a6b..f767ae5 100644 --- a/src/auditlog/models.py +++ b/src/auditlog/models.py @@ -6,6 +6,8 @@ from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.translation import ugettext_lazy as _ +from jsonfield import JSONField + class LogEntryManager(models.Manager): """ @@ -28,6 +30,10 @@ class LogEntryManager(models.Manager): if isinstance(pk, (int, long)): kwargs.setdefault('object_id', pk) + get_detailed_object_repr = getattr(instance, 'get_detailed_object_repr', None) + if callable(get_detailed_object_repr): + kwargs.setdefault('detailed_object_repr', instance.get_detailed_object_repr()) + # 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: @@ -110,6 +116,7 @@ class LogEntry(models.Model): changes = models.TextField(blank=True, verbose_name=_("change message")) actor = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_("actor")) timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp")) + detailed_object_repr = JSONField(blank=True, null=True) objects = LogEntryManager() From d4522ad74add7848cfddb25bfcab656a7d47730e Mon Sep 17 00:00:00 2001 From: Ann Paul Date: Thu, 16 Apr 2015 11:34:21 -0700 Subject: [PATCH 02/26] Add migration for detailed_object_repr field --- .../0003_logentry_detailed_object_repr.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/auditlog/migrations/0003_logentry_detailed_object_repr.py diff --git a/src/auditlog/migrations/0003_logentry_detailed_object_repr.py b/src/auditlog/migrations/0003_logentry_detailed_object_repr.py new file mode 100644 index 0000000..bd71ddb --- /dev/null +++ b/src/auditlog/migrations/0003_logentry_detailed_object_repr.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('auditlog', '0002_auto_support_long_primary_keys'), + ] + + operations = [ + migrations.AddField( + model_name='logentry', + name='detailed_object_repr', + field=jsonfield.fields.JSONField(null=True, blank=True), + ), + ] From 4409e1c1a6f850b77e58e90bd6d89aeecff7a53a Mon Sep 17 00:00:00 2001 From: Ann Paul Date: Thu, 16 Apr 2015 11:34:48 -0700 Subject: [PATCH 03/26] Add jsonfield to pip requirements --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index be600a3..0828b7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,4 @@ Django>=1.5 + +#allows use of verified JSON model fields +django-jsonfield==0.9.13 From 8b47ce1b19367165181856365a0d0ed92bb5a64b Mon Sep 17 00:00:00 2001 From: Ann Paul Date: Mon, 20 Apr 2015 15:52:59 -0700 Subject: [PATCH 04/26] Add django-jsonfield dependency to setup file and update pip --- requirements.txt | 2 +- setup.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0828b7f..9c0b077 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ Django>=1.5 #allows use of verified JSON model fields -django-jsonfield==0.9.13 +django-jsonfield>=0.9.13 diff --git a/setup.py b/setup.py index 969439f..443d5f3 100644 --- a/setup.py +++ b/setup.py @@ -11,6 +11,7 @@ setup( author_email='janjelle@jjkester.nl', description='Audit log app for Django', install_requires=[ - 'Django>=1.5' + 'Django>=1.5', + 'django-jsonfield>=0.9.13' ] ) From 1d5118460fa7fa58bca7f8026e81f7637938d4ba Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Fri, 15 May 2015 11:41:15 +0200 Subject: [PATCH 05/26] Refactor tests to not use a Django project --- .travis.yml | 2 +- README.md | 2 + .../__init__.py | 0 src/{testapp => auditlog_tests}/models.py | 0 src/auditlog_tests/runtests.py | 15 ++++ src/auditlog_tests/test_settings.py | 24 +++++++ src/{testapp => auditlog_tests}/tests.py | 2 +- src/manage.py | 10 --- src/test_project/settings.py | 68 ------------------- src/test_project/urls.py | 17 ----- src/test_project/wsgi.py | 32 --------- src/testapp/__init__.py | 0 12 files changed, 43 insertions(+), 129 deletions(-) rename src/{test_project => auditlog_tests}/__init__.py (100%) rename src/{testapp => auditlog_tests}/models.py (100%) create mode 100644 src/auditlog_tests/runtests.py create mode 100644 src/auditlog_tests/test_settings.py rename src/{testapp => auditlog_tests}/tests.py (98%) delete mode 100644 src/manage.py delete mode 100644 src/test_project/settings.py delete mode 100644 src/test_project/urls.py delete mode 100644 src/test_project/wsgi.py delete mode 100644 src/testapp/__init__.py diff --git a/.travis.yml b/.travis.yml index 71eb5d9..0c424b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,4 +8,4 @@ env: install: - "pip install -r requirements.txt" - "pip install Django==$DJANGO_VERSION" -script: "python src/manage.py test testapp" +script: "python src/auditlog_tests/runtests.py" diff --git a/README.md b/README.md index e9b0b3e..45d0089 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ django-auditlog =============== +[![Build Status](https://travis-ci.org/jjkester/django-auditlog.svg?branch=master)](https://travis-ci.org/jjkester/django-auditlog) + **Please remember that this app is still in development and not yet suitable for production environments.** ```django-auditlog``` (Auditlog) is a reusable app for Django that makes logging object changes a breeze. Auditlog tries to use as much as Python and Django’s built in functionality to keep the list of dependencies as short as possible. Also, Auditlog aims to be fast and simple to use. diff --git a/src/test_project/__init__.py b/src/auditlog_tests/__init__.py similarity index 100% rename from src/test_project/__init__.py rename to src/auditlog_tests/__init__.py diff --git a/src/testapp/models.py b/src/auditlog_tests/models.py similarity index 100% rename from src/testapp/models.py rename to src/auditlog_tests/models.py diff --git a/src/auditlog_tests/runtests.py b/src/auditlog_tests/runtests.py new file mode 100644 index 0000000..b7df405 --- /dev/null +++ b/src/auditlog_tests/runtests.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import os +import sys + +import django +from django.conf import settings +from django.test.utils import get_runner + +if __name__ == "__main__": + os.environ['DJANGO_SETTINGS_MODULE'] = 'auditlog_tests.test_settings' + django.setup() + TestRunner = get_runner(settings) + test_runner = TestRunner() + failures = test_runner.run_tests(["auditlog_tests"]) + sys.exit(bool(failures)) diff --git a/src/auditlog_tests/test_settings.py b/src/auditlog_tests/test_settings.py new file mode 100644 index 0000000..84ca8f6 --- /dev/null +++ b/src/auditlog_tests/test_settings.py @@ -0,0 +1,24 @@ +""" +Settings file for the Auditlog test suite. +""" + +SECRET_KEY = 'test' + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'auditlog', + 'auditlog_tests', +] + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'auditlog.middleware.AuditlogMiddleware', +) + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'auditlog_test', + } +} diff --git a/src/testapp/tests.py b/src/auditlog_tests/tests.py similarity index 98% rename from src/testapp/tests.py rename to src/auditlog_tests/tests.py index 80e36dd..cd93bd3 100644 --- a/src/testapp/tests.py +++ b/src/auditlog_tests/tests.py @@ -6,7 +6,7 @@ from django.http import HttpResponse from django.test import TestCase, RequestFactory from auditlog.middleware import AuditlogMiddleware from auditlog.models import LogEntry -from testapp.models import SimpleModel, AltPrimaryKeyModel, ProxyModel, \ +from auditlog_tests.models import SimpleModel, AltPrimaryKeyModel, ProxyModel, \ SimpleIncludeModel, SimpleExcludeModel diff --git a/src/manage.py b/src/manage.py deleted file mode 100644 index 0fc36a3..0000000 --- a/src/manage.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/src/test_project/settings.py b/src/test_project/settings.py deleted file mode 100644 index 9666e46..0000000 --- a/src/test_project/settings.py +++ /dev/null @@ -1,68 +0,0 @@ -# Django settings for test_project project. -import os - -BASEDIR = os.path.dirname(os.path.realpath(__file__)) - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASEDIR, 'test.db'), - } -} - -# Make this unique, and don't share it with anybody. -SECRET_KEY = 'j6qdljfn(m$w-4r5*wx_m!!o4-z0ehe09y%k8s@kf)zmyc366*' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.app_directories.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'auditlog.middleware.AuditlogMiddleware', -) - -ROOT_URLCONF = 'test_project.urls' - -# Python dotted path to the WSGI application used by Django's runserver. -WSGI_APPLICATION = 'test_project.wsgi.application' - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'auditlog', - 'testapp', -) - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error when DEBUG=False. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } - }, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} diff --git a/src/test_project/urls.py b/src/test_project/urls.py deleted file mode 100644 index cc4356b..0000000 --- a/src/test_project/urls.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.conf.urls import patterns, include, url - -# Uncomment the next two lines to enable the admin: -# from django.contrib import admin -# admin.autodiscover() - -urlpatterns = patterns('', - # Examples: - # url(r'^$', 'test_project.views.home', name='home'), - # url(r'^test_project/', include('test_project.foo.urls')), - - # Uncomment the admin/doc line below to enable admin documentation: - # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - - # Uncomment the next line to enable the admin: - # url(r'^admin/', include(admin.site.urls)), -) diff --git a/src/test_project/wsgi.py b/src/test_project/wsgi.py deleted file mode 100644 index 43ff066..0000000 --- a/src/test_project/wsgi.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -WSGI config for test_project project. - -This module contains the WSGI application used by Django's development server -and any production WSGI deployments. It should expose a module-level variable -named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover -this application via the ``WSGI_APPLICATION`` setting. - -Usually you will have the standard Django WSGI application here, but it also -might make sense to replace the whole Django WSGI application with a custom one -that later delegates to the Django one. For example, you could introduce WSGI -middleware here, or combine a Django application with an application of another -framework. - -""" -import os - -# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks -# if running multiple sites in the same mod_wsgi process. To fix this, use -# mod_wsgi daemon mode with each site in its own daemon process, or use -# os.environ["DJANGO_SETTINGS_MODULE"] = "test_project.settings" -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") - -# This application object is used by any WSGI server configured to use this -# file. This includes Django's development server, if the WSGI_APPLICATION -# setting points here. -from django.core.wsgi import get_wsgi_application -application = get_wsgi_application() - -# Apply WSGI middleware here. -# from helloworld.wsgi import HelloWorldApplication -# application = HelloWorldApplication(application) diff --git a/src/testapp/__init__.py b/src/testapp/__init__.py deleted file mode 100644 index e69de29..0000000 From a28a117d3b0a2f3fe6684b82a08d2b4e277314f5 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Fri, 15 May 2015 11:44:01 +0200 Subject: [PATCH 06/26] Fix typo in test settings --- src/auditlog_tests/test_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auditlog_tests/test_settings.py b/src/auditlog_tests/test_settings.py index 84ca8f6..de5c684 100644 --- a/src/auditlog_tests/test_settings.py +++ b/src/auditlog_tests/test_settings.py @@ -19,6 +19,6 @@ MIDDLEWARE_CLASSES = ( DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'auditlog_test', + 'NAME': 'auditlog_tests', } } From 5c1fe92ad3649dbb2c75b275bf9d8fe7990c95dc Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Fri, 15 May 2015 11:46:59 +0200 Subject: [PATCH 07/26] Place runtests.py outside tests app --- src/{auditlog_tests => }/runtests.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{auditlog_tests => }/runtests.py (100%) diff --git a/src/auditlog_tests/runtests.py b/src/runtests.py similarity index 100% rename from src/auditlog_tests/runtests.py rename to src/runtests.py From 872845436878ca286cecb76672d262b8ea90d482 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Fri, 15 May 2015 11:49:31 +0200 Subject: [PATCH 08/26] Fix travis config --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0c424b2..78b9652 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,4 +8,4 @@ env: install: - "pip install -r requirements.txt" - "pip install Django==$DJANGO_VERSION" -script: "python src/auditlog_tests/runtests.py" +script: "python src/runtests.py" From 3623812ab4e6c74fcbe68453325d7638700d0570 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Fri, 15 May 2015 15:14:57 +0200 Subject: [PATCH 09/26] Some work on m2m relationships --- src/auditlog/models.py | 18 ++++++++++++++++++ src/auditlog_tests/models.py | 3 +++ src/auditlog_tests/tests.py | 16 +++++++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/auditlog/models.py b/src/auditlog/models.py index ad0184b..4d5930d 100644 --- a/src/auditlog/models.py +++ b/src/auditlog/models.py @@ -6,6 +6,7 @@ from django.conf import settings from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.db import models +from django.db.models import QuerySet, Q from django.utils.encoding import python_2_unicode_compatible, smart_text from django.utils.six import iteritems, integer_types from django.utils.translation import ugettext_lazy as _ @@ -59,6 +60,23 @@ class LogEntryManager(models.Manager): else: return self.filter(content_type=content_type, object_pk=pk) + def get_for_objects(self, queryset): + """ + Get log entries for the objects in the specified queryset. + + :param queryset: The queryset to get the log entries for. + :type queryset: QuerySet + :return: The LogEntry objects for the objects in the given queryset. + :rtype: QuerySet + """ + if not isinstance(queryset, QuerySet) or queryset.count() == 0: + return self.none() + + content_type = ContentType.objects.get_for_model(queryset.model) + primary_keys = queryset.values_list(queryset.model._meta.pk.name, flat=True) + + return self.filter(content_type=content_type).filter(Q(object_id__in=primary_keys) | Q(object_pk__in=primary_keys)) + def get_for_model(self, model): """ Get log entries for all objects of a specified type. diff --git a/src/auditlog_tests/models.py b/src/auditlog_tests/models.py index 9a05d26..4d554a4 100644 --- a/src/auditlog_tests/models.py +++ b/src/auditlog_tests/models.py @@ -85,5 +85,8 @@ class SimpleExcludeModel(models.Model): auditlog.register(SimpleModel) auditlog.register(AltPrimaryKeyModel) auditlog.register(ProxyModel) +auditlog.register(RelatedModel) +auditlog.register(ManyRelatedModel) +auditlog.register(ManyRelatedModel.related.through) auditlog.register(SimpleIncludeModel, include_fields=['label', ]) auditlog.register(SimpleExcludeModel, exclude_fields=['text', ]) diff --git a/src/auditlog_tests/tests.py b/src/auditlog_tests/tests.py index cd93bd3..02ae9fa 100644 --- a/src/auditlog_tests/tests.py +++ b/src/auditlog_tests/tests.py @@ -7,7 +7,7 @@ from django.test import TestCase, RequestFactory from auditlog.middleware import AuditlogMiddleware from auditlog.models import LogEntry from auditlog_tests.models import SimpleModel, AltPrimaryKeyModel, ProxyModel, \ - SimpleIncludeModel, SimpleExcludeModel + SimpleIncludeModel, SimpleExcludeModel, RelatedModel, ManyRelatedModel class SimpleModelTest(TestCase): @@ -75,6 +75,20 @@ class ProxyModelTest(SimpleModelTest): 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()) + + class MiddlewareTest(TestCase): """ Test the middleware responsible for connecting and disconnecting the signals used in automatic logging. From 1173d3bc910ad17315cfa28f45f93065afa5b594 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Sun, 31 May 2015 13:52:29 +0200 Subject: [PATCH 10/26] Fix issue with possible duplicates in get_for_objects method --- src/auditlog/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auditlog/models.py b/src/auditlog/models.py index 4d5930d..c4c1f8e 100644 --- a/src/auditlog/models.py +++ b/src/auditlog/models.py @@ -75,7 +75,7 @@ class LogEntryManager(models.Manager): content_type = ContentType.objects.get_for_model(queryset.model) primary_keys = queryset.values_list(queryset.model._meta.pk.name, flat=True) - return self.filter(content_type=content_type).filter(Q(object_id__in=primary_keys) | Q(object_pk__in=primary_keys)) + return self.filter(content_type=content_type).filter(Q(object_id__in=primary_keys) | Q(object_pk__in=primary_keys)).distinct() def get_for_model(self, model): """ From 72cdde68648857471e34ecea6e0be5a58a163c70 Mon Sep 17 00:00:00 2001 From: Ann Paul Date: Mon, 1 Jun 2015 09:24:13 -0700 Subject: [PATCH 11/26] Change naming to use additional_data --- src/auditlog/.DS_Store | Bin 0 -> 6148 bytes .../0003_logentry_detailed_object_repr.py | 2 +- src/auditlog/models.py | 8 ++++---- 3 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 src/auditlog/.DS_Store diff --git a/src/auditlog/.DS_Store b/src/auditlog/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e45ac8a880831b863a137497b3602d393a46a6bc GIT binary patch literal 6148 zcmeHK!A`LnxFR zF~-a^GjF>)ud{t^x?KR$c};GBCV(1Uu~KI!F&$SwuuACBL2=jUV}%S0OnT`-wsrhP z1@zg~(MA`24CveEmyJhhHsYwh8lUu!jWf1@26G3PVNPGUb{j8P_tFCEj+w0;);SX9 zKk+Eb)*3CkY&ML*6`q-$@JKPiJBIpRl`-X>*}HWv9COP2A!j$^8#%|aI5+v)%q1vvnd)!jka7!_Bbt%54J7a&O4PqQH_sAZa{RlW2 KESLhns=yD@jCxi8 literal 0 HcmV?d00001 diff --git a/src/auditlog/migrations/0003_logentry_detailed_object_repr.py b/src/auditlog/migrations/0003_logentry_detailed_object_repr.py index bd71ddb..23f59bd 100644 --- a/src/auditlog/migrations/0003_logentry_detailed_object_repr.py +++ b/src/auditlog/migrations/0003_logentry_detailed_object_repr.py @@ -14,7 +14,7 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='logentry', - name='detailed_object_repr', + name='additional_data', field=jsonfield.fields.JSONField(null=True, blank=True), ), ] diff --git a/src/auditlog/models.py b/src/auditlog/models.py index f767ae5..323cf23 100644 --- a/src/auditlog/models.py +++ b/src/auditlog/models.py @@ -30,9 +30,9 @@ class LogEntryManager(models.Manager): if isinstance(pk, (int, long)): kwargs.setdefault('object_id', pk) - get_detailed_object_repr = getattr(instance, 'get_detailed_object_repr', None) - if callable(get_detailed_object_repr): - kwargs.setdefault('detailed_object_repr', instance.get_detailed_object_repr()) + get_additional_data = getattr(instance, 'get_additional_data', None) + if callable(get_additional_data): + kwargs.setdefault('additional_data', instance.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. @@ -116,7 +116,7 @@ class LogEntry(models.Model): changes = models.TextField(blank=True, verbose_name=_("change message")) actor = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_("actor")) timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp")) - detailed_object_repr = JSONField(blank=True, null=True) + additional_data = JSONField(blank=True, null=True) objects = LogEntryManager() From 5fb006b226c9604747cfa753af0b6bedf7cf5d17 Mon Sep 17 00:00:00 2001 From: Ann Paul Date: Mon, 1 Jun 2015 11:01:40 -0700 Subject: [PATCH 12/26] Tests for models with/without get_additional_data defined --- src/testapp/models.py | 25 +++++++++++++++++++++++++ src/testapp/tests.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/testapp/models.py b/src/testapp/models.py index 4d5d0eb..055ede1 100644 --- a/src/testapp/models.py +++ b/src/testapp/models.py @@ -82,8 +82,33 @@ class SimpleExcludeModel(models.Model): history = AuditlogHistoryField() +class AdditionDataIncludedModel(models.Model): + """ + A model where get_additional_data is defined which allows for logging extra + information about the model in JSON + """ + + label = models.CharField(max_length=100) + text = models.TextField(blank=True) + related = models.ForeignKey(SimpleModel) + + history = AuditlogHistoryField() + + def get_additional_data(self): + """ + Returns JSON that captures a snapshot of additional details of the + model instance. This method, if defined, is accessed by auditlog + manager and added to each logentry instance on creation. + """ + object_details = { + 'related_model_id': self.related.id, + 'related_model_text': self.related.text + } + return object_details + auditlog.register(SimpleModel) auditlog.register(AltPrimaryKeyModel) auditlog.register(ProxyModel) auditlog.register(SimpleIncludeModel, include_fields=['label', ]) auditlog.register(SimpleExcludeModel, exclude_fields=['label', ]) +auditlog.register(AdditionDataIncludedModel) diff --git a/src/testapp/tests.py b/src/testapp/tests.py index 53c73ae..6bfa01f 100644 --- a/src/testapp/tests.py +++ b/src/testapp/tests.py @@ -6,8 +6,8 @@ from django.http import HttpResponse from django.test import TestCase, RequestFactory from auditlog.middleware import AuditlogMiddleware from auditlog.models import LogEntry -from testapp.models import SimpleModel, AltPrimaryKeyModel, ProxyModel, \ - SimpleIncludeModel, SimpleExcludeModel +from testapp.models import (SimpleModel, AltPrimaryKeyModel, ProxyModel, + SimpleIncludeModel, SimpleExcludeModel, AdditionDataIncludedModel,) class SimpleModelTest(TestCase): @@ -178,3 +178,28 @@ class SimpeExcludeModelTest(TestCase): sem.text = 'Short text' sem.save() self.assertTrue(sem.history.count() == 2, msg="There are two log entries") + + +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_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') + obj_with_additional_data = AdditionDataIncludedModel( + 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") + log_entry = obj_with_additional_data.history.get() + self.assertIsNotNone(log_entry.additional_data) + extra_data = log_entry.additional_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") From 9d252f3310a8fe507188bb065d7e785a51d0a587 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 3 Jun 2015 15:50:41 +0200 Subject: [PATCH 13/26] Respect default values and non-string objects in diffs --- src/auditlog/diff.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/auditlog/diff.py b/src/auditlog/diff.py index b8194e2..2db3c3c 100644 --- a/src/auditlog/diff.py +++ b/src/auditlog/diff.py @@ -1,15 +1,15 @@ from __future__ import unicode_literals from django.core.exceptions import ObjectDoesNotExist -from django.db.models import Model +from django.db.models import Model, NOT_PROVIDED from django.utils.encoding import smart_text def model_instance_diff(old, new, **kwargs): """ Calculate the differences between two model instances. One of the instances may be None (i.e., a newly - created model or deleted model). This will cause all fields with a value to have changed (from None). - + created model or deleted model). This will cause all fields with a value to have changed (from the fields default + value). """ from auditlog.registry import auditlog @@ -48,17 +48,17 @@ def model_instance_diff(old, new, **kwargs): for field in fields: try: - old_value = smart_text(getattr(old, field.name, None)) + old_value = getattr(old, field.name, None) except ObjectDoesNotExist: - old_value = None + old_value = field.default if field.default is not NOT_PROVIDED else None try: - new_value = smart_text(getattr(new, field.name, None)) + new_value = getattr(new, field.name, None) except ObjectDoesNotExist: new_value = None if old_value != new_value: - diff[field.name] = (old_value, new_value) + diff[field.name] = (smart_text(old_value), smart_text(new_value)) if len(diff) == 0: diff = None From 18a04cfe7ed2ffc48879831f0c85cda346ee12fb Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 3 Jun 2015 16:06:25 +0200 Subject: [PATCH 14/26] Use _meta API in diffs where possible --- src/auditlog/diff.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/auditlog/diff.py b/src/auditlog/diff.py index 2db3c3c..3f11513 100644 --- a/src/auditlog/diff.py +++ b/src/auditlog/diff.py @@ -5,6 +5,26 @@ from django.db.models import Model, NOT_PROVIDED from django.utils.encoding import smart_text +def get_fields_in_model(instance): + """ + Returns the list of fields in the given model instance. Checks whether to use the official _meta API or use the raw + data. This method excludes many to many fields. + + :param instance: The model instance to get the fields for + :type instance: Model + :return: The list of fields for the given model (instance) + :rtype: list + """ + assert isinstance(instance, Model) + + # Check if the Django 1.8 _meta API is available + use_api = hasattr(instance._meta, 'get_fields') and callable(instance._meta.get_fields) + + if use_api: + return [f for f in instance._meta.get_fields() if not f.many_to_many] + return instance._meta.fields + + def model_instance_diff(old, new, **kwargs): """ Calculate the differences between two model instances. One of the instances may be None (i.e., a newly @@ -21,13 +41,13 @@ def model_instance_diff(old, new, **kwargs): diff = {} if old is not None and new is not None: - fields = set(old._meta.fields + new._meta.fields) + fields = set(get_fields_in_model(old) + get_fields_in_model(new)) model_fields = auditlog.get_model_fields(new._meta.model) elif old is not None: - fields = set(old._meta.fields) + fields = set(get_fields_in_model(old)) model_fields = auditlog.get_model_fields(old._meta.model) elif new is not None: - fields = set(new._meta.fields) + fields = set(get_fields_in_model(new)) model_fields = auditlog.get_model_fields(new._meta.model) else: fields = set() From 6e74d412681f36b921d84c0cc72929be0a3df828 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 3 Jun 2015 16:29:40 +0200 Subject: [PATCH 15/26] Use threadlocals to store middleware data instead of the request --- src/auditlog/middleware.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/auditlog/middleware.py b/src/auditlog/middleware.py index b62a9f4..6dbf401 100644 --- a/src/auditlog/middleware.py +++ b/src/auditlog/middleware.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import threading import time from django.conf import settings @@ -9,6 +10,9 @@ from django.db.models.loading import get_model from auditlog.models import LogEntry +threadlocal = threading.local() + + class AuditlogMiddleware(object): """ Middleware to couple the request's user to log items. This is accomplished by currying the signal receiver with the @@ -20,19 +24,27 @@ class AuditlogMiddleware(object): Gets the current user from the request and prepares and connects a signal receiver with the user already attached to it. """ + # Initialize thread local storage + threadlocal.auditlog = { + '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] + + # Connect signal for automatic logging if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated(): - user = request.user - request.auditlog_ts = time.time() - set_actor = curry(self.set_actor, user) - pre_save.connect(set_actor, sender=LogEntry, dispatch_uid=(self.__class__, request.auditlog_ts), weak=False) + set_actor = curry(self.set_actor, request.user) + 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. """ - # Disconnecting the signal receiver is required because it will not be garbage collected (non-weak reference) - if hasattr(request, 'auditlog_ts'): - pre_save.disconnect(sender=LogEntry, dispatch_uid=(self.__class__, request.auditlog_ts)) + if hasattr(threadlocal, 'auditlog'): + pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid']) return response @@ -40,8 +52,8 @@ class AuditlogMiddleware(object): """ Disconnects the signal receiver to prevent it from staying active in case of an exception. """ - if hasattr(request, 'auditlog_ts'): - pre_save.disconnect(sender=LogEntry, dispatch_uid=(self.__class__, request.auditlog_ts)) + if hasattr(threadlocal, 'auditlog'): + pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid']) return None From f630ef7da1ca8030ee0c5b371cba4fca10d53cdf Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 3 Jun 2015 16:45:30 +0200 Subject: [PATCH 16/26] Fix test db setting --- src/auditlog_tests/test_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auditlog_tests/test_settings.py b/src/auditlog_tests/test_settings.py index de5c684..fa2826f 100644 --- a/src/auditlog_tests/test_settings.py +++ b/src/auditlog_tests/test_settings.py @@ -19,6 +19,6 @@ MIDDLEWARE_CLASSES = ( DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'auditlog_tests', + 'NAME': 'auditlog_tests.db', } } From 93e468ab03ec2e01c976b934699cbd76822f274b Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 3 Jun 2015 16:45:51 +0200 Subject: [PATCH 17/26] Add remote address field to LogEntry and autopopulate it when possible --- .../migrations/0003_logentry_remote_addr.py | 19 +++++++++++++++++++ src/auditlog/models.py | 9 +++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/auditlog/migrations/0003_logentry_remote_addr.py diff --git a/src/auditlog/migrations/0003_logentry_remote_addr.py b/src/auditlog/migrations/0003_logentry_remote_addr.py new file mode 100644 index 0000000..948f63c --- /dev/null +++ b/src/auditlog/migrations/0003_logentry_remote_addr.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('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), + ), + ] diff --git a/src/auditlog/models.py b/src/auditlog/models.py index ad0184b..6df032d 100644 --- a/src/auditlog/models.py +++ b/src/auditlog/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import json +import threading from django.conf import settings from django.contrib.contenttypes import generic @@ -119,6 +120,7 @@ class LogEntry(models.Model): action = models.PositiveSmallIntegerField(choices=Action.choices, verbose_name=_("action")) changes = models.TextField(blank=True, verbose_name=_("change message")) actor = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_("actor")) + remote_addr = models.GenericIPAddressField(blank=True, null=True, verbose_name=_("remote address")) timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp")) objects = LogEntryManager() @@ -141,6 +143,13 @@ class LogEntry(models.Model): return fstring.format(repr=self.object_repr) + def clean(self): + threadlocal = threading.local() + + # Set remote_addr on creation if empty and available in thread + if not self.pk and self.remote_addr is None and hasattr(threadlocal, 'auditlog'): + self.remote_addr = threading.local().auditlog.get('remote_addr', None) + @property def changes_dict(self): """ From 0c0d1dffe121d469f495bbe8e96a1b9dc32390be Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 3 Jun 2015 16:49:04 +0200 Subject: [PATCH 18/26] Only set remote_addr in set_actor method --- src/auditlog/middleware.py | 2 ++ src/auditlog/models.py | 8 -------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/auditlog/middleware.py b/src/auditlog/middleware.py index 6dbf401..14263cb 100644 --- a/src/auditlog/middleware.py +++ b/src/auditlog/middleware.py @@ -70,3 +70,5 @@ class AuditlogMiddleware(object): auth_user_model = get_model('auth', 'user') if sender == LogEntry and isinstance(user, auth_user_model) and instance.actor is None: instance.actor = user + if hasattr(threadlocal, 'auditlog'): + instance.remote_addr = threading.local().auditlog['remote_addr'] diff --git a/src/auditlog/models.py b/src/auditlog/models.py index 6df032d..0af6056 100644 --- a/src/auditlog/models.py +++ b/src/auditlog/models.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import json -import threading from django.conf import settings from django.contrib.contenttypes import generic @@ -143,13 +142,6 @@ class LogEntry(models.Model): return fstring.format(repr=self.object_repr) - def clean(self): - threadlocal = threading.local() - - # Set remote_addr on creation if empty and available in thread - if not self.pk and self.remote_addr is None and hasattr(threadlocal, 'auditlog'): - self.remote_addr = threading.local().auditlog.get('remote_addr', None) - @property def changes_dict(self): """ From c40ae32955b29e87307d4bc9608931c91a45f8b0 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 3 Jun 2015 16:50:00 +0200 Subject: [PATCH 19/26] Update documentation for new feature --- docs/source/usage.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index e043d31..f473436 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -25,7 +25,8 @@ Actors ------ When using automatic logging, the actor is empty by default. However, auditlog can set the actor from the current -request automatically. This does not need any custom code, adding a middleware class is enough. +request automatically. This does not need any custom code, adding a middleware class is enough. When an actor is logged +the remote address of that actor will be logged as well. To enable the automatic logging of the actors, simply add the following to your ``MIDDLEWARE_CLASSES`` setting in your project's configuration file:: From 23927ea31741942d0481eca045a518f258e6a568 Mon Sep 17 00:00:00 2001 From: Ann Paul Date: Wed, 3 Jun 2015 08:47:50 -0700 Subject: [PATCH 20/26] Add verbose name to the additional_data field Directly call the get_additional_data method without instance --- src/auditlog/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/auditlog/models.py b/src/auditlog/models.py index 323cf23..dbe82b1 100644 --- a/src/auditlog/models.py +++ b/src/auditlog/models.py @@ -32,7 +32,7 @@ class LogEntryManager(models.Manager): get_additional_data = getattr(instance, 'get_additional_data', None) if callable(get_additional_data): - kwargs.setdefault('additional_data', instance.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. @@ -116,7 +116,7 @@ class LogEntry(models.Model): changes = models.TextField(blank=True, verbose_name=_("change message")) actor = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name='+', verbose_name=_("actor")) timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp")) - additional_data = JSONField(blank=True, null=True) + additional_data = JSONField(blank=True, null=True, verbose_name=_("additional data")) objects = LogEntryManager() From bade679b8136e8b8332a8e4d27eaa2fa8cff9d60 Mon Sep 17 00:00:00 2001 From: Ann Paul Date: Wed, 3 Jun 2015 08:49:03 -0700 Subject: [PATCH 21/26] Fix typo on test class name changing it to AdditionalDataIncludedModel --- src/testapp/models.py | 4 ++-- src/testapp/tests.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/testapp/models.py b/src/testapp/models.py index 055ede1..96c0a9d 100644 --- a/src/testapp/models.py +++ b/src/testapp/models.py @@ -82,7 +82,7 @@ class SimpleExcludeModel(models.Model): history = AuditlogHistoryField() -class AdditionDataIncludedModel(models.Model): +class AdditionalDataIncludedModel(models.Model): """ A model where get_additional_data is defined which allows for logging extra information about the model in JSON @@ -111,4 +111,4 @@ auditlog.register(AltPrimaryKeyModel) auditlog.register(ProxyModel) auditlog.register(SimpleIncludeModel, include_fields=['label', ]) auditlog.register(SimpleExcludeModel, exclude_fields=['label', ]) -auditlog.register(AdditionDataIncludedModel) +auditlog.register(AdditionalDataIncludedModel) diff --git a/src/testapp/tests.py b/src/testapp/tests.py index 6bfa01f..8d395bc 100644 --- a/src/testapp/tests.py +++ b/src/testapp/tests.py @@ -7,7 +7,7 @@ from django.test import TestCase, RequestFactory from auditlog.middleware import AuditlogMiddleware from auditlog.models import LogEntry from testapp.models import (SimpleModel, AltPrimaryKeyModel, ProxyModel, - SimpleIncludeModel, SimpleExcludeModel, AdditionDataIncludedModel,) + SimpleIncludeModel, SimpleExcludeModel, AdditionalDataIncludedModel,) class SimpleModelTest(TestCase): @@ -191,7 +191,7 @@ class AdditionalDataModelTest(TestCase): def test_model_with_additional_data(self): related_model = SimpleModel.objects.create(text='Log my reference') - obj_with_additional_data = AdditionDataIncludedModel( + obj_with_additional_data = AdditionalDataIncludedModel( label='Additional data to log entries', related=related_model) obj_with_additional_data.save() self.assertTrue(obj_with_additional_data.history.count() == 1, From a689823b2629952a6a66458a1daa7a74ea8f62ca Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 22 Jul 2015 00:03:54 +0200 Subject: [PATCH 22/26] Exclude AuditlogHistoryField from diffs --- src/auditlog/diff.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/auditlog/diff.py b/src/auditlog/diff.py index 3f11513..b25dac3 100644 --- a/src/auditlog/diff.py +++ b/src/auditlog/diff.py @@ -15,13 +15,14 @@ def get_fields_in_model(instance): :return: The list of fields for the given model (instance) :rtype: list """ + from auditlog.models import AuditlogHistoryField assert isinstance(instance, Model) # Check if the Django 1.8 _meta API is available use_api = hasattr(instance._meta, 'get_fields') and callable(instance._meta.get_fields) if use_api: - return [f for f in instance._meta.get_fields() if not f.many_to_many] + return [f for f in instance._meta.get_fields() if not (f.many_to_many or isinstance(f, AuditlogHistoryField))] return instance._meta.fields From 7d6380206fc3ae1b015dd3e9878c175603c32d0e Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 22 Jul 2015 00:16:33 +0200 Subject: [PATCH 23/26] Extract field tracking test, do not track any relation to LogEntry --- src/auditlog/diff.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/auditlog/diff.py b/src/auditlog/diff.py index b25dac3..15643d1 100644 --- a/src/auditlog/diff.py +++ b/src/auditlog/diff.py @@ -5,6 +5,29 @@ from django.db.models import Model, NOT_PROVIDED from django.utils.encoding import smart_text +def track_field(field): + """ + Returns whether the given field should be tracked by Auditlog. + + Untracked fields are many-to-many relations and relations to the Auditlog LogEntry model. + + :param field: The field to check. + :type field: Field + :return: Whether the given field should be tracked. + :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, 'rel') is not None and field.rel.to == LogEntry: + return False + + return True + + def get_fields_in_model(instance): """ Returns the list of fields in the given model instance. Checks whether to use the official _meta API or use the raw @@ -15,14 +38,13 @@ def get_fields_in_model(instance): :return: The list of fields for the given model (instance) :rtype: list """ - from auditlog.models import AuditlogHistoryField assert isinstance(instance, Model) # Check if the Django 1.8 _meta API is available use_api = hasattr(instance._meta, 'get_fields') and callable(instance._meta.get_fields) if use_api: - return [f for f in instance._meta.get_fields() if not (f.many_to_many or isinstance(f, AuditlogHistoryField))] + return [f for f in instance._meta.get_fields() if track_field(f)] return instance._meta.fields From 882415ea188279e351d5b8f9872ee7607512831a Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 22 Jul 2015 00:17:04 +0200 Subject: [PATCH 24/26] Explicitly specify sudo in travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 78b9652..2aeedd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,3 +9,4 @@ install: - "pip install -r requirements.txt" - "pip install Django==$DJANGO_VERSION" script: "python src/runtests.py" +sudo: false From 99d87bee52314559bb219bb539b477377c7e1421 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 22 Jul 2015 00:24:46 +0200 Subject: [PATCH 25/26] Fix overlapping migrations --- ...led_object_repr.py => 0004_logentry_detailed_object_repr.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/auditlog/migrations/{0003_logentry_detailed_object_repr.py => 0004_logentry_detailed_object_repr.py} (86%) diff --git a/src/auditlog/migrations/0003_logentry_detailed_object_repr.py b/src/auditlog/migrations/0004_logentry_detailed_object_repr.py similarity index 86% rename from src/auditlog/migrations/0003_logentry_detailed_object_repr.py rename to src/auditlog/migrations/0004_logentry_detailed_object_repr.py index 23f59bd..2b9af2c 100644 --- a/src/auditlog/migrations/0003_logentry_detailed_object_repr.py +++ b/src/auditlog/migrations/0004_logentry_detailed_object_repr.py @@ -8,7 +8,7 @@ import jsonfield.fields class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0002_auto_support_long_primary_keys'), + ('auditlog', '0003_logentry_remote_addr'), ] operations = [ From ccaf3925e478662cee17fabc3cd7dadde59206f0 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 22 Jul 2015 00:32:27 +0200 Subject: [PATCH 26/26] Provide fallback for when a field has no 'rel' attribute --- src/auditlog/diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auditlog/diff.py b/src/auditlog/diff.py index 15643d1..7346d8d 100644 --- a/src/auditlog/diff.py +++ b/src/auditlog/diff.py @@ -22,7 +22,7 @@ def track_field(field): return False # Do not track relations to LogEntry - if getattr(field, 'rel') is not None and field.rel.to == LogEntry: + if getattr(field, 'rel', None) is not None and field.rel.to == LogEntry: return False return True