Skip to content

Commit 89ccdce

Browse files
committed
Add exclude_history_fields field for binder history
1 parent ed91f29 commit 89ccdce

File tree

3 files changed

+87
-1
lines changed

3 files changed

+87
-1
lines changed

binder/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ def binder_serialize_m2m_field(self, field):
608608

609609
class Binder:
610610
history = False
611+
exclude_history_fields: list[str] = []
611612

612613
class Meta:
613614
abstract = True
@@ -683,13 +684,22 @@ def field_needs_nullability_check(field):
683684
def history_obj_post_init(sender, instance, **kwargs):
684685
instance._history = instance.binder_concrete_fields_as_dict(skip_deferred_fields=True)
685686

687+
excluded_fields = getattr(sender.Binder, 'exclude_history_fields', [])
688+
for field_name in excluded_fields:
689+
instance._history.pop(field_name, None)
690+
686691
if not instance.pk:
687692
instance._history = {k: history.NewInstanceField for k in instance._history}
688693

689694

690695

691696
def history_obj_post_save(sender, instance, **kwargs):
697+
excluded_fields = getattr(sender.Binder, 'exclude_history_fields', [])
698+
692699
for field_name, new_value in instance.binder_concrete_fields_as_dict().items():
700+
if field_name in excluded_fields:
701+
continue
702+
693703
try:
694704
old_value = instance._history[field_name]
695705
if old_value != new_value:

docs/models.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,20 @@ class Animal(BinderModel):
9898
history = True
9999
```
100100

101+
You can also exclude specific fields from history tracking by setting `exclude_history_fields`:
102+
103+
```python
104+
from binder.models import BinderModel
105+
106+
class Animal(BinderModel):
107+
name = models.TextField()
108+
secret_notes = models.TextField() # This field won't be tracked
109+
110+
class Binder:
111+
history = True
112+
exclude_history_fields = ['secret_notes']
113+
```
114+
101115
Saving the model will result in one changeset. With a changeset, the user that changed it and datetime is saved.
102116

103117
A changeset contains changes for each field that has been changed to a new value. For each change, you can see the old value and the new value.

tests/test_history.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from binder import history
88
from binder.history import Change, Changeset
99

10-
from .testapp.models import Animal, Caretaker, ContactPerson
10+
from .testapp.models import Animal, Caretaker, ContactPerson, Zoo
1111

1212

1313
class HistoryTest(TestCase):
@@ -346,3 +346,65 @@ class TestException(Exception):
346346
# Aborted, so still no history
347347
self.assertEqual(0, Changeset.objects.count())
348348
self.assertEqual(0, Change.objects.count())
349+
350+
def test_exclude_history_fields_prevents_tracking(self):
351+
"""Test that fields listed in exclude_history_fields are not tracked in history"""
352+
original_exclude_fields = getattr(Animal.Binder, 'exclude_history_fields', [])
353+
Animal.Binder.exclude_history_fields = ['name']
354+
355+
try:
356+
model_data = {
357+
'name': 'Test Animal',
358+
}
359+
response = self.client.post('/animal/', data=json.dumps(model_data), content_type='application/json')
360+
self.assertEqual(response.status_code, 200)
361+
animal_id = json.loads(response.content)['id']
362+
363+
# Check that we have a changeset for the creation
364+
self.assertEqual(1, Changeset.objects.count())
365+
cs = Changeset.objects.get()
366+
367+
# Check that changes exist for normal fields but not for excluded fields
368+
changes = Change.objects.filter(changeset=cs)
369+
field_names = [change.field for change in changes]
370+
371+
# The 'name' field should be excluded from history
372+
self.assertNotIn('name', field_names)
373+
# Other fields should still be tracked
374+
self.assertIn('id', field_names)
375+
self.assertIn('deleted', field_names)
376+
377+
model_data = {
378+
'name': 'Updated Animal Name',
379+
}
380+
response = self.client.patch(f'/animal/{animal_id}/', data=json.dumps(model_data), content_type='application/json')
381+
self.assertEqual(response.status_code, 200)
382+
383+
# Should still only have 1 changeset (no new one created for excluded field)
384+
self.assertEqual(1, Changeset.objects.count())
385+
386+
# Now update both an excluded field AND a non-excluded field
387+
zoo = Zoo.objects.create(name='Test Zoo')
388+
model_data = {
389+
'name': 'Another Name Update', # excluded field
390+
'zoo': zoo.id, # non-excluded field
391+
}
392+
response = self.client.patch(f'/animal/{animal_id}/', data=json.dumps(model_data), content_type='application/json')
393+
self.assertEqual(response.status_code, 200)
394+
395+
# Now we should have 2 changesets (original creation + this mixed update)
396+
self.assertEqual(2, Changeset.objects.count())
397+
398+
# Check the latest changeset (for the mixed update)
399+
latest_cs = Changeset.objects.order_by('-id').first()
400+
latest_changes = Change.objects.filter(changeset=latest_cs)
401+
latest_field_names = [change.field for change in latest_changes]
402+
403+
# Only the zoo field should be recorded, not the name
404+
self.assertEqual(1, len(latest_field_names))
405+
self.assertIn('zoo', latest_field_names)
406+
self.assertNotIn('name', latest_field_names)
407+
408+
finally:
409+
# Restore original exclude_history_fields setting
410+
Animal.Binder.exclude_history_fields = original_exclude_fields

0 commit comments

Comments
 (0)