Skip to content

Commit

Permalink
Add relation history
Browse files Browse the repository at this point in the history
  • Loading branch information
knokko committed Jan 25, 2024
1 parent 9e8bd20 commit 3799849
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 1 deletion.
28 changes: 28 additions & 0 deletions binder/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,22 @@ def _commit():
)
changeset.save()

changed_objs = dict()

for (model, oid, field), (old, new, diff) in _Transaction.changes.items():
changed_fields = changed_objs.get((model, oid), [])
changed_fields.append(field)
changed_objs[(model, oid)] = changed_fields

relation_changes = dict()
for (model, oid), fields in changed_objs.items():
for field in model._meta.get_fields():
if field.auto_created and not field.concrete and not field.many_to_many and field.related_model.Binder.history:
related_field = field.field.name + '_id'
related_objects = list(field.related_model.objects.filter(**{related_field: oid}).all())
for related_object in related_objects:
relation_changes[(field.related_model, related_object.pk, field.field.name)] = fields

for (model, oid, field), (old, new, diff) in _Transaction.changes.items():
# New instances get None for all the before values
if old is NewInstanceField:
Expand All @@ -187,10 +203,22 @@ def _commit():
before=jsondumps(old),
after=jsondumps(new),
)
relation_changes.pop((model, oid, field), None)
change.save()

transaction_commit.send(sender=None, changeset=changeset)

for (model, oid, field), related_fields in relation_changes.items():
Change(
changeset=changeset,
model=model.__name__,
oid=oid,
field=field,
diff=False,
before='Related object changed',
after='Changed ' + str(related_fields)
).save()

# Save the changeset again, to update the date to be as close to DB transaction commit start as possible.
changeset.save()
_Transaction.stop()
Expand Down
279 changes: 278 additions & 1 deletion tests/test_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from binder import history
from binder.history import Change, Changeset

from .testapp.models import Animal, Caretaker, ContactPerson
from .testapp.models import Animal, Caretaker, Costume, ContactPerson, Zoo


