Repository: jazzband/django-simple-history

Issue:
Query previous/next historical entry
One of the main use cases I've found with django-simple-history has been to get the previous historical entry to calculate deltas in values.

Issue #201 talks about the Django utility functions, which allows for getting the last value by a field. I think it would be great to add a `previous` property to the historical model that grabbed the `get_previous_by_history_date` that reflected the same tracked instance automatically. I feel like a `next` and `previous` method fits perfectly within the historical records context.

As a dependent though, comparing versions between points in time would be excellent, too, but I'll submit a second issue based on response to this.



Discussion:
What _is_ the best way, given an object, to get the "previous" fields of that object from the historical record?

In my use case, `thing` and `thing.history.most_recent()` have the same field values in all cases, except they don't compare identically because `thing._state != thing.history.most_recent()._state`, and `id(thing) != id(thing.history.most_recent())`.
@ransford I know this is old, but maybe still useful (or useful to others)

I wrote a function to do this:

```
def get_prev_hist_entry(entry_hist):
    ''' Given a HistoryRecord, return previous record for same instance '''
    entry = entry_hist.instance
    history = entry.history.order_by('history_date')
    hist_list = [hist for hist in history]
    try:
        entry_hist_idx = hist_list.index(entry_hist)
        except ValueError:
            return None
    return hist_list[entry_hist_idx - 1] if entry_hist_idx > 0 else None
```
Thank you @SpainTrain!
Thanks @SpainTrain . Feel free to add a PR, documentation, and tests for this and I'd be happy to merge. 
:white_check_mark: https://github.com/treyhunner/django-simple-history/pull/365

Resolving patch:
diff --git a/simple_history/models.py b/simple_history/models.py
--- a/simple_history/models.py
+++ b/simple_history/models.py
@@ -9,6 +9,7 @@
 from django.conf import settings
 from django.contrib import admin
 from django.db import models, router
+from django.db.models import Q
 from django.db.models.fields.proxy import OrderWrt
 from django.urls import reverse
 from django.utils import six
@@ -222,6 +223,22 @@ def get_instance(self):
                 for field in fields.values()
             })
 
+        def get_next_record(self):
+            """
+            Get the next history record for the instance. `None` if last.
+            """
+            return self.instance.history.filter(
+                Q(history_date__gt=self.history_date)
+            ).order_by('history_date').first()
+
+        def get_prev_record(self):
+            """
+            Get the previous history record for the instance. `None` if first.
+            """
+            return self.instance.history.filter(
+                Q(history_date__lt=self.history_date)
+            ).order_by('history_date').last()
+
         if self.history_id_field:
             history_id_field = self.history_id_field
             history_id_field.primary_key = True
@@ -252,6 +269,8 @@ def get_instance(self):
             ),
             'instance': property(get_instance),
             'instance_type': model,
+            'next_record': property(get_next_record),
+            'prev_record': property(get_prev_record),
             'revert_url': revert_url,
             '__str__': lambda self: '%s as of %s' % (self.history_object,
                                                      self.history_date)


Test patch:
diff --git a/simple_history/tests/tests/test_models.py b/simple_history/tests/tests/test_models.py
--- a/simple_history/tests/tests/test_models.py
+++ b/simple_history/tests/tests/test_models.py
@@ -382,6 +382,80 @@ def test_uuid_default_history_id(self):
         history = entry.history.all()[0]
         self.assertTrue(isinstance(history.history_id, uuid.UUID))
 
+    def test_get_prev_record(self):
+        poll = Poll(question="what's up?", pub_date=today)
+        poll.save()
+        poll.question = "ask questions?"
+        poll.save()
+        poll.question = "eh?"
+        poll.save()
+        poll.question = "one more?"
+        poll.save()
+        first_record = poll.history.filter(question="what's up?").get()
+        second_record = poll.history.filter(question="ask questions?").get()
+        third_record = poll.history.filter(question="eh?").get()
+        fourth_record = poll.history.filter(question="one more?").get()
+        self.assertIsNone(first_record.prev_record)
+
+        def assertRecordsMatch(record_a, record_b):
+            self.assertEqual(record_a, record_b)
+            self.assertEqual(record_a.question, record_b.question)
+        assertRecordsMatch(second_record.prev_record, first_record)
+        assertRecordsMatch(third_record.prev_record, second_record)
+        assertRecordsMatch(fourth_record.prev_record, third_record)
+
+    def test_get_prev_record_none_if_only(self):
+        poll = Poll(question="what's up?", pub_date=today)
+        poll.save()
+        self.assertEqual(poll.history.count(), 1)
+        record = poll.history.get()
+        self.assertIsNone(record.prev_record)
+
+    def test_get_prev_record_none_if_earliest(self):
+        poll = Poll(question="what's up?", pub_date=today)
+        poll.save()
+        poll.question = "ask questions?"
+        poll.save()
+        first_record = poll.history.filter(question="what's up?").get()
+        self.assertIsNone(first_record.prev_record)
+
+    def test_get_next_record(self):
+        poll = Poll(question="what's up?", pub_date=today)
+        poll.save()
+        poll.question = "ask questions?"
+        poll.save()
+        poll.question = "eh?"
+        poll.save()
+        poll.question = "one more?"
+        poll.save()
+        first_record = poll.history.filter(question="what's up?").get()
+        second_record = poll.history.filter(question="ask questions?").get()
+        third_record = poll.history.filter(question="eh?").get()
+        fourth_record = poll.history.filter(question="one more?").get()
+        self.assertIsNone(fourth_record.next_record)
+
+        def assertRecordsMatch(record_a, record_b):
+            self.assertEqual(record_a, record_b)
+            self.assertEqual(record_a.question, record_b.question)
+        assertRecordsMatch(first_record.next_record, second_record)
+        assertRecordsMatch(second_record.next_record, third_record)
+        assertRecordsMatch(third_record.next_record, fourth_record)
+
+    def test_get_next_record_none_if_only(self):
+        poll = Poll(question="what's up?", pub_date=today)
+        poll.save()
+        self.assertEqual(poll.history.count(), 1)
+        record = poll.history.get()
+        self.assertIsNone(record.next_record)
+
+    def test_get_next_record_none_if_most_recent(self):
+        poll = Poll(question="what's up?", pub_date=today)
+        poll.save()
+        poll.question = "ask questions?"
+        poll.save()
+        recent_record = poll.history.filter(question="ask questions?").get()
+        self.assertIsNone(recent_record.next_record)
+
 
 class CreateHistoryModelTests(unittest.TestCase):