From 9290db23b84ccd6f9e8c2cbcb1d6ef61d5bf9704 Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Fri, 4 Jul 2014 14:53:24 -0700 Subject: [PATCH 01/14] Added initial 'compare view' work --- simple_history/admin.py | 70 ++++++++++++++++++- .../simple_history/history_compare.html | 47 +++++++++++++ .../simple_history/object_history.html | 61 +++++++++------- 3 files changed, 153 insertions(+), 25 deletions(-) create mode 100644 simple_history/templates/simple_history/history_compare.html diff --git a/simple_history/admin.py b/simple_history/admin.py index 6d6f67a32..573131e03 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import re +import difflib from django import template from django.core.exceptions import PermissionDenied try: @@ -10,7 +12,7 @@ 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.shortcuts import get_object_or_404, render_to_response, render from django.contrib.admin.util import unquote from django.utils.text import capfirst from django.utils.html import mark_safe @@ -31,6 +33,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.""" @@ -46,6 +49,9 @@ def get_urls(self): 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 @@ -173,6 +179,68 @@ def history_form_view(self, request, object_id, version_id): return render_to_response(self.object_history_form_template, context, 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, curr = history.get(pk=request.GET['from']), history.get(pk=request.GET['to']) + def generate_diff(prev, curr): + markup = "" + try: + prev = re.split("(\W)", prev) + curr = re.split("(\W)", curr) + except TypeError: + if prev != curr: + return '{removed_content}
{added_content}'.format( + removed_content=prev, added_content=curr) + return curr + p_a, p_b, p_l = (0, 0, 0) + try: + for block in difflib.SequenceMatcher(a=prev, b=curr).get_matching_blocks(): + a, b, l = block + removed = prev[p_a+p_l:a] + added = curr[p_b+p_l:b] + same = curr[b:b+l] + if removed: + markup += '{content}'.format(content="".join(removed)) + if added: + markup += '{content}'.format(content="".join(added)) + if same: + markup += '{content}'.format(content="".join(same)) + p_a, p_b, p_l = block + + except TypeError: + return curr + return markup + + fields = [{ + 'name': field.attname, + 'contents': generate_diff(getattr(prev, field.attname), getattr(curr, field.attname)), + } 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': object_id, + 'object': obj, + 'history_bef': prev, + 'history_aft': curr, + '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""" obj._history_user = request.user 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..b66c42159 --- /dev/null +++ b/simple_history/templates/simple_history/history_compare.html @@ -0,0 +1,47 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls admin_static admin_modify %} + +{% block extrastyle %}{{ block.super }}{% endblock %} + +{% 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 }} +

{{ field.contents|safe }}

