Skip to content

Compare history versions #103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
59 changes: 49 additions & 10 deletions simple_history/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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."""
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"""
Expand Down
51 changes: 51 additions & 0 deletions simple_history/templates/simple_history/history_compare.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{% extends "admin/change_form.html" %}
{% load i18n %}
{% load url from future %}
{% load admin_urls %}
{% load simple_history_compare %}

{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ module_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'change' object_id %}">{{ object|truncatewords:"18" }}</a>
&rsaquo; {% trans 'Compare History' %}
</div>
{% endblock %}

{% block content %}

<style>
.diff_sub {
background: DarkSalmon;
}
.diff_add {
background: YellowGreen;
}
.diff_chg {
background: Khaki;
}
</style>

<div id="content-main">
<div class="module">

{% for field in fields %}
<div class="form-row field-{{ field.name }}">
{% if field.name %}<h4>{{ field.name }}</h4>{% endif %}
{% if field.description %}
<div class="description">{{ field.description|safe }}</div>
{% endif %}
<div>
{{ field.label_tag }}
<div>
{% diff_table field.content field.prev_content field.section_break %}
</div>
</div>
</div>
{% endfor %}

</div>
</div>
{% endblock content %}
61 changes: 37 additions & 24 deletions simple_history/templates/simple_history/object_history.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,45 @@

<div class="module">
{% if action_list %}
<table id="change-history" class="table table-bordered table-striped">
<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 %}
<form method="GET" action="../compare/">
<table id="change-history" class="table table-bordered table-striped">
<thead>
<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>
{% if action.history_user %}
<a href="{% url admin_user_view action.history_user_id %}">{{ action.history_user }}</a>
{% else %}
None
{% endif %}
</td>
<th scope="col" colspan="2"><input type="submit" value="{% trans 'Compare' %}"></th>
<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>
{% endfor %}
</tbody>
</table>
</thead>
<tbody>
{% for action in action_list %}
<tr>
<td>
{% if not forloop.first %}
<input type="radio" name="from" value="{{ action.pk|iriencode }}"{% if forloop.counter == 2 %} checked{% endif %}>
{% endif %}
</td>
<td>
{% if not forloop.last %}
<input type="radio" name="to" value="{{ action.pk|iriencode }}"{% if forloop.first %} checked{% endif %}>
{% endif %}
</td>
<td><a href="{{ action.revert_url }}">{{ action.history_object }}</a></td>
<td>{{ action.history_date }}</td>
<td>{{ action.get_history_type_display }}</td>
<td>
{% if action.history_user %}
<a href="{% url admin_user_view action.history_user_id %}">{{ action.history_user }}</a>
{% else %}
None
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% else %}
<p>{% trans "This object doesn't have a change history." %}</p>
{% endif %}
Expand Down
Empty file.
26 changes: 26 additions & 0 deletions simple_history/templatetags/simple_history_compare.py
Original file line number Diff line number Diff line change
@@ -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"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add unit tests for this?

differ = difflib.HtmlDiff(wrapcolumn=80)
try:
return differ.make_table(a.split(line_split), b.split(line_split))
except AttributeError:
if a != b:
a = '<span class="diff_sub">{a}</span>'.format(a=a)
b = '<span class="diff_add">{b}</span>'.format(b=b)
return """<table class="diff" id="difflib_chg_to0__top" cellspacing="0" cellpadding="0" rules="groups">
<colgroup></colgroup> <colgroup></colgroup> <colgroup></colgroup>
<colgroup></colgroup> <colgroup></colgroup> <colgroup></colgroup>

<tbody>
<tr><td class="diff_next"><a href="#difflib_chg_to0__top">t</a></td><td class="diff_header" id="from0_1">1</td><td nowrap="nowrap">{a}</td><td class="diff_next"><a href="#difflib_chg_to0__top">t</a></td><td class="diff_header" id="to0_1">1</td><td nowrap="nowrap">{b}</td></tr>
</tbody>
</table>
""".format(a=a, b=b)
45 changes: 44 additions & 1 deletion simple_history/tests/tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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

Expand All @@ -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')
Expand All @@ -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:
Expand Down Expand Up @@ -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("<title>Compare ")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very rudimentary test. Should we check a comparison or two?



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, """
<table class="diff" id="difflib_chg_to1__top"
cellspacing="0" cellpadding="0" rules="groups" >
<colgroup></colgroup> <colgroup></colgroup> <colgroup></colgroup>
<colgroup></colgroup> <colgroup></colgroup> <colgroup></colgroup>

<tbody>
<tr><td class="diff_next" id="difflib_chg_to1__0"><a href="#difflib_chg_to1__0">f</a></td><td class="diff_header" id="from1_1">1</td><td nowrap="nowrap">this</td><td class="diff_next"><a href="#difflib_chg_to1__0">f</a></td><td class="diff_header" id="to1_1">1</td><td nowrap="nowrap">this</td></tr>
<tr><td class="diff_next"><a href="#difflib_chg_to1__top">t</a></td><td class="diff_header" id="from1_2">2</td><td nowrap="nowrap"><span class="diff_sub">an</span></td><td class="diff_next"><a href="#difflib_chg_to1__top">t</a></td><td class="diff_header" id="to1_2">2</td><td nowrap="nowrap"><span class="diff_add">is</span></td></tr>
<tr><td class="diff_next"></td><td class="diff_header"></td><td nowrap="nowrap"></td><td class="diff_next"></td><td class="diff_header" id="to1_3">3</td><td nowrap="nowrap"><span class="diff_add">a</span></td></tr>
<tr><td class="diff_next"></td><td class="diff_header" id="from1_3">3</td><td nowrap="nowrap">test</td><td class="diff_next"></td><td class="diff_header" id="to1_4">4</td><td nowrap="nowrap">test</td></tr>
</tbody>
</table>""")
Loading