diff --git a/.travis.yml b/.travis.yml
index 331ae917f..bef985b67 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,12 +5,12 @@ python:
- 2.7
- 3.2
- 3.3
+ - 3.4
env:
- - DJANGO=Django==1.3.7
- - DJANGO=Django==1.4.10
- - DJANGO=Django==1.5.5
- - DJANGO=Django==1.6.2
+ - DJANGO=Django==1.4.13
+ - DJANGO=Django==1.5.8
+ - DJANGO=Django==1.6.5
- DJANGO=https://github.com/django/django/tarball/stable/1.7.x
install:
@@ -21,16 +21,18 @@ script: coverage run -a setup.py test
matrix:
exclude:
- python: 2.6
- env: DJANGO=Django==1.6.2
+ env: DJANGO=Django==1.6.5
- python: 2.6
env: DJANGO=https://github.com/django/django/tarball/stable/1.7.x
- python: 3.2
- env: DJANGO=Django==1.3.7
+ env: DJANGO=Django==1.4.13
- python: 3.3
- env: DJANGO=Django==1.3.7
- - python: 3.2
- env: DJANGO=Django==1.4.10
- - python: 3.3
- env: DJANGO=Django==1.4.10
+ env: DJANGO=Django==1.4.13
+ - python: 3.4
+ env: DJANGO=Django==1.4.13
+ - python: 3.4
+ env: DJANGO=Django==1.5.8
+ - python: 3.4
+ env: DJANGO=Django==1.6.5
after_success: coveralls
diff --git a/setup.py b/setup.py
index 1a9b8ea82..81e99ca03 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,12 @@
author_email='corey@qr7.com',
maintainer='Trey Hunner',
url='https://github.com/treyhunner/django-simple-history',
- packages=["simple_history", "simple_history.management", "simple_history.management.commands"],
+ packages=[
+ "simple_history",
+ "simple_history.templatetags",
+ "simple_history.management",
+ "simple_history.management.commands",
+ ],
classifiers=[
"Development Status :: 5 - Production/Stable",
"Framework :: Django",
diff --git a/simple_history/admin.py b/simple_history/admin.py
index 6d6f67a32..db87deb79 100644
--- a/simple_history/admin.py
+++ b/simple_history/admin.py
@@ -4,20 +4,20 @@
from django.core.exceptions import PermissionDenied
try:
from django.conf.urls import patterns, url
-except ImportError:
+except ImportError: # pragma: no cover
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.shortcuts import get_object_or_404, render
+from django.contrib.admin.util import quote, unquote
from django.utils.text import capfirst
from django.utils.html import mark_safe
from django.utils.translation import ugettext as _
try:
from django.utils.encoding import force_text
-except ImportError: # django 1.3 compatibility
+except ImportError: # pragma: no cover, django 1.3 compatibility
from django.utils.encoding import force_unicode as force_text
from django.conf import settings
@@ -31,6 +31,7 @@
class SimpleHistoryAdmin(admin.ModelAdmin):
object_history_template = "simple_history/object_history.html"
object_history_form_template = "simple_history/object_history_form.html"
+ object_compare_template = "simple_history/history_compare.html"
def get_urls(self):
"""Returns the additional urls used by the Reversion admin."""
@@ -39,13 +40,16 @@ def get_urls(self):
opts = self.model._meta
try:
info = opts.app_label, opts.module_name
- except AttributeError:
+ except AttributeError: # pragma: no cover
info = opts.app_label, opts.model_name
history_urls = patterns(
"",
url("^([^/]+)/history/([^/]+)/$",
admin_site.admin_view(self.history_form_view),
name='%s_%s_simple_history' % info),
+ url("^([^/]+)/compare/$",
+ admin_site.admin_view(self.compare_view),
+ name='%s_%s_simple_compare' % info),
)
return history_urls + urls
@@ -77,8 +81,8 @@ def history_view(self, request, object_id, extra_context=None):
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)
+ return render(request, self.object_history_template,
+ dictionary=context, context_instance=context_instance)
def history_form_view(self, request, object_id, version_id):
original_model = self.model
@@ -132,7 +136,7 @@ def history_form_view(self, request, object_id, version_id):
try:
model_name = original_opts.module_name
- except AttributeError:
+ except AttributeError: # pragma: no cover
model_name = original_opts.model_name
url_triplet = self.admin_site.name, original_opts.app_label, model_name
content_type_id = ContentType.objects.get_for_model(self.model).id
@@ -170,8 +174,43 @@ def history_form_view(self, request, object_id, version_id):
request,
current_app=self.admin_site.name,
)
- return render_to_response(self.object_history_form_template, context,
- context_instance)
+ return render(request, self.object_history_form_template,
+ dictionary=context, context_instance=context_instance)
+
+ def compare_view(self, request, object_id, extra_context=None):
+ object_id = unquote(object_id)
+ obj = get_object_or_404(self.model, pk=object_id)
+ history = getattr(obj,
+ self.model._meta.simple_history_manager_attribute)
+ prev = history.get(pk=request.GET['from'])
+ curr = history.get(pk=request.GET['to'])
+
+ fields = [{
+ 'name': field.attname,
+ 'content': getattr(curr, field.attname),
+ 'prev_content': getattr(prev, field.attname),
+ 'section_break': "\n",
+ } for field in self.model._meta.fields]
+ opts = self.model._meta
+ d = {
+ 'title': _('Compare %s') % force_text(obj),
+ 'app_label': opts.app_label,
+ 'module_name': capfirst(force_text(opts.verbose_name_plural)),
+ 'object_id': quote(object_id),
+ 'object': obj,
+ 'fields': fields,
+ 'opts': opts,
+ 'add': False,
+ 'change': False,
+ 'show_delete': False,
+ 'is_popup': False,
+ 'save_as': self.save_as,
+ '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),
+ }
+ return render(request, template_name=self.object_compare_template,
+ current_app=self.admin_site.name, dictionary=d)
def save_model(self, request, obj, form, change):
"""Set special model attribute to user for reference after save"""
diff --git a/simple_history/templates/simple_history/history_compare.html b/simple_history/templates/simple_history/history_compare.html
new file mode 100644
index 000000000..51aafa2b7
--- /dev/null
+++ b/simple_history/templates/simple_history/history_compare.html
@@ -0,0 +1,51 @@
+{% extends "admin/change_form.html" %}
+{% load i18n %}
+{% load url from future %}
+{% load admin_urls %}
+{% load simple_history_compare %}
+
+{% block breadcrumbs %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+{% for field in fields %}
+
+{% endfor %}
+
+
+
+{% endblock content %}
diff --git a/simple_history/templates/simple_history/object_history.html b/simple_history/templates/simple_history/object_history.html
index 2069b1dc8..318d1fb6b 100644
--- a/simple_history/templates/simple_history/object_history.html
+++ b/simple_history/templates/simple_history/object_history.html
@@ -10,32 +10,45 @@
{% if action_list %}
-
-
-
- {% trans 'Object' %} |
- {% trans 'Date/time' %} |
- {% trans 'Comment' %} |
- {% trans 'Changed by' %} |
-
-
-
- {% for action in action_list %}
+
+
{% else %}
{% trans "This object doesn't have a change history." %}
{% endif %}
diff --git a/simple_history/templatetags/__init__.py b/simple_history/templatetags/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/simple_history/templatetags/simple_history_compare.py b/simple_history/templatetags/simple_history_compare.py
new file mode 100644
index 000000000..9ff76afee
--- /dev/null
+++ b/simple_history/templatetags/simple_history_compare.py
@@ -0,0 +1,26 @@
+from __future__ import unicode_literals
+
+import difflib
+from django import template
+
+register = template.Library()
+
+
+@register.simple_tag
+def diff_table(a, b, line_split="\n"):
+ differ = difflib.HtmlDiff(wrapcolumn=80)
+ try:
+ return differ.make_table(a.split(line_split), b.split(line_split))
+ except AttributeError:
+ if a != b:
+ a = '
{a}'.format(a=a)
+ b = '
{b}'.format(b=b)
+ return """
+""".format(a=a, b=b)
diff --git a/simple_history/tests/tests/test_admin.py b/simple_history/tests/tests/test_admin.py
index 00c6e15c0..b09ff0370 100644
--- a/simple_history/tests/tests/test_admin.py
+++ b/simple_history/tests/tests/test_admin.py
@@ -1,5 +1,6 @@
from datetime import datetime, timedelta
from django_webtest import WebTest
+from django.test import TestCase
from django import VERSION
from django.core.urlresolvers import reverse
try:
@@ -8,6 +9,7 @@
except ImportError: # django 1.4 compatibility
from django.contrib.auth.models import User
from django.contrib.admin.util import quote
+from simple_history.templatetags import simple_history_compare
from ..models import Book, Person, Poll
@@ -29,7 +31,8 @@ def get_history_url(model, history_index=None):
return reverse('admin:%s_%s_history' % info, args=[quote(model.pk)])
-class AdminSiteTest(WebTest):
+class AdminTest(WebTest):
+
def setUp(self):
self.user = User.objects.create_superuser('user_login',
'u@example.com', 'pass')
@@ -42,6 +45,9 @@ def login(self, user=None):
form['password'] = 'pass'
return form.submit()
+
+class AdminSiteTest(AdminTest):
+
def test_history_list(self):
if VERSION >= (1, 5):
try:
@@ -156,3 +162,40 @@ def test_historical_user_with_setter(self):
self.login()
add_page = self.app.get(reverse('admin:tests_paper_add'))
add_page.form.submit()
+
+ def test_compare_history(self):
+ self.login()
+
+
+class CompareHistoryTest(AdminTest):
+
+ def setUp(self):
+ super(CompareHistoryTest, self).setUp()
+ self.login()
+ self.poll = Poll.objects.create(question="Who?", pub_date=today)
+ for question in ("What?", "Where?", "When?", "Why?", "How?"):
+ self.poll.question = question
+ self.poll.save()
+
+ def test_navigate_to_compare(self):
+ response = self.app.get(get_history_url(self.poll)).form.submit()
+ response.mustcontain("
Compare ")
+
+
+class CompareTableTest(TestCase):
+
+ def test_diff_table(self):
+ table_markup = simple_history_compare.diff_table(a="this\nan\ntest", b="this\nis\na\ntest")
+ self.assertEqual(table_markup, """
+
+
+
+
+
+ f | this | f | this |
+ t | an | t | is |
+ | | | a |
+ | test | | test |
+
+
""")
diff --git a/tox.ini b/tox.ini
index e077c5485..589393600 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,9 +1,10 @@
[tox]
envlist =
- py26-1.3, py26-1.4, py26-1.5, py26-1.6,
- py27-1.3, py27-1.4, py27-1.5, py27-1.6, py27-1.7, py27-trunk,
+ py26-1.4, py26-1.5, py26-1.6,
+ py27-1.4, py27-1.5, py27-1.6, py27-1.7, py27-trunk,
py32-1.5, py32-1.6, py32-1.7, py32-trunk,
py33-1.5, py33-1.6, py33-1.7, py33-trunk,
+ py34-1.7, py34-trunk,
docs, flake8
@@ -27,28 +28,22 @@ exclude = __init__.py
deps = flake8
commands = flake8 simple_history
-[testenv:py26-1.3]
-basepython = python2.6
-deps =
- django == 1.3.7
- coverage == 3.6
-
[testenv:py26-1.4]
basepython = python2.6
deps =
- django == 1.4.10
+ django == 1.4.13
coverage == 3.6
[testenv:py26-1.5]
basepython = python2.6
deps =
- django == 1.5.5
+ django == 1.5.8
coverage == 3.6
[testenv:py26-1.6]
basepython = python2.6
deps =
- django == 1.6.2
+ django == 1.6.5
coverage == 3.6
[testenv:py26-trunk]
@@ -57,29 +52,22 @@ deps =
https://github.com/django/django/tarball/master
coverage == 3.6
-
-[testenv:py27-1.3]
-basepython = python2.7
-deps =
- django == 1.3.7
- coverage == 3.6
-
[testenv:py27-1.4]
basepython = python2.7
deps =
- django == 1.4.10
+ django == 1.4.13
coverage == 3.6
[testenv:py27-1.5]
basepython = python2.7
deps =
- django == 1.5.5
+ django == 1.5.8
coverage == 3.6
[testenv:py27-1.6]
basepython = python2.7
deps =
- django == 1.6.2
+ django == 1.6.5
coverage == 3.6
[testenv:py27-1.7]
@@ -98,13 +86,13 @@ deps =
[testenv:py32-1.5]
basepython = python3.2
deps =
- django == 1.5.5
+ django == 1.5.8
coverage == 3.6
[testenv:py32-1.6]
basepython = python3.2
deps =
- django == 1.6.2
+ django == 1.6.5
coverage == 3.6
[testenv:py32-1.7]
@@ -123,13 +111,13 @@ deps =
[testenv:py33-1.5]
basepython = python3.3
deps =
- django == 1.5.5
+ django == 1.5.8
coverage == 3.6
[testenv:py33-1.6]
basepython = python3.3
deps =
- django == 1.6.2
+ django == 1.6.5
coverage == 3.6
[testenv:py33-1.7]
@@ -143,3 +131,15 @@ basepython = python3.3
deps =
https://github.com/django/django/tarball/master
coverage == 3.6
+
+[testenv:py34-1.7]
+basepython = python3.4
+deps =
+ https://github.com/django/django/tarball/stable/1.7.x
+ coverage == 3.6
+
+[testenv:py34-trunk]
+basepython = python3.4
+deps =
+ https://github.com/django/django/tarball/master
+ coverage == 3.6