Going through historical changes with django-simple-history

django May 3, 2021

I have been using django-simple-history for one of my project just to track the changes between edits. This library provides an easy way to revisit the historical changes by storing the Django model state between every create/update/delete operation. It also provides some really handy utilities out of the box. One such utility is history diffing.

jazzband/django-simple-history
Store model history and view/revert changes from admin site. - jazzband/django-simple-history

When we have two instances of the same HistoricalRecord, we can perform diffs to see what has changed.

The document uses the example of a simple Polls application and creates two records. It then tries to check the delta change between those records -

>>> p = Poll.objects.create(question="What's up?")
>>> p.question = "What's Cooking?"
>>> p.save()

>>> new_record, old_record = p.history.all()
>>> delta = new_record.diff_against(old_record)

>>> for change in delta.changes():
>>>     print("{} changed from {} to {}".format(change.field, change.old, change.new))

This is straight forward. But in a real world scenario, we will have more than two records. This is where we will start seeing ValueError exception because there will be more than two values to unpack -

>>> new_record, old_record = p.history.all()

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack

In this article, we will go beyond to check the changes for a record that has been updated more than twice. One way to do it is by efficiently pairing the records like below -

from itertools import tee


def pair_iterable_for_delta_changes(iterable):
    a, b = tee(iterable)
    next(b, None)
    return zip(a,b)


def check_delta_for_poll(poll):
    poll_iterator = poll.history.all().order_by('history_date').iterator()
    
    for record_pair in pair_iterable_for_delta_changes(poll_iterator):
        old_record, new_record = record_pair
        delta = new_record.diff_against(old_record)
        
        for change in delta.changes():
            print("{} changed from {} to {}".format(change.field, change.old, change.new))

Here we have used itertools.tee function to pair the records. The pairing will work like this -

s = tee(iter([1,2,3,4,...])) = [s(1, 2), s(2, 3), s(3, 4), ...]

We can now use the same diff_against on the record pairs to check the diff between new_record and old_record.

itertools — Functions creating iterators for efficient looping — Python 3.9.4 documentation

Tags