Skip to content

Commit 05b329a

Browse files
committed
Imported Marty Alchin's initial implementation from the Pro Django book.
0 parents  commit 05b329a

File tree

4 files changed

+198
-0
lines changed

4 files changed

+198
-0
lines changed

LICENSE.txt

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Copyright (c) 2008, Marty Alchin
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions are
6+
met:
7+
8+
* Redistributions of source code must retain the above copyright
9+
notice, this list of conditions and the following disclaimer.
10+
* Redistributions in binary form must reproduce the above
11+
copyright notice, this list of conditions and the following
12+
disclaimer in the documentation and/or other materials provided
13+
with the distribution.
14+
* Neither the name of the author nor the names of other
15+
contributors may be used to endorse or promote products derived
16+
from this software without specific prior written permission.
17+
18+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

simple_history/__init__.py

Whitespace-only changes.

simple_history/manager.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from django.db import models
2+
3+
class HistoryDescriptor(object):
4+
def __init__(self, model):
5+
self.model = model
6+
7+
def __get__(self, instance, owner):
8+
if instance is None:
9+
return HistoryManager(self.model)
10+
return HistoryManager(self.model, instance)
11+
12+
class HistoryManager(models.Manager):
13+
def __init__(self, model, instance=None):
14+
super(HistoryManager, self).__init__()
15+
self.model = model
16+
self.instance = instance
17+
18+
def get_query_set(self):
19+
if self.instance is None:
20+
return super(HistoryManager, self).get_query_set()
21+
22+
filter = {self.instance._meta.pk.name: self.instance.pk}
23+
return super(HistoryManager, self).get_query_set().filter(**filter)
24+
25+
def most_recent(self):
26+
"""
27+
Returns the most recent copy of the instance available in the history.
28+
"""
29+
if not self.instance:
30+
raise TypeError("Can't use most_recent() without a %s instance." % \
31+
self.instance._meta.object_name)
32+
fields = (field.name for field in self.instance._meta.fields)
33+
try:
34+
values = self.values_list(*fields)[0]
35+
except IndexError:
36+
raise self.instance.DoesNotExist("%s has no historical record." % \
37+
self.instance._meta.object_name)
38+
return self.instance.__class__(*values)
39+
40+
def as_of(self, date):
41+
"""
42+
Returns an instance of the original model with all the attributes set
43+
according to what was present on the object on the date provided.
44+
"""
45+
if not self.instance:
46+
raise TypeError("Can't use as_of() without a %s instance." % \
47+
self.instance._meta.object_name)
48+
fields = (field.name for field in self.instance._meta.fields)
49+
qs = self.filter(history_date__lte=date)
50+
try:
51+
values = qs.values_list('history_type', *fields)[0]
52+
except IndexError:
53+
raise self.instance.DoesNotExist("%s had not yet been created." % \
54+
self.instance._meta.object_name)
55+
if values[0] == '-':
56+
raise self.instance.DoesNotExist("%s had already been deleted." % \
57+
self.instance._meta.object_name)
58+
return self.instance.__class__(*values[1:])

simple_history/models.py

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import copy
2+
import datetime
3+
4+
from django.db import models
5+
6+
from chapter11.current_user import models as current_user
7+
from chapter11.history import manager
8+
9+
class HistoricalRecords(object):
10+
def contribute_to_class(self, cls, name):
11+
self.manager_name = name
12+
models.signals.class_prepared.connect(self.finalize, sender=cls)
13+
14+
def finalize(self, sender, **kwargs):
15+
history_model = self.create_history_model(sender)
16+
17+
# The HistoricalRecords object will be discarded,
18+
# so the signal handlers can't use weak references.
19+
models.signals.post_save.connect(self.post_save, sender=sender,
20+
weak=False)
21+
models.signals.post_delete.connect(self.post_delete, sender=sender,
22+
weak=False)
23+
24+
descriptor = manager.HistoryDescriptor(history_model)
25+
setattr(sender, self.manager_name, descriptor)
26+
27+
def create_history_model(self, model):
28+
"""
29+
Creates a historical model to associate with the model provided.
30+
"""
31+
attrs = self.copy_fields(model)
32+
attrs.update(self.get_extra_fields(model))
33+
attrs.update(Meta=type('Meta', (), self.get_meta_options(model)))
34+
name = 'Historical%s' % model._meta.object_name
35+
return type(name, (models.Model,), attrs)
36+
37+
def copy_fields(self, model):
38+
"""
39+
Creates copies of the model's original fields, returning
40+
a dictionary mapping field name to copied field object.
41+
"""
42+
# Though not strictly a field, this attribute
43+
# is required for a model to function properly.
44+
fields = {'__module__': model.__module__}
45+
46+
for field in model._meta.fields:
47+
field = copy.copy(field)
48+
49+
if isinstance(field, models.AutoField):
50+
# The historical model gets its own AutoField, so any
51+
# existing one must be replaced with an IntegerField.
52+
field.__class__ = models.IntegerField
53+
54+
if field.primary_key or field.unique:
55+
# Unique fields can no longer be guaranteed unique,
56+
# but they should still be indexed for faster lookups.
57+
field.primary_key = False
58+
field._unique = False
59+
field.db_index = True
60+
fields[field.name] = field
61+
62+
return fields
63+
64+
def get_extra_fields(self, model):
65+
"""
66+
Returns a dictionary of fields that will be added to the historical
67+
record model, in addition to the ones returned by copy_fields below.
68+
"""
69+
rel_nm = '_%s_history' % model._meta.object_name.lower()
70+
return {
71+
'history_id': models.AutoField(primary_key=True),
72+
'history_date': models.DateTimeField(default=datetime.datetime.now),
73+
'history_user': current_user.CurrentUserField(related_name=rel_nm),
74+
'history_type': models.CharField(max_length=1, choices=(
75+
('+', 'Created'),
76+
('~', 'Changed'),
77+
('-', 'Deleted'),
78+
)),
79+
'history_object': HistoricalObjectDescriptor(model),
80+
'__unicode__': lambda self: u'%s as of %s' % (self.history_object,
81+
self.history_date)
82+
}
83+
84+
def get_meta_options(self, model):
85+
"""
86+
Returns a dictionary of fields that will be added to
87+
the Meta inner class of the historical record model.
88+
"""
89+
return {
90+
'ordering': ('-history_date',),
91+
}
92+
93+
def post_save(self, instance, created, **kwargs):
94+
self.create_historical_record(instance, created and '+' or '~')
95+
96+
def post_delete(self, instance, **kwargs):
97+
self.create_historical_record(instance, '-')
98+
99+
def create_historical_record(self, instance, type):
100+
manager = getattr(instance, self.manager_name)
101+
attrs = {}
102+
for field in instance._meta.fields:
103+
attrs[field.attname] = getattr(instance, field.attname)
104+
manager.create(history_type=type, **attrs)
105+
106+
class HistoricalObjectDescriptor(object):
107+
def __init__(self, model):
108+
self.model = model
109+
110+
def __get__(self, instance, owner):
111+
values = (getattr(instance, f.attname) for f in self.model._meta.fields)
112+
return self.model(*values)

0 commit comments

Comments
 (0)