Skip to content

Commit 7635120

Browse files
committed
Merge pull request #106 from treyhunner/non-instance-as-of
Extend the 'as_of' method to work as a class method
2 parents 7cec7da + dff287b commit 7635120

File tree

4 files changed

+118
-25
lines changed

4 files changed

+118
-25
lines changed

simple_history/manager.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,27 +60,35 @@ def most_recent(self):
6060
return self.instance.__class__(*values)
6161

6262
def as_of(self, date):
63-
"""
64-
Returns an instance of the original model with all the attributes set
65-
according to what was present on the object on the date provided.
63+
"""Get a snapshot as of a specific date.
64+
65+
Returns an instance, or an iterable of the instances, of the
66+
original model with all the attributes set according to what
67+
was present on the object on the date provided.
6668
"""
6769
if not self.instance:
68-
raise TypeError("Can't use as_of() without a %s instance." %
69-
self.model._meta.object_name)
70-
tmp = []
71-
for field in self.instance._meta.fields:
72-
if isinstance(field, models.ForeignKey):
73-
tmp.append(field.name + "_id")
74-
else:
75-
tmp.append(field.name)
76-
fields = tuple(tmp)
77-
qs = self.filter(history_date__lte=date)
70+
return self._as_of_set(date)
71+
queryset = self.filter(history_date__lte=date)
7872
try:
79-
values = qs.values_list('history_type', *fields)[0]
73+
history_obj = queryset[0]
8074
except IndexError:
81-
raise self.instance.DoesNotExist("%s had not yet been created." %
82-
self.instance._meta.object_name)
83-
if values[0] == '-':
84-
raise self.instance.DoesNotExist("%s had already been deleted." %
85-
self.instance._meta.object_name)
86-
return self.instance.__class__(*values[1:])
75+
raise self.instance.DoesNotExist(
76+
"%s had not yet been created." %
77+
self.instance._meta.object_name)
78+
if history_obj.history_type == '-':
79+
raise self.instance.DoesNotExist(
80+
"%s had already been deleted." %
81+
self.instance._meta.object_name)
82+
return history_obj.instance
83+
84+
def _as_of_set(self, date):
85+
model = type(self.model().instance) # a bit of a hack to get the model
86+
pk_attr = model._meta.pk.name
87+
queryset = self.filter(history_date__lte=date)
88+
for original_pk in set(
89+
queryset.order_by().values_list(pk_attr, flat=True)):
90+
changes = queryset.filter(**{pk_attr: original_pk})
91+
last_change = changes.latest('history_date')
92+
if changes.filter(history_date=last_change.history_date, history_type='-').exists():
93+
continue
94+
yield last_change.instance
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
from .test_models import *
22
from .test_admin import *
33
from .test_commands import *
4+
from .test_manager import *
5+
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from datetime import datetime, timedelta
2+
from django.test import TestCase
3+
try:
4+
from django.contrib.auth import get_user_model
5+
except ImportError:
6+
from django.contrib.auth.models import User
7+
else:
8+
User = get_user_model()
9+
10+
from .. import models
11+
12+
13+
class AsOfTest(TestCase):
14+
model = models.Document
15+
16+
def setUp(self):
17+
user = User.objects.create_user("tester", "[email protected]")
18+
self.now = datetime.now()
19+
self.yesterday = self.now - timedelta(days=1)
20+
self.obj = self.model.objects.create()
21+
self.obj.changed_by = user
22+
self.obj.save()
23+
self.model.objects.all().delete() # allows us to leave PK on instance
24+
self.delete_history, self.change_history, self.create_history = (
25+
self.model.history.all())
26+
self.create_history.history_date = self.now - timedelta(days=2)
27+
self.create_history.save()
28+
self.change_history.history_date = self.now - timedelta(days=1)
29+
self.change_history.save()
30+
self.delete_history.history_date = self.now
31+
self.delete_history.save()
32+
33+
def test_created_after(self):
34+
"""An object created after the 'as of' date should not be
35+
included.
36+
37+
"""
38+
as_of_list = list(
39+
self.model.history.as_of(self.now - timedelta(days=5)))
40+
self.assertFalse(as_of_list)
41+
42+
def test_deleted_before(self):
43+
"""An object deleted before the 'as of' date should not be
44+
included.
45+
46+
"""
47+
as_of_list = list(
48+
self.model.history.as_of(self.now + timedelta(days=1)))
49+
self.assertFalse(as_of_list)
50+
51+
def test_deleted_after(self):
52+
"""An object created before, but deleted after the 'as of'
53+
date should be included.
54+
55+
"""
56+
as_of_list = list(
57+
self.model.history.as_of(self.now - timedelta(days=1)))
58+
self.assertEqual(len(as_of_list), 1)
59+
self.assertEqual(as_of_list[0].pk, self.obj.pk)
60+
61+
def test_modified(self):
62+
"""An object modified before the 'as of' date should reflect
63+
the last version.
64+
65+
"""
66+
as_of_list = list(
67+
self.model.history.as_of(self.now - timedelta(days=1)))
68+
self.assertEqual(as_of_list[0].changed_by, self.obj.changed_by)
69+
70+
71+
class AsOfAdditionalTestCase(TestCase):
72+
73+
def test_create_and_delete(self):
74+
now = datetime.now()
75+
document = models.Document.objects.create()
76+
document.delete()
77+
for doc_change in models.Document.history.all():
78+
doc_change.history_date = now
79+
doc_change.save()
80+
docs_as_of_tmw = models.Document.history.as_of(now + timedelta(days=1))
81+
self.assertFalse(list(docs_as_of_tmw))
82+
83+
def test_multiple(self):
84+
document1 = models.Document.objects.create()
85+
document2 = models.Document.objects.create()
86+
historical = models.Document.history.as_of(datetime.now()
87+
+ timedelta(days=1))
88+
self.assertEqual(list(historical), [document1, document2])

simple_history/tests/tests/test_models.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -425,11 +425,6 @@ def test_as_of(self):
425425
self.assertEqual(question_as_of(times[1]), "how's it going?")
426426
self.assertEqual(question_as_of(times[2]), "what's up?")
427427

428-
def test_as_of_on_model_class(self):
429-
Poll.objects.create(question="what's up?", pub_date=today)
430-
time = Poll.history.all()[0].history_date
431-
self.assertRaises(TypeError, Poll.history.as_of, time)
432-
433428
def test_as_of_nonexistant(self):
434429
# Unsaved poll
435430
poll = Poll(question="what's up?", pub_date=today)

0 commit comments

Comments
 (0)