class HistoryTest(TestCase):
Expand Down Expand Up @@ -151,6 +151,283 @@ def test_model_with_related_history_model_creates_changes_on_the_same_changeset(
self.assertEqual(1, Change.objects.filter(changeset=cs, model='Caretaker', field='name', before='"Mickey"', after='"Mickey Mouse"').count())


def test_change_related_object(self):
mickey = Caretaker(name='Mickey')
mickey.save()
pluto = Animal(name='Pluto', caretaker=mickey)
pluto.save()

self.assertEqual(0, Change.objects.count())

model_data = {
'data': [{
'id': mickey.id,
'name': 'Mickey Mouse',
}],
}
response = self.client.put('/caretaker/', data=json.dumps(model_data), content_type='application/json')
self.assertEqual(response.status_code, 200)

self.assertEqual(2, Change.objects.count())
self.assertEqual(1, Change.objects.filter(model='Animal', field='caretaker', before='Related object changed', after="Changed ['name']").count())
self.assertEqual(1, Change.objects.filter(model='Caretaker', field='name', before='"Mickey"', after='"Mickey Mouse"').count())


def test_change_related_object_one_to_one(self):
pluto = Animal(name='Pluto')
pluto.save()
costume = Costume(nickname='Flapdrol', animal=pluto)
costume.save()

self.assertEqual(0, Change.objects.count())

model_data = {
'data': [{
'id': pluto.id,
'name': 'Pluto the dog',
}],
}
response = self.client.put('/animal/', data=json.dumps(model_data), content_type='application/json')
self.assertEqual(response.status_code, 200)

self.assertEqual(2, Change.objects.count())
self.assertEqual(1, Change.objects.filter(model='Animal', field='name', before='"Pluto"', after='"Pluto the dog"').count())
self.assertEqual(1, Change.objects.filter(model='Costume', field='animal', before='Related object changed', after="Changed ['name']").count())


def test_change_related_object_one_to_one_ignore_forward(self):
pluto = Animal(name='Pluto')
pluto.save()
costume = Costume(nickname='Flapdrol', animal=pluto)
costume.save()

self.assertEqual(0, Change.objects.count())

model_data = {
'data': [{
'id': pluto.id,
'nickname': 'Cutie',
}],
}
response = self.client.put('/costume/', data=json.dumps(model_data), content_type='application/json')
self.assertEqual(response.status_code, 200)

self.assertEqual(1, Change.objects.count())
self.assertEqual(1, Change.objects.filter(model='Costume', field='nickname', before='"Flapdrol"', after='"Cutie"').count())


def test_change_related_object_multiple_fields(self):
mickey = Caretaker(name='Mickey')
mickey.save()
pluto = Animal(name='Pluto', caretaker=mickey)
pluto.save()
mars = Animal(name='Mars', caretaker=mickey)
mars.save()

self.assertEqual(0, Change.objects.count())

model_data = {
'data': [{
'id': mickey.id,
'name': 'Mickey Mouse',
'ssn': 'test1234'
}],
}
response = self.client.put('/caretaker/', data=json.dumps(model_data), content_type='application/json')
self.assertEqual(response.status_code, 200)

self.assertEqual(4, Change.objects.count())
self.assertEqual(1, Change.objects.filter(model='Animal', oid=pluto.id, field='caretaker', before='Related object changed', after="Changed ['name', 'ssn']").count())
self.assertEqual(1, Change.objects.filter(model='Animal', oid=mars.id, field='caretaker', before='Related object changed', after="Changed ['name', 'ssn']").count())
self.assertEqual(1, Change.objects.filter(model='Caretaker', field='name', before='"Mickey"', after='"Mickey Mouse"').count())
self.assertEqual(1, Change.objects.filter(model='Caretaker', field='ssn', before='"my secret ssn"', after='"test1234"').count())


def test_assign_related_object(self):
mickey = Caretaker(name='Mickey')
mickey.save()
pluto = Animal(name='Pluto')
pluto.save()

self.assertEqual(0, Change.objects.count())
model_data = {
'data': [{
'id': pluto.id,
'caretaker': mickey.id
}],
'with': {
'caretaker': [{
'id': mickey.id,
'name': 'Mickey Mouse',
}],
},
}

response = self.client.put('/animal/', data=json.dumps(model_data), content_type='application/json')
self.assertEqual(response.status_code, 200)

self.assertEqual(2, Change.objects.count())
self.assertEqual(1, Change.objects.filter(model='Animal', field='caretaker', before='null', after=mickey.id).count())
self.assertEqual(1, Change.objects.filter(model='Caretaker', field='name', before='"Mickey"', after='"Mickey Mouse"').count())


def test_unassign_related_object(self):
mickey = Caretaker(name='Mickey')
mickey.save()
pluto = Animal(name='Pluto', caretaker=mickey)
pluto.save()

self.assertEqual(0, Change.objects.count())
model_data = {
'data': [{
'id': pluto.id,
'caretaker': None
}],
'with': {
'caretaker': [{
'id': mickey.id,
'name': 'Mickey Mouse',
}],
},
}

response = self.client.put('/animal/', data=json.dumps(model_data), content_type='application/json')
self.assertEqual(response.status_code, 200)

self.assertEqual(2, Change.objects.count())
self.assertEqual(1, Change.objects.filter(model='Animal', field='caretaker', before=mickey.id, after='null').count())
self.assertEqual(1, Change.objects.filter(model='Caretaker', field='name', before='"Mickey"', after='"Mickey Mouse"').count())


def test_change_object_and_related_object(self):
mickey = Caretaker(name='Mickey')
mickey.save()
pluto = Animal(name='Pluto', caretaker=mickey)
pluto.save()

self.assertEqual(0, Change.objects.count())

model_data = {
'data': [{
'id': pluto.id,
'name': 'Pluto the dog',
}],
'with': {
'caretaker': [{
'id': mickey.id,
'name': 'Mickey Mouse',
}],
},
}
response = self.client.put('/animal/', data=json.dumps(model_data), content_type='application/json')
self.assertEqual(response.status_code, 200)

self.assertEqual(3, Change.objects.count())
self.assertEqual(1, Change.objects.filter(model='Animal', field='name', before='"Pluto"', after='"Pluto the dog"').count())
self.assertEqual(1, Change.objects.filter(model='Animal', field='caretaker', before='Related object changed', after="Changed ['name']").count())
self.assertEqual(1, Change.objects.filter(model='Caretaker', field='name', before='"Mickey"', after='"Mickey Mouse"').count())


def test_related_changes_ignore_many_to_many(self):
pluto = Animal(name='Pluto')
pluto.save()

disney = Zoo(name='Disney')
disney.save()

disney.most_popular_animals.set([pluto])
disney.save()

self.assertEqual(0, Change.objects.count())

model_data = {
'data': [{
'id': pluto.id,
'name': 'Pluto the dog',
}],
}
response = self.client.put('/animal/', data=json.dumps(model_data), content_type='application/json')
self.assertEqual(response.status_code, 200)

self.assertEqual(1, Change.objects.count())
self.assertEqual(1, Change.objects.filter(model='Animal', field='name', before='"Pluto"', after='"Pluto the dog"').count())


def test_related_changes_ignore_many_to_many_reverse(self):
pluto = Animal(name='Pluto')
pluto.save()

disney = Zoo(name='Disney')
disney.save()

disney.most_popular_animals.set([pluto])
disney.save()

self.assertEqual(0, Change.objects.count())

model_data = {
'data': [{
'id': disney.id,
'name': 'Disneyland',
}],
}
response = self.client.put('/zoo/', data=json.dumps(model_data), content_type='application/json')
self.assertEqual(response.status_code, 200)

self.assertEqual(1, Change.objects.count())
self.assertEqual(1, Change.objects.filter(model='Zoo', field='name', before='"Disney"', after='"Disneyland"').count())


def test_related_changes_ignore_many_to_many_named(self):
contact = ContactPerson(name='Joe')
contact.save()

disney = Zoo(name='Disney')
disney.save()

disney.contacts.set([contact])
disney.save()

self.assertEqual(0, Change.objects.count())

model_data = {
'data': [{
'id': contact.id,
'name': 'Uncle Joe',
}],
}
response = self.client.put('/contact_person/', data=json.dumps(model_data), content_type='application/json')
self.assertEqual(response.status_code, 200)

self.assertEqual(2, Change.objects.count())
self.assertEqual(1, Change.objects.filter(model='ContactPerson', field='name', before='"Joe"', after='"Uncle Joe"').count())
self.assertEqual(1, Change.objects.filter(model='ContactPerson', field='updated_at').count())


def test_related_changes_ignore_many_to_many_named_reverse(self):
contact = ContactPerson(name='Joe')
contact.save()

disney = Zoo(name='Disney')
disney.save()

disney.contacts.set([contact])
disney.save()

self.assertEqual(0, Change.objects.count())

model_data = {
'data': [{
'id': disney.id,
'name': 'Disneyland',
}],
}
response = self.client.put('/zoo/', data=json.dumps(model_data), content_type='application/json')
self.assertEqual(response.status_code, 200)

self.assertEqual(1, Change.objects.count())
self.assertEqual(1, Change.objects.filter(model='Zoo', field='name', before='"Disney"', after='"Disneyland"').count())


def test_manual_history_direct_success(self):
history.start(source='tests')
Expand Down
3 changes: 3 additions & 0 deletions tests/testapp/models/contact_person.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ class ContactPerson(BinderModel):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Binder:
history = True

def __str__(self):
return 'contact_person %d: %s' % (self.pk, self.name)

Expand Down
3 changes: 3 additions & 0 deletions tests/testapp/models/costume.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class Costume(BinderModel):
class Meta(BinderModel.Meta):
ordering = ['animal_id']

class Binder:
history = True

nickname = models.TextField(blank=True)
description = models.TextField(blank=True, null=True)
animal = models.OneToOneField('Animal', on_delete=models.CASCADE, related_name='costume', primary_key=True)
Expand Down

0 comments on commit 3799849

Please sign in to comment.