diff --git a/requirements.txt b/requirements.txt index e38f742..e601b92 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ Django>=1.7 +django-jsonfield>=0.9.13 diff --git a/setup.py b/setup.py index a1af638..7b4d184 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.7' + 'Django>=1.7', + 'django-jsonfield>=0.9.13', ] ) 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..23f59bd --- /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='additional_data', + field=jsonfield.fields.JSONField(null=True, blank=True), + ), + ] diff --git a/src/auditlog/models.py b/src/auditlog/models.py index c4c1f8e..724bb66 100644 --- a/src/auditlog/models.py +++ b/src/auditlog/models.py @@ -11,6 +11,8 @@ 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 _ +from jsonfield import JSONField + class LogEntryManager(models.Manager): """ @@ -33,6 +35,10 @@ class LogEntryManager(models.Manager): if isinstance(pk, integer_types): kwargs.setdefault('object_id', pk) + get_additional_data = getattr(instance, 'get_additional_data', None) + if callable(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: @@ -138,6 +144,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, verbose_name=_("additional data")) objects = LogEntryManager() diff --git a/src/auditlog_tests/models.py b/src/auditlog_tests/models.py index 4d554a4..d301cf2 100644 --- a/src/auditlog_tests/models.py +++ b/src/auditlog_tests/models.py @@ -82,11 +82,36 @@ class SimpleExcludeModel(models.Model): history = AuditlogHistoryField() +class AdditionalDataIncludedModel(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(RelatedModel) auditlog.register(ManyRelatedModel) auditlog.register(ManyRelatedModel.related.through) -auditlog.register(SimpleIncludeModel, include_fields=['label', ]) -auditlog.register(SimpleExcludeModel, exclude_fields=['text', ]) +auditlog.register(SimpleIncludeModel, include_fields=['label']) +auditlog.register(SimpleExcludeModel, exclude_fields=['text']) +auditlog.register(AdditionalDataIncludedModel) diff --git a/src/auditlog_tests/tests.py b/src/auditlog_tests/tests.py index 02ae9fa..46b768d 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, RelatedModel, ManyRelatedModel + SimpleIncludeModel, SimpleExcludeModel, RelatedModel, ManyRelatedModel, AdditionalDataIncludedModel class SimpleModelTest(TestCase): @@ -192,3 +192,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 = AdditionalDataIncludedModel( + 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")