From f8fed8ca49e5329d40781ad2470779d057b39f77 Mon Sep 17 00:00:00 2001 From: groovecoder Date: Wed, 4 Jul 2012 18:48:06 -0500 Subject: [PATCH] fix bug 767537 - inline diff in rss feed --- apps/wiki/feeds.py | 56 ++++++++++++++++++++------------ apps/wiki/helpers.py | 61 +++++++++++++++++++++++++++++++++-- apps/wiki/tests/test_views.py | 18 ++++++++--- settings.py | 7 +++- 4 files changed, 114 insertions(+), 28 deletions(-) diff --git a/apps/wiki/feeds.py b/apps/wiki/feeds.py index 8ca67c60194..366b68a5504 100644 --- a/apps/wiki/feeds.py +++ b/apps/wiki/feeds.py @@ -6,22 +6,21 @@ import jingo +from django.conf import settings +from django.contrib.auth.models import User from django.contrib.syndication.views import Feed, FeedDoesNotExist +from django.shortcuts import get_object_or_404 from django.utils.feedgenerator import (SyndicationFeed, Rss201rev2Feed, - Atom1Feed, get_tag_uri) + Atom1Feed) +from django.utils.html import escape import django.utils.simplejson as json -from django.shortcuts import get_object_or_404 - from django.utils.translation import ugettext as _ -from django.contrib.auth.models import User -from django.conf import settings - from sumo.urlresolvers import reverse from devmo.models import UserProfile -from wiki.models import (Document, Revision, HelpfulVote, EditorToolbar, - ReviewTag,) +from wiki.helpers import diff_table, diff_inline +from wiki.models import Document, Revision MAX_FEED_ITEMS = getattr(settings, 'MAX_FEED_ITEMS', 15) @@ -196,24 +195,41 @@ def items(self): return Revision.objects.order_by('-created')[:50] def item_title(self, item): - return "%s edited %s" % (item.creator.username, item.title) + return "%s/%s" % (item.document.locale, item.document.full_path) def item_description(self, item): previous = item.get_previous() if previous is None: - return 'Document created' - return item.comment + return '

Created by: %s

' % item.creator.username + # TODO: put this in a jinja template if django syndication will let us + by = '

Edited by: %s

' % item.creator.username + comment = '

Comment: %s

' % item.comment + diff = ("Diff:
%s
" % ( + diff_inline(previous.content, item.content))) + link_cell = '%s' + view_cell = link_cell % (reverse('wiki.document', + args=[item.document.full_path]), + _('View Page')) + edit_cell = link_cell % (reverse('wiki.edit_document', + args=[item.document.full_path]), + _('Edit Page')) + compare_cell = link_cell % (reverse('wiki.compare_revisions', + args=[item.document.full_path]) + + '?' + + urllib.urlencode({'from': previous.id, + 'to': item.id}), + _('Show comparison')) + history_cell = link_cell % (reverse('wiki.document_revisions', + args=[item.document.full_path]), + _('History')) + links_table = '' + links_table = links_table + '%s%s%s%s' % (view_cell, edit_cell, compare_cell, history_cell) + links_table = links_table + '
' + description = "%s%s%s%s" % (by, comment, diff, links_table) + return description def item_link(self, item): - previous = item.get_previous() - if previous is None: - return item.document.get_absolute_url() - compare_url = reverse('wiki.compare_revisions', - args=[item.document.slug]) - qs = {'from': previous.id, - 'to': item.id} - return "%s?%s" % (self.request.build_absolute_uri(compare_url), - urllib.urlencode(qs)) + return reverse('wiki.document', args=[item.document.full_path]) def item_pubdate(self, item): return item.created diff --git a/apps/wiki/helpers.py b/apps/wiki/helpers.py index 14f7f794efb..356f44cf094 100644 --- a/apps/wiki/helpers.py +++ b/apps/wiki/helpers.py @@ -1,5 +1,7 @@ +import difflib +import re + import constance.config -from difflib import HtmlDiff from jingo import register import jinja2 @@ -7,16 +9,71 @@ from wiki import parser +# http://stackoverflow.com/q/774316/571420 +def show_diff(seqm): + """Unify operations between two compared strings +seqm is a difflib.SequenceMatcher instance whose a & b are strings""" + lines = constance.config.FEED_DIFF_CONTEXT_LINES + full_output= [] + for opcode, a0, a1, b0, b1 in seqm.get_opcodes(): + if opcode == 'equal': + full_output.append(seqm.a[a0:a1]) + elif opcode == 'insert': + full_output.append("" + seqm.b[b0:b1] + "") + elif opcode == 'delete': + full_output.append("" + seqm.a[a0:a1] + "") + elif opcode == 'replace': + full_output.append(" " + seqm.a[a0:a1] + " ") + full_output.append(" " + seqm.b[b0:b1] + " ") + else: + raise RuntimeError, "unexpected opcode" + output = [] + whitespace_change = False + for piece in full_output: + if '' in piece or '' in piece: + # a change + if re.match('<(ins|del)>\W+', piece): + # the change is whitespace, + # ignore it and remove preceding context + output = output[:-lines] + whitespace_change = True + continue + else: + output.append(piece) + else: + context_lines = piece.splitlines() + if output == []: + # first context only shows preceding lines for next change + context = ['

