summaryrefslogtreecommitdiff
path: root/django-simple-history/simple_history/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'django-simple-history/simple_history/models.py')
-rw-r--r--django-simple-history/simple_history/models.py169
1 files changed, 169 insertions, 0 deletions
diff --git a/django-simple-history/simple_history/models.py b/django-simple-history/simple_history/models.py
new file mode 100644
index 000000000..06054ba34
--- /dev/null
+++ b/django-simple-history/simple_history/models.py
@@ -0,0 +1,169 @@
+import copy
+from django.db import models
+from django.contrib import admin
+from django.contrib.auth.models import User
+from django.utils import importlib
+from manager import HistoryDescriptor
+
+
+class HistoricalRecords(object):
+ def contribute_to_class(self, cls, name):
+ self.manager_name = name
+ self.module = cls.__module__
+ models.signals.class_prepared.connect(self.finalize, sender=cls)
+
+ def save_without_historical_record(self, *args, **kwargs):
+ """Caution! Make sure you know what you're doing before you use this method."""
+ self.skip_history_when_saving = True
+ ret = self.save(*args, **kwargs)
+ del self.skip_history_when_saving
+ return ret
+ setattr(cls, 'save_without_historical_record', save_without_historical_record)
+
+ def finalize(self, sender, **kwargs):
+ history_model = self.create_history_model(sender)
+ module = importlib.import_module(self.module)
+ setattr(module, history_model.__name__, history_model)
+
+ # The HistoricalRecords object will be discarded,
+ # so the signal handlers can't use weak references.
+ models.signals.post_save.connect(self.post_save, sender=sender,
+ weak=False)
+ models.signals.post_delete.connect(self.post_delete, sender=sender,
+ weak=False)
+
+ descriptor = HistoryDescriptor(history_model)
+ setattr(sender, self.manager_name, descriptor)
+ sender._meta.simple_history_manager_attribute = self.manager_name
+
+ def create_history_model(self, model):
+ """
+ Creates a historical model to associate with the model provided.
+ """
+ attrs = {'__module__': self.module}
+
+ fields = self.copy_fields(model)
+ attrs.update(fields)
+ attrs.update(self.get_extra_fields(model, fields))
+ attrs.update(Meta=type('Meta', (), self.get_meta_options(model)))
+ name = 'Historical%s' % model._meta.object_name
+ return type(name, (models.Model,), attrs)
+
+ def copy_fields(self, model):
+ """
+ Creates copies of the model's original fields, returning
+ a dictionary mapping field name to copied field object.
+ """
+ fields = {}
+
+ for field in model._meta.fields:
+ field = copy.copy(field)
+ fk = None
+
+ if isinstance(field, models.AutoField):
+ # The historical model gets its own AutoField, so any
+ # existing one must be replaced with an IntegerField.
+ field.__class__ = models.IntegerField
+
+ if isinstance(field, models.ForeignKey):
+ field.__class__ = models.IntegerField
+ #ughhhh. open to suggestions here
+ try:
+ field.rel = None
+ except:
+ pass
+ try:
+ field.related = None
+ except:
+ pass
+ try:
+ field.related_query_name = None
+ except:
+ pass
+ field.null = True
+ field.blank = True
+ fk = True
+ else:
+ fk = False
+
+ # The historical instance should not change creation/modification timestamps.
+ field.auto_now = False
+ field.auto_now_add = False
+
+ if field.primary_key or field.unique:
+ # Unique fields can no longer be guaranteed unique,
+ # but they should still be indexed for faster lookups.
+ field.primary_key = False
+ field._unique = False
+ field.db_index = True
+ field.serialize = True
+ if fk:
+ field.name = field.name + "_id"
+ fields[field.name] = field
+
+ return fields
+
+ def get_extra_fields(self, model, fields):
+ """
+ Returns a dictionary of fields that will be added to the historical
+ record model, in addition to the ones returned by copy_fields below.
+ """
+ @models.permalink
+ def revert_url(self):
+ opts = model._meta
+ return ('%s:%s_%s_simple_history' %
+ (admin.site.name, opts.app_label, opts.module_name),
+ [getattr(self, opts.pk.attname), self.history_id])
+ def get_instance(self):
+ return model(**dict([(k, getattr(self, k)) for k in fields]))
+
+ return {
+ 'history_id': models.AutoField(primary_key=True),
+ 'history_date': models.DateTimeField(auto_now_add=True),
+ 'history_user': models.ForeignKey(User, null=True),
+ 'history_type': models.CharField(max_length=1, choices=(
+ ('+', 'Created'),
+ ('~', 'Changed'),
+ ('-', 'Deleted'),
+ )),
+ 'history_object': HistoricalObjectDescriptor(model),
+ 'instance': property(get_instance),
+ 'revert_url': revert_url,
+ '__unicode__': lambda self: u'%s as of %s' % (self.history_object,
+ self.history_date)
+ }
+
+ def get_meta_options(self, model):
+ """
+ Returns a dictionary of fields that will be added to
+ the Meta inner class of the historical record model.
+ """
+ return {
+ 'ordering': ('-history_date', '-history_id'),
+ }
+
+ def post_save(self, instance, created, **kwargs):
+ if not created and hasattr(instance, 'skip_history_when_saving'):
+ return
+ if not kwargs.get('raw', False):
+ self.create_historical_record(instance, created and '+' or '~')
+
+ def post_delete(self, instance, **kwargs):
+ self.create_historical_record(instance, '-')
+
+ def create_historical_record(self, instance, type):
+ history_user = getattr(instance, '_history_user', None)
+ manager = getattr(instance, self.manager_name)
+ attrs = {}
+ for field in instance._meta.fields:
+ attrs[field.attname] = getattr(instance, field.attname)
+ manager.create(history_type=type, history_user=history_user, **attrs)
+
+
+class HistoricalObjectDescriptor(object):
+ def __init__(self, model):
+ self.model = model
+
+ def __get__(self, instance, owner):
+ values = (getattr(instance, f.attname) for f in self.model._meta.fields)
+ return self.model(*values)