Skip to content

Commit

Permalink
(feature) Allow AuditlogHistoryField to block cascading deletes (#172)
Browse files Browse the repository at this point in the history
Fixes #108

Thanks to @andrewwatts for suggesting and researching the approach.
  • Loading branch information
kbussell authored and audiolion committed Mar 27, 2018
1 parent bc886fa commit 3a82338
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 3 deletions.
22 changes: 20 additions & 2 deletions src/auditlog/models.py
Expand Up @@ -7,7 +7,7 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist
from django.db import models
from django.db import models, DEFAULT_DB_ALIAS
from django.db.models import QuerySet, Q
from django.utils import formats, timezone
from django.utils.encoding import python_2_unicode_compatible, smart_text
Expand Down Expand Up @@ -321,9 +321,13 @@ class AuditlogHistoryField(GenericRelation):
:param pk_indexable: Whether the primary key for this model is not an :py:class:`int` or :py:class:`long`.
:type pk_indexable: bool
:param delete_related: By default, including a generic relation into a model will cause all related objects to be
cascade-deleted when the parent object is deleted. Passing False to this overrides this behavior, retaining
the full auditlog history for the object. Defaults to True, because that's Django's default behavior.
:type delete_related: bool
"""

def __init__(self, pk_indexable=True, **kwargs):
def __init__(self, pk_indexable=True, delete_related=True, **kwargs):
kwargs['to'] = LogEntry

if pk_indexable:
Expand All @@ -332,8 +336,22 @@ def __init__(self, pk_indexable=True, **kwargs):
kwargs['object_id_field'] = 'object_pk'

kwargs['content_type_field'] = 'content_type'
self.delete_related = delete_related
super(AuditlogHistoryField, self).__init__(**kwargs)

def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS):
"""
Return all objects related to ``objs`` via this ``GenericRelation``.
"""
if self.delete_related:
return super(AuditlogHistoryField, self).bulk_related_objects(objs, using)

# When deleting, Collector.collect() finds related objects using this
# method. However, because we don't want to delete these related
# objects, we simply return an empty list.
return []


# South compatibility for AuditlogHistoryField
try:
from south.modelsinspector import add_introspection_rules
Expand Down
7 changes: 7 additions & 0 deletions src/auditlog_tests/models.py
Expand Up @@ -209,6 +209,12 @@ class PostgresArrayFieldModel(models.Model):
history = AuditlogHistoryField()


class NoDeleteHistoryModel(models.Model):
integer = models.IntegerField(blank=True, null=True)

history = AuditlogHistoryField(delete_related=False)


auditlog.register(AltPrimaryKeyModel)
auditlog.register(UUIDPrimaryKeyModel)
auditlog.register(ProxyModel)
Expand All @@ -222,3 +228,4 @@ class PostgresArrayFieldModel(models.Model):
auditlog.register(ChoicesFieldModel)
auditlog.register(CharfieldTextfieldModel)
auditlog.register(PostgresArrayFieldModel)
auditlog.register(NoDeleteHistoryModel)
32 changes: 31 additions & 1 deletion src/auditlog_tests/tests.py
Expand Up @@ -16,7 +16,7 @@
from auditlog_tests.models import SimpleModel, AltPrimaryKeyModel, UUIDPrimaryKeyModel, \
ProxyModel, SimpleIncludeModel, SimpleExcludeModel, SimpleMappingModel, RelatedModel, \
ManyRelatedModel, AdditionalDataIncludedModel, DateTimeFieldModel, ChoicesFieldModel, \
CharfieldTextfieldModel, PostgresArrayFieldModel
CharfieldTextfieldModel, PostgresArrayFieldModel, NoDeleteHistoryModel
from auditlog import compat


Expand Down Expand Up @@ -663,3 +663,33 @@ def test_auditlog_admin(self):
res = self.client.get("/admin/auditlog/logentry/{}/history/".format(log_pk))
assert res.status_code == 200


class NoDeleteHistoryTest(TestCase):
def test_delete_related(self):
instance = SimpleModel.objects.create(integer=1)
assert LogEntry.objects.all().count() == 1
instance.integer = 2
instance.save()
assert LogEntry.objects.all().count() == 2

instance.delete()
entries = LogEntry.objects.order_by('id')

# The "DELETE" record is always retained
assert LogEntry.objects.all().count() == 1
assert entries.first().action == LogEntry.Action.DELETE

def test_no_delete_related(self):
instance = NoDeleteHistoryModel.objects.create(integer=1)
self.assertEqual(LogEntry.objects.all().count(), 1)
instance.integer = 2
instance.save()
self.assertEqual(LogEntry.objects.all().count(), 2)

instance.delete()
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]
)

0 comments on commit 3a82338

Please sign in to comment.