...

'] + context_lines[-lines:] + elif whitespace_change: + # context shows preceding lines for next change + context = ['

...

'] + context_lines[-lines:] + whitespace_change = False + else: + # context shows subsequent lines + # and preceding lines for next change + context = context_lines[:lines] + ['

...

'] + context_lines[-lines:] + output = output + context + # remove extra context from the very end + output = output[:-lines] + return ''.join(output) + @register.function def diff_table(content_from, content_to): """Creates an HTML diff of the passed in content_from and content_to.""" - html_diff = HtmlDiff(wrapcolumn=DIFF_WRAP_COLUMN) + html_diff = difflib.HtmlDiff(wrapcolumn=DIFF_WRAP_COLUMN) from_lines = content_from.splitlines() to_lines = content_to.splitlines() diff = html_diff.make_table(from_lines, to_lines, context=True, numlines=constance.config.DIFF_CONTEXT_LINES) return jinja2.Markup(diff) +@register.function +def diff_inline(content_from, content_to): + sm = difflib.SequenceMatcher(None, content_from, content_to) + diff = show_diff(sm) + return jinja2.Markup(diff) + @register.function def generate_video(v): diff --git a/apps/wiki/tests/test_views.py b/apps/wiki/tests/test_views.py index 5278778fdae..d907f428482 100644 --- a/apps/wiki/tests/test_views.py +++ b/apps/wiki/tests/test_views.py @@ -1186,6 +1186,7 @@ def test_revisions_feed(self): for i in xrange(1, 6): r = revision(save=True, document=d, title='HTML9', comment='Revision %s' % i, + content = "Some Content %s" % i, is_approved=True, created=datetime.datetime.now()\ + datetime.timedelta(seconds=5*i)) @@ -1193,11 +1194,18 @@ def test_revisions_feed(self): resp = self.client.get(reverse('wiki.feeds.recent_revisions', args=(), kwargs={'format': 'rss'})) eq_(200, resp.status_code) + feed = pq(resp.content) + eq_(5, len(feed.find('item'))) + for i, item in enumerate(feed.find('item')): + desc_text = pq(item).find('description').text() + ok_('by: testuser' in desc_text) + if "Edited" in desc_text: + ok_('Comment: Revision' in desc_text) + ok_('' in desc_text) + ok_('$compare?to' in desc_text) + ok_('$edit' in desc_text) + ok_('$history' in desc_text) - ok_('Document created' in resp.content) - ok_('Revision 3' in resp.content) - ok_('$compare?to' in resp.content) - class SectionEditingResourceTests(TestCaseBase): fixtures = ['test_users.json'] @@ -1838,4 +1846,4 @@ def test_legacy_redirect(self): 'filename': f['filename']}) resp = self.client.get(mindtouch_url) eq_(301, resp.status_code) - ok_(a.get_absolute_url() in resp['Location']) \ No newline at end of file + ok_(a.get_absolute_url() in resp['Location']) diff --git a/settings.py b/settings.py index 81286ded1b7..f9e22b90321 100644 --- a/settings.py +++ b/settings.py @@ -998,7 +998,12 @@ def read_only_mode(env): DIFF_CONTEXT_LINES = ( 0, - 'Number of lines of context to show in diff displays.', + 'Number of lines of context to show in diff display.', + ), + + FEED_DIFF_CONTEXT_LINES = ( + 3, + 'Number of lines of context to show in feed diff display.', ), )