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 %} +
+ {% if field.name %}

{{ field.name }}

{% endif %} + {% if field.description %} +
{{ field.description|safe }}
+ {% endif %} +
+ {{ field.label_tag }} +
+ {% diff_table field.content field.prev_content field.section_break %} +
+
+
+{% 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 %} - - - - - - - - - - - {% for action in action_list %} + +
{% trans 'Object' %}{% trans 'Date/time' %}{% trans 'Comment' %}{% trans 'Changed by' %}
+ - - - - + + + + + - {% endfor %} - -
{{ action.history_object }}{{ action.history_date }}{{ action.get_history_type_display }} - {% if action.history_user %} - {{ action.history_user }} - {% else %} - None - {% endif %} - {% trans 'Object' %}{% trans 'Date/time' %}{% trans 'Comment' %}{% trans 'Changed by' %}
+ + + {% for action in action_list %} + + + {% if not forloop.first %} + + {% endif %} + + + {% if not forloop.last %} + + {% endif %} + + {{ action.history_object }} + {{ action.history_date }} + {{ action.get_history_type_display }} + + {% if action.history_user %} + {{ action.history_user }} + {% else %} + None + {% endif %} + + + {% endfor %} + + + {% 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 """ + + + + + + +
t1{a}t1{b}
+""".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, """ + + + + + + + + + + +
f1thisf1this
t2ant2is
3a
3test4test
""") 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