summaryrefslogtreecommitdiff
path: root/django-simple-history/simple_history
diff options
context:
space:
mode:
authorÉtienne Loks <etienne.loks@iggdrasil.net>2016-01-17 18:09:26 +0100
committerÉtienne Loks <etienne.loks@iggdrasil.net>2016-01-17 18:09:26 +0100
commitf384337ef0d9d2e40d09204f18c4a486e925132c (patch)
tree434a8a1ee961aa3adf79b35fe42ac41f075b50ef /django-simple-history/simple_history
parentad4a7e6015c26fef5bbad4783d638a138e687bdb (diff)
downloadIshtar-f384337ef0d9d2e40d09204f18c4a486e925132c.tar.bz2
Ishtar-f384337ef0d9d2e40d09204f18c4a486e925132c.zip
Include django-simple-history - fix install script
Diffstat (limited to 'django-simple-history/simple_history')
-rwxr-xr-xdjango-simple-history/simple_history/__init__.py22
-rw-r--r--django-simple-history/simple_history/admin.py139
-rwxr-xr-xdjango-simple-history/simple_history/manager.py75
-rw-r--r--django-simple-history/simple_history/models.py169
-rw-r--r--django-simple-history/simple_history/templates/simple_history/object_history.html38
-rw-r--r--django-simple-history/simple_history/templates/simple_history/object_history_form.html24
6 files changed, 467 insertions, 0 deletions
diff --git a/django-simple-history/simple_history/__init__.py b/django-simple-history/simple_history/__init__.py
new file mode 100755
index 000000000..6df0b60b6
--- /dev/null
+++ b/django-simple-history/simple_history/__init__.py
@@ -0,0 +1,22 @@
+import models
+
+
+registered_models = {}
+
+
+def register(model, app=None, manager_name='history'):
+ """
+ Create historical model for `model` and attach history manager to `model`.
+
+ Keyword arguments:
+ app -- App to install historical model into (defaults to model.__module__)
+ manager_name -- class attribute name to use for historical manager
+
+ This method should be used as an alternative to attaching an
+ `HistoricalManager` instance directly to `model`.
+ """
+ if not model in registered_models:
+ records = models.HistoricalRecords()
+ records.manager_name = manager_name
+ records.module = ("%s.models" % app) or model.__module__
+ records.finalize(model)
diff --git a/django-simple-history/simple_history/admin.py b/django-simple-history/simple_history/admin.py
new file mode 100644
index 000000000..7d83e1016
--- /dev/null
+++ b/django-simple-history/simple_history/admin.py
@@ -0,0 +1,139 @@
+from django import template
+from django.core.exceptions import PermissionDenied
+from django.conf.urls.defaults import patterns, url
+from django.contrib import admin
+from django.contrib.admin import helpers
+from django.contrib.contenttypes.models import ContentType
+from django.core.urlresolvers import reverse
+from django.shortcuts import get_object_or_404, render_to_response
+from django.contrib.admin.util import unquote
+from django.utils.text import capfirst
+from django.utils.html import mark_safe
+from django.utils.translation import ugettext as _
+from django.utils.encoding import force_unicode
+
+
+class SimpleHistoryAdmin(admin.ModelAdmin):
+ object_history_template = "simple_history/object_history.html"
+ object_history_form_template = "simple_history/object_history_form.html"
+
+ def get_urls(self):
+ """Returns the additional urls used by the Reversion admin."""
+ urls = super(SimpleHistoryAdmin, self).get_urls()
+ admin_site = self.admin_site
+ opts = self.model._meta
+ info = opts.app_label, opts.module_name,
+ history_urls = patterns("",
+ url("^([^/]+)/history/([^/]+)/$",
+ admin_site.admin_view(self.history_form_view),
+ name='%s_%s_simple_history' % info),)
+ return history_urls + urls
+
+ def history_view(self, request, object_id, extra_context=None):
+ "The 'history' admin view for this model."
+ model = self.model
+ opts = model._meta
+ app_label = opts.app_label
+ pk_name = opts.pk.attname
+ history = getattr(model, model._meta.simple_history_manager_attribute)
+ action_list = history.filter(**{pk_name: object_id})
+ # If no history was found, see whether this object even exists.
+ obj = get_object_or_404(model, pk=unquote(object_id))
+ context = {
+ 'title': _('Change history: %s') % force_unicode(obj),
+ 'action_list': action_list,
+ 'module_name': capfirst(force_unicode(opts.verbose_name_plural)),
+ 'object': obj,
+ 'root_path': getattr(self.admin_site, 'root_path', None),
+ 'app_label': app_label,
+ 'opts': opts
+ }
+ context.update(extra_context or {})
+ context_instance = template.RequestContext(request, current_app=self.admin_site.name)
+ return render_to_response(self.object_history_template, context, context_instance=context_instance)
+
+ def history_form_view(self, request, object_id, version_id):
+ original_model = self.model
+ original_opts = original_model._meta
+ history = getattr(self.model, self.model._meta.simple_history_manager_attribute)
+ model = history.model
+ opts = model._meta
+ pk_name = original_opts.pk.attname
+ record = get_object_or_404(model, **{pk_name: object_id, 'history_id': version_id})
+ obj = record.instance
+ obj._state.adding = False
+
+ if not self.has_change_permission(request, obj):
+ raise PermissionDenied
+
+ if request.method == 'POST' and '_saveas_new' in request.POST:
+ return self.add_view(request, form_url='../add/')
+
+ formsets = []
+ ModelForm = self.get_form(request, obj)
+ if request.method == 'POST':
+ form = ModelForm(request.POST, request.FILES, instance=obj)
+ if form.is_valid():
+ form_validated = True
+ new_object = self.save_form(request, form, change=True)
+ else:
+ form_validated = False
+ new_object = obj
+ prefixes = {}
+
+ if form_validated:
+ self.save_model(request, new_object, form, change=True)
+ form.save_m2m()
+
+ change_message = self.construct_change_message(request, form, formsets)
+ self.log_change(request, new_object, change_message)
+ return self.response_change(request, new_object)
+
+ else:
+ form = ModelForm(instance=obj)
+
+ adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
+ self.prepopulated_fields, self.get_readonly_fields(request, obj),
+ model_admin=self)
+ media = self.media + adminForm.media
+
+ url_triplet = (self.admin_site.name, original_opts.app_label,
+ original_opts.module_name)
+ context = {
+ 'title': _('Revert %s') % force_unicode(obj),
+ 'adminform': adminForm,
+ 'object_id': object_id,
+ 'original': obj,
+ 'is_popup': False,
+ 'media': mark_safe(media),
+ 'errors': helpers.AdminErrorList(form, formsets),
+ 'app_label': opts.app_label,
+ 'original_opts': original_opts,
+ 'changelist_url': reverse('%s:%s_%s_changelist' % url_triplet),
+ 'change_url': reverse('%s:%s_%s_change' % url_triplet, args=(obj.pk,)),
+ 'history_url': reverse('%s:%s_%s_history' % url_triplet, args=(obj.pk,)),
+ # Context variables copied from render_change_form
+ 'add': False,
+ 'change': True,
+ 'has_add_permission': self.has_add_permission(request),
+ 'has_change_permission': self.has_change_permission(request, obj),
+ 'has_delete_permission': self.has_delete_permission(request, obj),
+ 'has_file_field': True,
+ 'has_absolute_url': False,
+ 'ordered_objects': opts.get_ordered_objects(),
+ 'form_url': '',
+ 'opts': opts,
+ 'content_type_id': ContentType.objects.get_for_model(self.model).id,
+ 'save_as': self.save_as,
+ 'save_on_top': self.save_on_top,
+ 'root_path': getattr(self.admin_site, 'root_path', None),
+ }
+ context_instance = template.RequestContext(request, current_app=self.admin_site.name)
+ return render_to_response(self.object_history_form_template, context, context_instance)
+
+ def save_model(self, request, obj, form, change):
+ """
+ Add the admin user to a special model attribute for reference after save
+ """
+ obj._history_user = request.user
+ super(SimpleHistoryAdmin, self).save_model(request, obj, form, change)
diff --git a/django-simple-history/simple_history/manager.py b/django-simple-history/simple_history/manager.py
new file mode 100755
index 000000000..923b310eb
--- /dev/null
+++ b/django-simple-history/simple_history/manager.py
@@ -0,0 +1,75 @@
+from django.db import models
+
+
+class HistoryDescriptor(object):
+ def __init__(self, model):
+ self.model = model
+
+ def __get__(self, instance, owner):
+ if instance is None:
+ return HistoryManager(self.model)
+ return HistoryManager(self.model, instance)
+
+
+class HistoryManager(models.Manager):
+ def __init__(self, model, instance=None):
+ super(HistoryManager, self).__init__()
+ self.model = model
+ self.instance = instance
+
+ def get_query_set(self):
+ if self.instance is None:
+ return super(HistoryManager, self).get_query_set()
+
+ if isinstance(self.instance._meta.pk, models.OneToOneField):
+ filter = {self.instance._meta.pk.name + "_id": self.instance.pk}
+ else:
+ filter = {self.instance._meta.pk.name: self.instance.pk}
+ return super(HistoryManager, self).get_query_set().filter(**filter)
+
+ def most_recent(self):
+ """
+ Returns the most recent copy of the instance available in the history.
+ """
+ if not self.instance:
+ raise TypeError("Can't use most_recent() without a %s instance." %
+ self.instance._meta.object_name)
+ tmp = []
+ for field in self.instance._meta.fields:
+ if isinstance(field, models.ForeignKey):
+ tmp.append(field.name + "_id")
+ else:
+ tmp.append(field.name)
+ fields = tuple(tmp)
+ try:
+ values = self.values_list(*fields)[0]
+ except IndexError:
+ raise self.instance.DoesNotExist("%s has no historical record." %
+ self.instance._meta.object_name)
+ return self.instance.__class__(*values)
+
+ def as_of(self, date):
+ """
+ Returns an instance of the original model with all the attributes set
+ according to what was present on the object on the date provided.
+ """
+ if not self.instance:
+ raise TypeError("Can't use as_of() without a %s instance." %
+ self.instance._meta.object_name)
+ tmp = []
+ for field in self.instance._meta.fields:
+ if isinstance(field, models.ForeignKey):
+ tmp.append(field.name + "_id")
+ else:
+ tmp.append(field.name)
+ fields = tuple(tmp)
+ qs = self.filter(history_date__lte=date)
+ try:
+ values = qs.values_list('history_type', *fields)[0]
+ except IndexError:
+ raise self.instance.DoesNotExist("%s had not yet been created." %
+ self.instance._meta.object_name)
+ if values[0] == '-':
+ raise self.instance.DoesNotExist("%s had already been deleted." %
+ self.instance._meta.object_name)
+ return self.instance.__class__(*values[1:])
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)
diff --git a/django-simple-history/simple_history/templates/simple_history/object_history.html b/django-simple-history/simple_history/templates/simple_history/object_history.html
new file mode 100644
index 000000000..d14338232
--- /dev/null
+++ b/django-simple-history/simple_history/templates/simple_history/object_history.html
@@ -0,0 +1,38 @@
+{% extends "admin/object_history.html" %}
+{% load i18n %}
+
+
+{% block content %}
+ <div id="content-main">
+
+ <p>{% blocktrans %}Choose a date from the list below to revert to a previous version of this object.{% endblocktrans %}</p>
+
+ <div class="module">
+ {% if action_list %}
+ <table id="change-history">
+ <thead>
+ <tr>
+ <th scope="col">{% trans 'Object' %}</th>
+ <th scope="col">{% trans 'Date/time' %}</th>
+ <th scope="col">{% trans 'Comment' %}</th>
+ <th scope="col">{% trans 'Changed by' %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for action in action_list %}
+ <tr>
+ <td><a href="{{ action.revert_url }}">{{ action.history_object }}</a></td>
+ <td>{{ action.history_date }}</td>
+ <td>{{ action.get_history_type_display }}</td>
+ <td><a href="{% url admin:auth_user_change action.history_user_id %}">{{ action.history_user }}</a></td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% else %}
+ <p>{% trans "This object doesn't have a change history." %}</p>
+ {% endif %}
+ </div>
+ </div>
+{% endblock %}
+
diff --git a/django-simple-history/simple_history/templates/simple_history/object_history_form.html b/django-simple-history/simple_history/templates/simple_history/object_history_form.html
new file mode 100644
index 000000000..fdc8f1a87
--- /dev/null
+++ b/django-simple-history/simple_history/templates/simple_history/object_history_form.html
@@ -0,0 +1,24 @@
+{% extends "admin/change_form.html" %}
+{% load i18n %}
+
+{% block breadcrumbs %}
+ <div class="breadcrumbs">
+ <a href="{% url admin:index %}">{% trans "Home" %}</a> &rsaquo;
+ <a href="{% url admin:app_list app_label %}">{{app_label|capfirst|escape}}</a> &rsaquo;
+ <a href="{{changelist_url}}">{{opts.verbose_name_plural|capfirst}}</a> &rsaquo;
+ <a href="{{change_url}}">{{original|truncatewords:"18"}}</a> &rsaquo;
+ <a href="../">{% trans "History" %}</a> &rsaquo;
+ {% blocktrans with original_opts.verbose_name as verbose_name %}Revert {{verbose_name}}{% endblocktrans %}
+ </div>
+{% endblock %}
+
+{% comment %}Hack to remove "Save as New" and "Save and Continue" buttons {% endcomment %}
+{% block content %}
+ {% with 1 as is_popup %}
+ {{block.super}}
+ {% endwith %}
+{% endblock %}
+
+{% block form_top %}
+ <p>{% blocktrans %}Press the save button below to revert to this version of the object.{% endblocktrans %}</p>
+{% endblock %}