+
+
+{% 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..3e803fe9a 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 %} From 463aa07b380b5311366e4ad7030291a475d1d60d Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Sat, 5 Jul 2014 12:57:57 -0700 Subject: [PATCH 02/14] Removed use of 'render_to_response' from admin --- simple_history/admin.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/simple_history/admin.py b/simple_history/admin.py index 573131e03..a51cf5963 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -12,7 +12,7 @@ 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, render +from django.shortcuts import get_object_or_404, render from django.contrib.admin.util import unquote from django.utils.text import capfirst from django.utils.html import mark_safe @@ -83,8 +83,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 @@ -176,8 +176,8 @@ 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) From a62abdecff3bc12e5cf549dc1b26a5704561e55e Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Sat, 5 Jul 2014 13:14:48 -0700 Subject: [PATCH 03/14] Cleanup, refactor logic --- simple_history/admin.py | 60 +++++++++---------- .../simple_history/history_compare.html | 27 +++++---- 2 files changed, 46 insertions(+), 41 deletions(-) diff --git a/simple_history/admin.py b/simple_history/admin.py index a51cf5963..7a40fb157 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -184,39 +184,14 @@ def compare_view(self, request, object_id, extra_context=None): obj = get_object_or_404(self.model, pk=object_id) history = getattr(obj, self.model._meta.simple_history_manager_attribute) - prev, curr = history.get(pk=request.GET['from']), history.get(pk=request.GET['to']) - def generate_diff(prev, curr): - markup = "" - try: - prev = re.split("(\W)", prev) - curr = re.split("(\W)", curr) - except TypeError: - if prev != curr: - return '{removed_content}
{added_content}'.format( - removed_content=prev, added_content=curr) - return curr - p_a, p_b, p_l = (0, 0, 0) - try: - for block in difflib.SequenceMatcher(a=prev, b=curr).get_matching_blocks(): - a, b, l = block - removed = prev[p_a+p_l:a] - added = curr[p_b+p_l:b] - same = curr[b:b+l] - if removed: - markup += '{content}'.format(content="".join(removed)) - if added: - markup += '{content}'.format(content="".join(added)) - if same: - markup += '{content}'.format(content="".join(same)) - p_a, p_b, p_l = block - - except TypeError: - return curr - return markup + prev = history.get(pk=request.GET['from']) + curr = history.get(pk=request.GET['to']) fields = [{ 'name': field.attname, - 'contents': generate_diff(getattr(prev, field.attname), getattr(curr, field.attname)), + 'content': getattr(curr, field.attname), + 'compare_nodes': self._get_delta_nodes( + getattr(prev, field.attname), getattr(curr, field.attname)), } for field in self.model._meta.fields] opts = self.model._meta d = { @@ -241,6 +216,31 @@ def generate_diff(prev, curr): return render(request, template_name=self.object_compare_template, current_app=self.admin_site.name, dictionary=d) + @staticmethod + def _get_delta_nodes(a, b): + delta_nodes = [] + try: + a = re.split("(\W)", a) + b = re.split("(\W)", b) + except TypeError: + if a != b: + return [('removed', a), ('added', b)] + return [('unchanged', b)] + prev_a_start, prev_b_start, prev_len = (0, 0, 0) + for block in difflib.SequenceMatcher(a=a, b=b).get_matching_blocks(): + a_start, b_start, length = block + removed = "".join(a[prev_a_start + prev_len:a_start]) + added = "".join(b[prev_b_start + prev_len:b_start]) + same = "".join(b[b_start:b_start + length]) + if removed: + delta_nodes.append(['removed', removed]) + if added: + delta_nodes.append(['added', added]) + if same: + delta_nodes.append(['unchanged', same]) + prev_a_start, prev_b_start, prev_len = block + return delta_nodes + def save_model(self, request, obj, form, change): """Set special model attribute to user for reference after save""" obj._history_user = request.user diff --git a/simple_history/templates/simple_history/history_compare.html b/simple_history/templates/simple_history/history_compare.html index b66c42159..58851016a 100644 --- a/simple_history/templates/simple_history/history_compare.html +++ b/simple_history/templates/simple_history/history_compare.html @@ -16,13 +16,16 @@ {% block content %} @@ -31,14 +34,16 @@ {% for field in fields %}
- {% if field.name %}

{{ field.name }}

{% endif %} - {% if field.description %} -
{{ field.description|safe }}
- {% endif %} -
- {{ field.label_tag }} -

{{ field.contents|safe }}

-
+ {% if field.name %}

{{ field.name }}

{% endif %} + {% if field.description %} +
{{ field.description|safe }}
+ {% endif %} +
+ {{ field.label_tag }} +

+ {% for type, content in field.compare_nodes %}{{ content|safe }}{% endfor %} +

