summaryrefslogtreecommitdiff
path: root/django-simple-history
diff options
context:
space:
mode:
Diffstat (limited to 'django-simple-history')
-rw-r--r--django-simple-history/AUTHORS14
-rw-r--r--django-simple-history/CHANGELOG5
-rw-r--r--django-simple-history/LICENSE.txt28
-rw-r--r--django-simple-history/MANIFEST.in5
-rw-r--r--django-simple-history/README77
-rw-r--r--django-simple-history/setup.py41
-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
12 files changed, 637 insertions, 0 deletions
diff --git a/django-simple-history/AUTHORS b/django-simple-history/AUTHORS
new file mode 100644
index 000000000..2865a62cc
--- /dev/null
+++ b/django-simple-history/AUTHORS
@@ -0,0 +1,14 @@
+The current maintainer of django-simple-history is Corey Bertram
+<corey@qr7.com>, who may be found online at <http://www.qr7.com/>.
+
+This code originally comes from Pro Django, published by Apress, Inc.
+in December 2008. The author of the book and primary author
+of the code is Marty Alchin <marty@martyalchin.com>, who
+may be found online at <http://martyalchin.com/>.
+
+As part of the technical review process, additional code
+modifications were provided by the technical reviewer,
+George Vilches <gav@thataddress.com>.
+
+Others who have contributed to the application:
+* Your name here!
diff --git a/django-simple-history/CHANGELOG b/django-simple-history/CHANGELOG
new file mode 100644
index 000000000..420d95b09
--- /dev/null
+++ b/django-simple-history/CHANGELOG
@@ -0,0 +1,5 @@
+Oct 22, 2010:
+ - Merged setup.py from Klaas van Schelven - Thanks!
+
+Feb 21, 2010:
+ - Initial project creation, with changes to support ForeignKey relations.
diff --git a/django-simple-history/LICENSE.txt b/django-simple-history/LICENSE.txt
new file mode 100644
index 000000000..1d62e68c7
--- /dev/null
+++ b/django-simple-history/LICENSE.txt
@@ -0,0 +1,28 @@
+Copyright (c) 2008, Marty Alchin
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of the author nor the names of other
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file
diff --git a/django-simple-history/MANIFEST.in b/django-simple-history/MANIFEST.in
new file mode 100644
index 000000000..f1b15f396
--- /dev/null
+++ b/django-simple-history/MANIFEST.in
@@ -0,0 +1,5 @@
+include CHANGELOG
+include MANIFEST.in
+include README
+include AUTHORS
+include LICENSE.txt
diff --git a/django-simple-history/README b/django-simple-history/README
new file mode 100644
index 000000000..3a12cfbf2
--- /dev/null
+++ b/django-simple-history/README
@@ -0,0 +1,77 @@
+django-simple-history is a tool to store state of DB objects on every create/update/delete. It has been tested to work in django 1.X (including 1.2.3 as of 10/25/2010).
+
+== Install ==
+Download the tar.gz, extract it and run the following inside the directory:
+ python setup.py install
+
+== Basic usage ==
+Using this package is _really_ simple; you just have to import HistoricalRecords and create an instance of it on every model you want to historically track.
+
+On your models you need to include the following line at the top:
+ from simple_history.models import HistoricalRecords
+
+Then in your model class, include the following line:
+ history = HistoricalRecords()
+
+Then from either the model class or from an instance, you can access history.all() which will give you either every history item of the class, or every history item of the specific instance.
+
+== Example ==
+class Poll(models.Model):
+ question = models.CharField(max_length = 200)
+ pub_date = models.DateTimeField('date published')
+
+ history = HistoricalRecords()
+
+class Choice(models.Model):
+ poll = models.ForeignKey(Poll)
+ choice = models.CharField(max_length=200)
+ votes = models.IntegerField()
+
+ history = HistoricalRecords()
+
+
+$ ./manage.py shell
+In [2]: from poll.models import Poll, Choice
+
+In [3]: Poll.objects.all()
+Out[3]: []
+
+In [4]: import datetime
+
+In [5]: p = Poll(question="what's up?", pub_date=datetime.datetime.now())
+
+In [6]: p.save()
+
+In [7]: p
+Out[7]: <Poll: Poll object>
+
+In [9]: p.history.all()
+Out[9]: [<HistoricalPoll: Poll object as of 2010-10-25 18:03:29.855689>]
+
+In [10]: p.pub_date = datetime.datetime(2007,4,1,0,0)
+
+In [11]: p.save()
+
+In [13]: p.history.all()
+Out[13]: [<HistoricalPoll: Poll object as of 2010-10-25 18:04:13.814128>, <HistoricalPoll: Poll object as of 2010-10-25 18:03:29.855689>]
+
+In [14]: p.choice_set.create(choice='Not Much', votes=0)
+Out[14]: <Choice: Choice object>
+
+In [15]: p.choice_set.create(choice='The sky', votes=0)
+Out[15]: <Choice: Choice object>
+
+In [16]: c = p.choice_set.create(choice='Just hacking again', votes=0)
+
+In [17]: c.poll
+Out[17]: <Poll: Poll object>
+
+In [19]: c.history.all()
+Out[19]: [<HistoricalChoice: Choice object as of 2010-10-25 18:05:30.160595>]
+
+In [20]: Choice.history
+Out[20]: <simple_history.manager.HistoryManager object at 0x1cc4290>
+
+In [21]: Choice.history.all()
+Out[21]: [<HistoricalChoice: Choice object as of 2010-10-25 18:05:30.160595>, <HistoricalChoice: Choice object as of 2010-10-25 18:05:12.183340>, <HistoricalChoice: Choice object as of 2010-10-25 18:04:59.047351>]
+
diff --git a/django-simple-history/setup.py b/django-simple-history/setup.py
new file mode 100644
index 000000000..712d7d2d4
--- /dev/null
+++ b/django-simple-history/setup.py
@@ -0,0 +1,41 @@
+from distutils.core import setup
+import os
+
+# compile the list of packages available, because distutils doesn't have an easy way to do this
+packages, data_files = [], []
+root_dir = os.path.dirname(__file__)
+if root_dir:
+ os.chdir(root_dir)
+
+for dirpath, dirnames, filenames in os.walk('simple_history'):
+ # ignore dirnames that start with '.'
+ for i, dirname in enumerate(dirnames):
+ if dirname.startswith('.'):
+ del dirnames[i]
+ if '__init__.py' in filenames:
+ pkg = dirpath.replace(os.path.sep, '.')
+ if os.path.altsep:
+ pkg = pkg.replace(os.path.altsep, '.')
+ packages.append(pkg)
+ elif filenames:
+ # strip 'simple_history/' or 'simple_history\'
+ prefix = dirpath[15:]
+ for f in filenames:
+ data_files.append(os.path.join(prefix, f))
+
+setup(name='simple_history',
+ version='1.1.3',
+ description='Store Django model history with the ability to revert back to a specific change at any time.',
+ author='Corey Bertram',
+ author_email='corey@qr7.com',
+ mantainer='Joao Pedro Francese',
+ mantainer_email='joaofrancese@gmail.com',
+ url='http://bitbucket.org/joaofrancese/django-simple-history',
+ package_dir={'simple_history': 'simple_history'},
+ packages=packages,
+ package_data={'simple_history': data_files},
+ classifiers=[
+ "Development Status :: 5 - Production/Stable",
+ "Framework :: Django",
+ ],
+ )
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 %}