+
{% endfor %} From 9057b8a2e72a63ed8053e6db89faf75cdcff355d Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Sat, 5 Jul 2014 19:00:11 -0700 Subject: [PATCH 04/14] Added test to access compare view --- .../simple_history/object_history.html | 2 +- simple_history/tests/tests/test_admin.py | 24 ++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/simple_history/templates/simple_history/object_history.html b/simple_history/templates/simple_history/object_history.html index 3e803fe9a..318d1fb6b 100644 --- a/simple_history/templates/simple_history/object_history.html +++ b/simple_history/templates/simple_history/object_history.html @@ -10,7 +10,7 @@
{% if action_list %} -
+ diff --git a/simple_history/tests/tests/test_admin.py b/simple_history/tests/tests/test_admin.py index 00c6e15c0..992b7087c 100644 --- a/simple_history/tests/tests/test_admin.py +++ b/simple_history/tests/tests/test_admin.py @@ -29,7 +29,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 +43,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 +160,21 @@ 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 ") From 11ecb172359c4d1256c9a902e39039d29df54ac8 Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Sat, 5 Jul 2014 19:10:06 -0700 Subject: [PATCH 05/14] Fix admin compatability-related coverage --- simple_history/admin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/simple_history/admin.py b/simple_history/admin.py index 7a40fb157..843fee3f3 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -6,7 +6,7 @@ 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 @@ -19,7 +19,7 @@ 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 @@ -42,7 +42,7 @@ 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( "", @@ -138,7 +138,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 From 935ae9a4ae8fdaf75ecd5e9fa5c25830f496aaa9 Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Sat, 5 Jul 2014 19:33:03 -0700 Subject: [PATCH 06/14] Added coverage for delta helper method --- simple_history/admin.py | 6 +++--- simple_history/tests/tests/test_admin.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/simple_history/admin.py b/simple_history/admin.py index 843fee3f3..6df3e16d0 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -233,11 +233,11 @@ def _get_delta_nodes(a, b): added = "".join(b[prev_b_start + prev_len:b_start]) same = "".join(b[b_start:b_start + length]) if removed: - delta_nodes.append(['removed', removed]) + delta_nodes.append(('removed', removed)) if added: - delta_nodes.append(['added', added]) + delta_nodes.append(('added', added)) if same: - delta_nodes.append(['unchanged', same]) + delta_nodes.append(('unchanged', same)) prev_a_start, prev_b_start, prev_len = block return delta_nodes diff --git a/simple_history/tests/tests/test_admin.py b/simple_history/tests/tests/test_admin.py index 992b7087c..3ae04975b 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 import admin from ..models import Book, Person, Poll @@ -178,3 +180,19 @@ def setUp(self): def test_navigate_to_compare(self): response = self.app.get(get_history_url(self.poll)).form.submit() response.mustcontain("Compare ") + + +class GenerateDeltaTest(TestCase): + cases = ( + ((1, 2), [('removed', 1), ('added', 2)]), + ((1, 1), [('unchanged', 1)]), + (("", ""), []), + (("one two three", "one too three"), + [('unchanged', "one "), ('removed', "two"), ('added', "too"), + ('unchanged', " three")]), + ) + + def test_delta_generation(self): + for values, delta_nodes in self.cases: + self.assertEqual(admin.SimpleHistoryAdmin._get_delta_nodes(*values), + delta_nodes) From 756d481b9860c9412b29023df2f5dee8ef93c391 Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Sat, 5 Jul 2014 21:03:04 -0700 Subject: [PATCH 07/14] Compatability fixes --- simple_history/admin.py | 4 ++-- .../templates/simple_history/history_compare.html | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/simple_history/admin.py b/simple_history/admin.py index 6df3e16d0..20e4c46ec 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -13,7 +13,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.urlresolvers import reverse from django.shortcuts import get_object_or_404, render -from django.contrib.admin.util import unquote +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 _ @@ -198,7 +198,7 @@ def compare_view(self, request, object_id, extra_context=None): 'title': _('Compare %s') % force_text(obj), 'app_label': opts.app_label, 'module_name': capfirst(force_text(opts.verbose_name_plural)), - 'object_id': object_id, + 'object_id': quote(object_id), 'object': obj, 'history_bef': prev, 'history_aft': curr, diff --git a/simple_history/templates/simple_history/history_compare.html b/simple_history/templates/simple_history/history_compare.html index 58851016a..1d9a582e4 100644 --- a/simple_history/templates/simple_history/history_compare.html +++ b/simple_history/templates/simple_history/history_compare.html @@ -1,14 +1,14 @@ -{% extends "admin/base_site.html" %} -{% load i18n admin_urls admin_static admin_modify %} - -{% block extrastyle %}{{ block.super }}{% endblock %} +{% extends "admin/change_form.html" %} +{% load i18n %} +{% load url from future %} +{% load admin_urls %} {% block breadcrumbs %} {% endblock %} From 63113aa062670c6ec22c31e261d7bab7ed92cba5 Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Sat, 12 Jul 2014 17:11:35 -0700 Subject: [PATCH 08/14] Removed Django 1.3 from the test matrix --- .travis.yml | 5 ----- tox.ini | 17 ++--------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 331ae917f..3a2c82b3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ python: - 3.3 env: - - DJANGO=Django==1.3.7 - DJANGO=Django==1.4.10 - DJANGO=Django==1.5.5 - DJANGO=Django==1.6.2 @@ -24,10 +23,6 @@ matrix: env: DJANGO=Django==1.6.2 - python: 2.6 env: DJANGO=https://github.com/django/django/tarball/stable/1.7.x - - python: 3.2 - env: DJANGO=Django==1.3.7 - - python: 3.3 - env: DJANGO=Django==1.3.7 - python: 3.2 env: DJANGO=Django==1.4.10 - python: 3.3 diff --git a/tox.ini b/tox.ini index e077c5485..e4f6734fb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [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, docs, flake8 @@ -27,12 +27,6 @@ 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 = @@ -57,13 +51,6 @@ 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 = From 3592ee0cd7bda4fbf13032d1288d9d329cee174e Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Sat, 12 Jul 2014 17:11:44 -0700 Subject: [PATCH 09/14] Fixed line length --- simple_history/tests/tests/test_admin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/simple_history/tests/tests/test_admin.py b/simple_history/tests/tests/test_admin.py index 3ae04975b..be3644788 100644 --- a/simple_history/tests/tests/test_admin.py +++ b/simple_history/tests/tests/test_admin.py @@ -194,5 +194,7 @@ class GenerateDeltaTest(TestCase): def test_delta_generation(self): for values, delta_nodes in self.cases: - self.assertEqual(admin.SimpleHistoryAdmin._get_delta_nodes(*values), - delta_nodes) + self.assertEqual( + admin.SimpleHistoryAdmin._get_delta_nodes(*values), + delta_nodes, + ) From 0777640ce7fa69ac248abf74a57a9a84fcfbc476 Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Sun, 13 Jul 2014 08:32:30 -0700 Subject: [PATCH 10/14] Added Python 3.4 to the matrix, bumped tested Django versions --- .travis.yml | 19 +++++++++++++------ tox.ini | 33 +++++++++++++++++++++++---------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3a2c82b3a..bef985b67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,12 @@ python: - 2.7 - 3.2 - 3.3 + - 3.4 env: - - 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: @@ -20,12 +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.4.10 + env: DJANGO=Django==1.4.13 - 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/tox.ini b/tox.ini index e4f6734fb..589393600 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = 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 @@ -30,19 +31,19 @@ commands = flake8 simple_history [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] @@ -54,19 +55,19 @@ deps = [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] @@ -85,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] @@ -110,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] @@ -130,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 From 03fcf59003d633dc9d42f8748ad2df02f225a155 Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Tue, 15 Jul 2014 20:31:53 -0700 Subject: [PATCH 11/14] Use HhtmlDiff to generate the diff table --- simple_history/admin.py | 33 ++----------------- .../simple_history/history_compare.html | 17 +++++----- simple_history/templatetags/__init__.py | 0 .../templatetags/simple_history_compare.py | 28 ++++++++++++++++ simple_history/tests/tests/test_admin.py | 4 +-- simple_history/utils.py | 32 ++++++++++++++++++ 6 files changed, 72 insertions(+), 42 deletions(-) create mode 100644 simple_history/templatetags/__init__.py create mode 100644 simple_history/templatetags/simple_history_compare.py create mode 100644 simple_history/utils.py diff --git a/simple_history/admin.py b/simple_history/admin.py index 20e4c46ec..db87deb79 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -import re -import difflib from django import template from django.core.exceptions import PermissionDenied try: @@ -190,8 +188,8 @@ def compare_view(self, request, object_id, extra_context=None): fields = [{ 'name': field.attname, 'content': getattr(curr, field.attname), - 'compare_nodes': self._get_delta_nodes( - getattr(prev, field.attname), 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 = { @@ -200,8 +198,6 @@ def compare_view(self, request, object_id, extra_context=None): 'module_name': capfirst(force_text(opts.verbose_name_plural)), 'object_id': quote(object_id), 'object': obj, - 'history_bef': prev, - 'history_aft': curr, 'fields': fields, 'opts': opts, 'add': False, @@ -216,31 +212,6 @@ def compare_view(self, request, object_id, extra_context=None): return render(request, template_name=self.object_compare_template, current_app=self.admin_site.name, dictionary=d) - @staticmethod - def _get_delta_nodes(a, b): - delta_nodes = [] - try: - a = re.split("(\W)", a) - b = re.split("(\W)", b) - except TypeError: - if a != b: - return [('removed', a), ('added', b)] - return [('unchanged', b)] - prev_a_start, prev_b_start, prev_len = (0, 0, 0) - for block in difflib.SequenceMatcher(a=a, b=b).get_matching_blocks(): - a_start, b_start, length = block - removed = "".join(a[prev_a_start + prev_len:a_start]) - added = "".join(b[prev_b_start + prev_len:b_start]) - same = "".join(b[b_start:b_start + length]) - if removed: - delta_nodes.append(('removed', removed)) - if added: - delta_nodes.append(('added', added)) - if same: - delta_nodes.append(('unchanged', same)) - prev_a_start, prev_b_start, prev_len = block - return delta_nodes - def save_model(self, request, obj, form, change): """Set special model attribute to user for reference after save""" obj._history_user = request.user diff --git a/simple_history/templates/simple_history/history_compare.html b/simple_history/templates/simple_history/history_compare.html index 1d9a582e4..51aafa2b7 100644 --- a/simple_history/templates/simple_history/history_compare.html +++ b/simple_history/templates/simple_history/history_compare.html @@ -2,6 +2,7 @@ {% load i18n %} {% load url from future %} {% load admin_urls %} +{% load simple_history_compare %} {% block breadcrumbs %} {% endfor %} 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..95f29cb38 --- /dev/null +++ b/simple_history/templatetags/simple_history_compare.py @@ -0,0 +1,28 @@ +from __future__ import unicode_literals + +import difflib +from django import template + +from .. import utils + +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 be3644788..50eeefbe7 100644 --- a/simple_history/tests/tests/test_admin.py +++ b/simple_history/tests/tests/test_admin.py @@ -9,7 +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 import admin +from simple_history import utils from ..models import Book, Person, Poll @@ -195,6 +195,6 @@ class GenerateDeltaTest(TestCase): def test_delta_generation(self): for values, delta_nodes in self.cases: self.assertEqual( - admin.SimpleHistoryAdmin._get_delta_nodes(*values), + utils.get_delta_sequences(*values), delta_nodes, ) diff --git a/simple_history/utils.py b/simple_history/utils.py new file mode 100644 index 000000000..539527c93 --- /dev/null +++ b/simple_history/utils.py @@ -0,0 +1,32 @@ +import re +import difflib + + +REMOVED = 'removed' +ADDED = 'added' +UNCHANGED = 'unchanged' + + +def get_delta_sequences(a, b): + delta_nodes = [] + try: + a = re.split("(\W)", a) + b = re.split("(\W)", b) + except TypeError: + if a != b: + return [(REMOVED, a), (ADDED, b)] + return [(UNCHANGED, b)] + prev_a_start, prev_b_start, prev_len = (0, 0, 0) + for block in difflib.SequenceMatcher(a=a, b=b).get_matching_blocks(): + a_start, b_start, length = block + removed = "".join(a[prev_a_start + prev_len:a_start]) + added = "".join(b[prev_b_start + prev_len:b_start]) + same = "".join(b[b_start:b_start + length]) + if removed: + delta_nodes.append((REMOVED, removed)) + if added: + delta_nodes.append((ADDED, added)) + if same: + delta_nodes.append((UNCHANGED, same)) + prev_a_start, prev_b_start, prev_len = block + return delta_nodes From 5cfe69e145a0224373202a401c0531e78ffc85b9 Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Sat, 2 Aug 2014 23:38:50 -0700 Subject: [PATCH 12/14] Removed unused custom delta sequence generator --- .../templatetags/simple_history_compare.py | 2 -- simple_history/tests/tests/test_admin.py | 19 ----------- simple_history/utils.py | 32 ------------------- 3 files changed, 53 deletions(-) delete mode 100644 simple_history/utils.py diff --git a/simple_history/templatetags/simple_history_compare.py b/simple_history/templatetags/simple_history_compare.py index 95f29cb38..9ff76afee 100644 --- a/simple_history/templatetags/simple_history_compare.py +++ b/simple_history/templatetags/simple_history_compare.py @@ -3,8 +3,6 @@ import difflib from django import template -from .. import utils - register = template.Library() diff --git a/simple_history/tests/tests/test_admin.py b/simple_history/tests/tests/test_admin.py index 50eeefbe7..84c1abdef 100644 --- a/simple_history/tests/tests/test_admin.py +++ b/simple_history/tests/tests/test_admin.py @@ -9,7 +9,6 @@ except ImportError: # django 1.4 compatibility from django.contrib.auth.models import User from django.contrib.admin.util import quote -from simple_history import utils from ..models import Book, Person, Poll @@ -180,21 +179,3 @@ def setUp(self): def test_navigate_to_compare(self): response = self.app.get(get_history_url(self.poll)).form.submit() response.mustcontain("Compare ") - - -class GenerateDeltaTest(TestCase): - cases = ( - ((1, 2), [('removed', 1), ('added', 2)]), - ((1, 1), [('unchanged', 1)]), - (("", ""), []), - (("one two three", "one too three"), - [('unchanged', "one "), ('removed', "two"), ('added', "too"), - ('unchanged', " three")]), - ) - - def test_delta_generation(self): - for values, delta_nodes in self.cases: - self.assertEqual( - utils.get_delta_sequences(*values), - delta_nodes, - ) diff --git a/simple_history/utils.py b/simple_history/utils.py deleted file mode 100644 index 539527c93..000000000 --- a/simple_history/utils.py +++ /dev/null @@ -1,32 +0,0 @@ -import re -import difflib - - -REMOVED = 'removed' -ADDED = 'added' -UNCHANGED = 'unchanged' - - -def get_delta_sequences(a, b): - delta_nodes = [] - try: - a = re.split("(\W)", a) - b = re.split("(\W)", b) - except TypeError: - if a != b: - return [(REMOVED, a), (ADDED, b)] - return [(UNCHANGED, b)] - prev_a_start, prev_b_start, prev_len = (0, 0, 0) - for block in difflib.SequenceMatcher(a=a, b=b).get_matching_blocks(): - a_start, b_start, length = block - removed = "".join(a[prev_a_start + prev_len:a_start]) - added = "".join(b[prev_b_start + prev_len:b_start]) - same = "".join(b[b_start:b_start + length]) - if removed: - delta_nodes.append((REMOVED, removed)) - if added: - delta_nodes.append((ADDED, added)) - if same: - delta_nodes.append((UNCHANGED, same)) - prev_a_start, prev_b_start, prev_len = block - return delta_nodes From aa5c915d26491b70653d67958ff783941d63d750 Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Sat, 2 Aug 2014 23:39:13 -0700 Subject: [PATCH 13/14] Added the templatetags package to the list of project packages --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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", From b71a5d4e5cd31f6b429e51498c2a3588da01fa96 Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Sun, 3 Aug 2014 00:18:46 -0700 Subject: [PATCH 14/14] Simple output check for `diff_table` templatetag --- simple_history/tests/tests/test_admin.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/simple_history/tests/tests/test_admin.py b/simple_history/tests/tests/test_admin.py index 84c1abdef..b09ff0370 100644 --- a/simple_history/tests/tests/test_admin.py +++ b/simple_history/tests/tests/test_admin.py @@ -9,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 @@ -179,3 +180,22 @@ def setUp(self): 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
""")