Commits

Victor Kotseruba committed 199f7b7

initial

  • Participants

Comments (0)

Files changed (30)

+# -*- coding: utf-8 -*-
+
+from setuptools import setup
+
+setup(
+    name='superwiki',
+    version='0.1',
+    packages=['superwiki'],
+    install_requires=[
+        'django',
+        'south',
+        'markdown',
+        'django-reversion',
+    ],
+    author='Viktor Kotseruba',
+    author_email='barbuzaster@gmail.com',
+    description='wiki for django',
+    license='MIT',
+    keywords='wiki web django',
+)

superwiki/__init__.py

+import signals

superwiki/admin.py

+# -*- coding: utf-8 -*-
+
+from django.contrib import admin
+
+from models import WikiPage
+
+
+class WikiPageAdmin(admin.ModelAdmin):
+    exclude = ('rendered_body', )
+
+
+admin.site.register(WikiPage, WikiPageAdmin)

superwiki/common.py

+# -*- coding: utf-8 -*-
+
+from django.utils.importlib import import_module
+from django.conf import settings
+
+
+URL_R = '[\w-]+(?:/|\w|_)*'
+
+def import_string(s):
+    module_name = '.'.join(s.split('.')[:-1])
+    obj_name = s.split('.')[-1]
+    return getattr(import_module(module_name), obj_name)
+
+
+bind_getter = import_string(settings.WIKI_BIND_GETTER)
+url_getter = import_string(settings.WIKI_URL_GETTER)
+write_perm_check = import_string(settings.WIKI_WRITE_PERM_CHECK)
+markup_processors = dict()
+for markup_type, processors in settings.WIKI_MARKUP_PROCESSORS.items():
+    markup_processors[markup_type] = map(import_string, processors)

superwiki/forms.py

+# -*- coding: utf-8 -*-
+
+from django import forms
+
+from models import WikiPage
+
+
+class WikiPageForm(forms.ModelForm):
+    
+    class Meta:
+        model = WikiPage
+        fields = ('title', 'body')

superwiki/html_diff.py

+import re
+import difflib
+from copy import copy
+
+try:
+    from BeautifulSoup import BeautifulSoup
+    use_beautiful_soup = True
+except ImportError:
+    use_beautiful_soup = False
+
+split_html_regex = re.compile(r"""
+    # regular expression to split on tags, words, and punctuation. We wrap the
+    # whole thing in matching group parentheses, so that re.split doesn't
+    # remove anything 
+    ( 
+        <.*?>         # html open or close tag
+        |&.*?;        # html entity
+        |[a-zA-Z]+    # a word
+        |\d+          # a number
+        |[^\w\d\s<&]+ # punctuation
+    )
+""", re.VERBOSE)
+def split_html(html):
+    # remove &nbsp; and aliases
+    html = html.replace('&nbsp;', ' ')
+    html = html.replace('&#160;', ' ')
+    html = html.replace('&#xA0;', ' ')
+
+    html_list = split_html_regex.split(html)
+
+    html_list = [s for s in html_list if s] # remove empty strings caused by split
+    # normalize whitespace
+    for i, item in enumerate(copy(html_list)):
+        if item.isspace():
+            html_list[i] = ' '
+    return html_list
+
+
+def html_diff(old_text, new_text):
+    if use_beautiful_soup:
+        old_text = BeautifulSoup(old_text).prettify()
+        new_text = BeautifulSoup(new_text).prettify()
+
+    inline_diff = []
+    old_text = split_html(old_text)
+    new_text = split_html(new_text)
+    sm = difflib.SequenceMatcher(None, old_text, new_text)
+    has_text_changes = [False]
+    
+    def delete(old_section):
+        if has_text(old_section):
+            has_text_changes[0] = True
+            inline_diff.append(wrap_html(old_section, '<del>', '</del>'))
+        else:
+            inline_diff.append(old_section)
+    def insert(new_section):
+        if has_text(new_section):
+            has_text_changes[0] = True
+            inline_diff.append(wrap_html(new_section, '<ins>', '</ins>'))
+        else:
+            inline_diff.append(new_section)
+
+    for (tag, old_start, old_end, new_start, new_end) in sm.get_opcodes():
+        old_section = ''.join(old_text[old_start:old_end])
+        new_section = ''.join(new_text[new_start:new_end])
+        if tag == 'replace':
+            insert(new_section)
+            delete(old_section)
+        elif tag == 'delete':
+            delete(old_section)
+        elif tag == 'insert':
+            insert(new_section)
+        elif tag == 'equal':
+            inline_diff.append(old_section)
+
+    result = '\n'.join(inline_diff)
+    if use_beautiful_soup:
+        result = BeautifulSoup(result).prettify()
+    return result, has_text_changes[0]
+
+
+tag_regex = re.compile(r'(<.*?>)')
+open_tag_regex = re.compile(r'<[^/]*?>')
+close_tag_regex = re.compile(r'</.*?>')
+self_closing_tag_regex = re.compile(r'<.*?/>')
+def wrap_html(html_section, start_tag, end_tag):
+    """
+    Wrap a section of html in a tag, respecting structure.
+
+    >>> wrap_html('<h1>simple</h1>', '<div>', '</div>')
+    '<h1><div>simple</div></h1>'
+    >>> wrap_html('outside<p>inside', '<b>', '</b>')
+    '<b>outside</b><p><b>inside</b>'
+    >>> wrap_html('inside</p>outside', '<b>', '</b>')
+    '<b>inside</b></p><b>outside</b>'
+    >>> wrap_html('one<em>two</em>three<em>four', '<b>', '</b>')
+    '<b>one</b><em><b>two</b></em><b>three</b><em><b>four</b>'
+    >>> wrap_html('line 1<br/>line 2', '<b>', '</b>')
+    '<b>line 1<br/>line 2</b>'
+    >>> wrap_html('<p>test', '<b>', '</b>')
+    '<p><b>test</b>'
+    >>> wrap_html('one<div>two<p>three', '<b>', '</b>')
+    '<b>one</b><div><b>two</b><p><b>three</b>'
+    >>> wrap_html('one</li><li>two</li><li>three', '<del>', '</del>')
+    '<del>one</del></li><li><del>two</del></li><li><del>three</del>'
+
+    Tests for regexes
+    >>> open = '<li>'
+    >>> close = '</li>'
+    >>> self_close = '<br />'
+    >>> tags = [open, close, self_close]
+    >>> [bool(tag_regex.match(t)) for t in tags]
+    [True, True, True]
+    >>> [bool(open_tag_regex.match(t)) for t in tags]
+    [True, False, False]
+    >>> [bool(close_tag_regex.match(t)) for t in tags]
+    [False, True, False]
+    >>> [bool(self_closing_tag_regex.match(t)) for t in tags]
+    [False, False, True]
+    """
+    split_html = tag_regex.split(html_section)
+    result_list = copy(split_html)
+    i = 0 # current position in the result list
+    result_list.insert(0, start_tag); i += 1 # beginning of section
+    for item in split_html:
+        if open_tag_regex.match(item) or close_tag_regex.match(item):
+            result_list.insert(i, end_tag); i += 1
+            result_list.insert(i + 1, start_tag); i += 1
+        i += 1
+    result_list.insert(len(result_list), end_tag); # end of section
+
+    result = ''.join(result_list)
+    result = result.replace(start_tag + end_tag, '') # remove empty wrapped tags
+    return result
+
+
+def has_text(html):
+    """
+    >>> has_text('<br />')
+    False
+    >>> has_text('<li>test</li>')
+    True
+    """
+    html = tag_regex.sub('', html)
+    return bool(html.strip())
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()

superwiki/markup.py

+# -*- coding: utf-8 -*-
+
+import re
+from django.conf import settings
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils.html import urlize
+from common import url_getter, URL_R
+from soup import html_links, strip_script_tags
+
+
+def markdown_processor(wiki_page):
+    import markdown
+    wiki_page.rendered_body = markdown.markdown(wiki_page.rendered_body)
+
+
+wikilink_re = re.compile('\[wiki:(%s)\s*([^\]]*)\]' % URL_R, re.UNICODE)
+
+def wikilinks_processor(wiki_page):
+    from views import get_wiki_page
+    bind = wiki_page.bind_to
+    linked_pages = []
+    def repl(m):
+        url, title = m.group(1, 2)
+        page_url = url_getter(bind, url)
+        exists = False
+        try:
+            page = get_wiki_page(bind, url=url)
+            exists = True
+            if page not in linked_pages:
+                linked_pages.append(page)
+        except ObjectDoesNotExist:
+            pass
+        if exists:
+            link = '<a href="%s">%s</a>' % (page_url, title or url)
+        else:
+            link = '<a class="missing" href="%s">%s</a>' % (page_url, title or url)
+        return link
+    
+    wiki_page.rendered_body = wikilink_re.sub(repl, wiki_page.rendered_body)
+    
+    # TODO should use delayed save if has no pk
+    if wiki_page.pk is not None:
+        wiki_page.linked_pages = linked_pages
+
+
+def autolinks_processor(wiki_page):
+    wiki_page.body = html_links(wiki_page.body)
+    wiki_page.rendered_body = html_links(wiki_page.rendered_body)
+
+
+def noscript_processor(wiki_page):
+    wiki_page.body = strip_script_tags(wiki_page.body)
+    wiki_page.rendered_body = strip_script_tags(wiki_page.rendered_body)

superwiki/migrations/0001_initial.py

+# -*- coding: utf-8 -*-
+
+from south.db import db
+from django.db import models
+from wiki.models import *
+
+class Migration:
+    
+    def forwards(self, orm):
+        
+        # Adding model 'WikiPage'
+        db.create_table('wiki', (
+            ('body', orm['wiki.WikiPage:body']),
+            ('author', orm['wiki.WikiPage:author']),
+            ('url', orm['wiki.WikiPage:url']),
+            ('rendered_body', orm['wiki.WikiPage:rendered_body']),
+            ('object_id', orm['wiki.WikiPage:object_id']),
+            ('created', orm['wiki.WikiPage:created']),
+            ('content_type', orm['wiki.WikiPage:content_type']),
+            ('id', orm['wiki.WikiPage:id']),
+        ))
+        db.send_create_signal('wiki', ['WikiPage'])
+        
+        # Creating unique_together for [object_id, content_type, url] on WikiPage.
+        db.create_unique('wiki', ['object_id', 'content_type_id', 'url'])
+        
+    
+    
+    def backwards(self, orm):
+        
+        # Deleting model 'WikiPage'
+        db.delete_table('wiki')
+        
+        # Deleting unique_together for [object_id, content_type, url] on WikiPage.
+        db.delete_unique('wiki', ['object_id', 'content_type_id', 'url'])
+        
+    
+    
+    models = {
+        'auth.user': {
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2009, 7, 7, 15, 58, 56, 72220)'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2009, 7, 7, 15, 58, 56, 72100)'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'auth.group': {
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'auth.permission': {
+            'Meta': {'unique_together': "(('content_type', 'codename'),)"},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'wiki.wikipage': {
+            'Meta': {'unique_together': "(('object_id', 'content_type', 'url'),)", 'db_table': "'wiki'"},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'body': ('django.db.models.fields.TextField', [], {}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'rendered_body': ('django.db.models.fields.TextField', [], {}),
+            'url': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+    
+    complete_apps = ['wiki']

superwiki/migrations/0002_add_rev.py

+# -*- coding: utf-8 -*-
+
+from south.db import db
+from django.db import models
+from wiki.models import *
+
+class Migration:
+    
+    def forwards(self, orm):
+        db.add_column('wiki', 'rev', orm['wiki.wikipage:rev'])
+    
+    def backwards(self, orm):
+        db.delete_column('wiki', 'rev')
+    
+    models = {
+        'auth.user': {
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2009, 7, 7, 16, 56, 43, 476880)'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2009, 7, 7, 16, 56, 43, 476762)'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'auth.group': {
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'auth.permission': {
+            'Meta': {'unique_together': "(('content_type', 'codename'),)"},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'wiki.wikipage': {
+            'Meta': {'unique_together': "(('object_id', 'content_type', 'url'),)", 'db_table': "'wiki'"},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'body': ('django.db.models.fields.TextField', [], {}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'rendered_body': ('django.db.models.fields.TextField', [], {}),
+            'rev': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'url': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+    
+    complete_apps = ['wiki']

superwiki/migrations/0003_add_title.py

+# -*- coding: utf-8 -*-
+
+from south.db import db
+from django.db import models
+from wiki.models import *
+
+class Migration:
+    
+    def forwards(self, orm):
+        
+        # Adding field 'WikiPage.title'
+        db.add_column('wiki', 'title', orm['wiki.wikipage:title'])
+        
+    
+    
+    def backwards(self, orm):
+        
+        # Deleting field 'WikiPage.title'
+        db.delete_column('wiki', 'title')
+        
+    
+    
+    models = {
+        'auth.user': {
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2009, 7, 8, 12, 15, 39, 315343)'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2009, 7, 8, 12, 15, 39, 315224)'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'auth.permission': {
+            'Meta': {'unique_together': "(('content_type', 'codename'),)"},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'auth.group': {
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'})
+        },
+        'wiki.wikipage': {
+            'Meta': {'unique_together': "(('object_id', 'content_type', 'url'),)", 'db_table': "'wiki'"},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'body': ('django.db.models.fields.TextField', [], {}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'rendered_body': ('django.db.models.fields.TextField', [], {}),
+            'rev': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True', 'null': 'True'}),
+            'url': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+    
+    complete_apps = ['wiki']

superwiki/migrations/0004_add_linked_pages.py

+# -*- coding: utf-8 -*-
+
+from south.db import db
+from django.db import models
+from wiki.models import *
+
+class Migration:
+    
+    def forwards(self, orm):
+        
+        # Adding ManyToManyField 'WikiPage.linked_pages'
+        db.create_table('wiki_linked_pages', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('from_wikipage', models.ForeignKey(orm.WikiPage, null=False)),
+            ('to_wikipage', models.ForeignKey(orm.WikiPage, null=False))
+        ))
+        
+    
+    
+    def backwards(self, orm):
+        
+        # Dropping ManyToManyField 'WikiPage.linked_pages'
+        db.delete_table('wiki_linked_pages')
+        
+    
+    
+    models = {
+        'auth.user': {
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2009, 7, 9, 16, 25, 11, 139897)'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2009, 7, 9, 16, 25, 11, 139775)'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'auth.group': {
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'auth.permission': {
+            'Meta': {'unique_together': "(('content_type', 'codename'),)"},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'wiki.wikipage': {
+            'Meta': {'unique_together': "(('object_id', 'content_type', 'url'),)", 'db_table': "'wiki'"},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'body': ('django.db.models.fields.TextField', [], {}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'linked_pages': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['wiki.WikiPage']", 'null': 'True', 'blank': 'True'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'rendered_body': ('django.db.models.fields.TextField', [], {}),
+            'rev': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+            'url': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+    
+    complete_apps = ['wiki']

superwiki/migrations/__init__.py

Empty file added.

superwiki/models.py

+# -*- coding: utf-8 -*-
+
+from django.db import models
+from django.utils.translation import ugettext as _
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+from django.conf import settings
+import reversion
+
+from common import markup_processors
+
+
+class WikiPage(models.Model):
+    
+    content_type = models.ForeignKey(ContentType)
+    object_id = models.PositiveIntegerField()
+    bind_to = generic.GenericForeignKey('content_type', 'object_id')
+    url = models.CharField(_('url'), max_length=100)
+    title = models.CharField(_('title'), max_length=100, null=True, blank=True)
+    author = models.ForeignKey(User, verbose_name=_('author'))
+    body = models.TextField(_('body'))
+    rendered_body = models.TextField(_('rendered body'))
+    created = models.DateTimeField(_('created'), auto_now_add=True)
+    rev = models.PositiveIntegerField(_('revision'), default=0)
+    linked_pages = models.ManyToManyField('self', null=True, blank=True, symmetrical=False)
+    
+    def save(self, *args, **kwargs):
+        self.rendered_body = self.body
+        if not self.rev:
+            self.rev = 0
+        self.rev += 1
+        markup_type = getattr(self.bind_to, 'markup_type', settings.MARKUP_TYPES[0][0])
+        for markup_processor in markup_processors[markup_type]:
+            markup_processor(self)
+        super(WikiPage, self).save(*args, **kwargs)
+    
+    def __unicode__(self):
+        return u'%s - %s' % (self.bind_to, self.url)
+    
+    class Meta:
+        db_table = 'wiki'
+        unique_together = ('object_id', 'content_type', 'url')
+
+
+reversion.register(WikiPage)

superwiki/signals.py

+# -*- coding: utf-8 -*-
+
+from django import dispatch
+
+from models import WikiPage
+
+
+wikipage_changed = dispatch.Signal(providing_args=['page', 'created', 'old_data', 'new_data'])

superwiki/soup.py

+# -*- coding: utf-8 -*-
+
+from BeautifulSoup import BeautifulSoup, Tag, NavigableString
+import re
+
+__all__ = ('get_text_content', 'html_links', 'strip_script_tags')
+
+
+def get_text_content(html):
+    
+    def _get_node_content(node):
+        content = []
+        for child in node.childGenerator():
+            if isinstance(child, NavigableString):
+                content.append(unicode(child))
+            elif isinstance(child, Tag):
+                content += _get_node_content(child)
+        return content
+    
+    soup = BeautifulSoup(html)
+    content = u''.join(_get_node_content(soup))
+    content = re.sub(r'\s+', ' ', content, re.UNICODE)
+    return content.strip()
+
+
+url_re = re.compile(r'((?:http|https|ftp)://(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|(?:[A-Z0-9]+(?:-*[A-Z0-9]+)*\.)+[A-Z]{2,6})(?::\d+)?/?[^\s|"|\'|<]*)', re.IGNORECASE)
+
+def html_links(html):
+    
+    def _html_links(node):
+        for child in node.childGenerator():
+            if isinstance(child, NavigableString):
+                urls = url_re.findall(unicode(child))
+                if not urls:
+                    continue
+                new = unicode(child)
+                for url in urls:
+                    new = new.replace(url, u'<a href="%s">%s</a>' % (url, url))
+                child.replaceWith(new)
+            elif isinstance(child, Tag):
+                if child.name !='a':
+                    _html_links(child)
+    
+    soup = BeautifulSoup(html)
+    _html_links(soup)
+    return unicode(soup)
+
+
+def strip_script_tags(html):
+    
+    def _strip_tags(node):
+        for child in node.childGenerator():
+            if isinstance(child, Tag):
+                if child.name == 'script':
+                    child.replaceWith('')
+                else:
+                    _strip_tags(child)
+    
+    soup  = BeautifulSoup(html)
+    _strip_tags(soup)
+    return unicode(soup)

superwiki/templates/wiki/create.html

+{% extends "wiki/layout.html" %}
+
+{% load wiki_extra %}
+
+{% block wiki_head %}
+  create "{{ url }}"
+{% endblock %}
+
+{% block wiki_toolbar %}
+{% endblock %}
+
+{% block wiki_content %}
+  {% include "wiki/editor_start.html" %}
+  <form method="post" class="wiki-edit">
+    {{ frm.as_p }}
+    <input type="submit" class="hide" value="create" />
+    <a href="#submit" class="button positive" onclick="$(this).parent().submit(); return false;">Create</a>
+  </form>
+{% endblock %}

superwiki/templates/wiki/delete.html

+{% extends "wiki/layout.html" %}
+
+{% load wiki_extra %}
+
+{% block wiki_head %}
+  delete "{% if page.title %}{{ page.title }}{% else %}{{ page.url }}{% endif %}"
+{% endblock %}
+
+{% block wiki_toolbar %}
+
+{% endblock %}
+
+{% block wiki_content %}
+  <form method="post">
+    <input type="hidden" name="confirm" value="confirm" />
+    <input type="submit" class="hide" />
+    <p>Delete page "{% if page.title %}{{ page.title }}{% else %}{{ page.url }}{% endif %}"?</p>
+    <a href="{% wiki_reverse bind page.url %}" class="button">No, thanks</a>
+    <a href="#delete" onclick="$(this).parent().submit();" class="button negative">Delete</a>
+  </form>
+{% endblock %}

superwiki/templates/wiki/diff-inline.html

+{{ diff|safe }}

superwiki/templates/wiki/diff2.html

+<html>
+  <head>
+    <style type="text/css" media="all">
+      ins { background: #afa; }
+      del { background: #faa; }
+    </style>
+  </head>
+  <body>
+    {{ diff|safe }}
+  </body>
+</html>

superwiki/templates/wiki/edit.html

+{% extends "wiki/layout.html" %}
+{% load wiki_extra %}
+
+{% block wiki_head %}
+  edit "{% if page.title %}{{ page.title }}{% else %}{{ page.url }}{% endif %}"
+{% endblock %}
+
+{% block wiki_toolbar %}
+{% endblock %}
+
+{% block wiki_content %}
+  {% include "wiki/editor_start.html" %}
+  <form method="post" class="wiki-edit">
+    {{ frm.as_p }}
+    <input type="submit" class="hide" value="create" />
+    <a href="#submit" class="button positive" onclick="$(this).parent().submit(); return false;">Save</a>
+    <a href="{% wiki_reverse bind page.url %}" class="button negative" onclick="if (! confirm('All changes will be lost!')) return false;">Cancel</a>
+  </form>
+{% endblock %}

superwiki/templates/wiki/editor_start.html

+{% ifequal bind.markup_type 'wysiwyg' %}
+<script type="text/javascript">
+    setTimeout(function() {
+        tinyMCE.init({
+            mode: 'exact',
+            elements: 'id_body',
+            theme: 'advanced',
+            theme_advanced_buttons1 : "save,bold,italic,underline,strikethrough," +
+                                      "bullist,numlist,|,outdent,indent," +
+                                      "justifyleft,justifycenter,justifyright,|," +
+                                      "undo,redo,|,link,image,hr",
+            theme_advanced_buttons2 : "forecolor,backcolor,formatselect,fontselect,fontsizeselect,cleanup,code",
+            theme_advanced_buttons3 : '',
+            theme_advanced_buttons4 : '',
+            theme_advanced_toolbar_location : "top",
+            theme_advanced_toolbar_align : "left",
+			      auto_focus : 'id_body',
+			      content_css: '{{ MEDIA_URL }}css/wiki-tinymce-content.css'
+        });
+    }, 500); 
+</script>
+{% endifequal %}

superwiki/templates/wiki/history.html

+{% extends "wiki/layout.html" %}
+{% load wiki_extra %}
+
+{% block wiki_head %}
+  history of "{% if page.title %}{{ page.title }}{% else %}{{ page.url }}{% endif %}"
+{% endblock %}
+
+{% block wiki_toolbar %}
+{% endblock %}
+
+{% block wiki_content %}
+  <style type="text/css" media="all">
+    ins { background: #afa; }
+    del { background: #faa; }
+  </style>
+  <script type="text/javascript">
+    jQuery(function($) {
+      var _a, _b;
+      var url = '{% wiki_reverse bind url %}';
+      $('#diff-submit').click(function(){
+        window.open(url + ':diff:' + _a + ':' + _b);
+      });
+      function show_diff_btn() {
+        $('#diff-submit').css('visibility', (_a == _b || !_a || !_b) ? 'hidden' : 'visible');
+      }
+      $('input[name=diff-a]').change(function(){
+        _a = this.value;
+        show_diff_btn();
+      });
+      $('input[name=diff-b]').change(function(){
+        _b = this.value;
+        show_diff_btn();
+      });
+    });
+  </script>
+  <table class="wiki-history">
+    <tr>
+      <th>a</th>
+      <th>b</th>
+      <th>
+        <input type="button" id="diff-submit" value="diff" />
+      </th>
+    </tr>
+    {% for version in versions %}
+      <tr>
+        <td>
+          <input type="radio" name="diff-a" value="{{ version.pk }}" />
+        </td>
+        <td>
+          <input type="radio" name="diff-b" value="{{ version.pk }}" />
+        </td>
+        <td class="revision">
+          <span class="timestamp">{{ version.revision.date_created|date:"Y-m-d"}} {{ version.revision.date_created|time:"H:i" }}</span>
+          by <a class="user" href="#">{{ version.revision.user }}</a>
+          {% ifchanged version.object_version.object.title %}
+            <div class="title-changed">title changed to "{{ version.object_version.object.title }}"</div>
+          {% endifchanged %}
+          {% ifchanged version.object_version.object.body %}
+            <div class="body-changed">
+              body changed <a href="{% wiki_reverse bind url %}:diff:{{ version.pk }}" target="diff" onclick="$('.wiki-text').empty(); $('#diff-{{ version.pk }}').load('{% wiki_reverse bind url %}:diff:{{ version.pk }}'); return false;">diff</a>
+            </div>
+            <div class="wiki-text" id="diff-{{ version.pk }}"></div>
+          {% endifchanged %}
+        </td>
+      </tr>
+    {% endfor %}
+  </table>
+{% endblock %}

superwiki/templates/wiki/index.html

+{% load wiki_extra %}
+
+{% wiki_index bind %}

superwiki/templates/wiki/show.html

+{% extends "wiki/layout.html" %}
+
+{% load wiki_extra %}
+
+{% block wiki_head %}
+  {% if exists %}
+    {% if page.title %}{{ page.title }}{% else %}{{ page.url }}{% endif %}
+  {% else %}
+    not found
+  {% endif %}
+{% endblock %}
+
+{% block wiki_toolbar %}
+  {% if exists %}
+    {% if edit_allowed %}
+    <div style="overflow: hidden;">
+      <a class="button" href="{% wiki_reverse bind url %}:edit">Edit</a>
+      <a class="button negative" href="{% wiki_reverse bind url %}:delete">Delete</a>
+    </div>
+    {% endif %}
+    <div><a href="{% wiki_reverse bind url %}:history">History</a><br/><br/></div>
+  {% endif %}
+{% endblock %}
+
+{% block wiki_content %}
+  {% if exists %}
+    <div class="wiki-text">
+      {{ page.rendered_body|safe }}
+    </div>
+  {% else %}
+    <div class="wiki-not-found">
+      wiki page with url "{{ url }}" doesn't exist
+      {% if edit_allowed %}
+        <br/>
+        <a class="button positive" href="{% wiki_reverse bind url %}:create">create</a>
+      {% endif %}
+    </div>
+  {% endif %}
+{% endblock %}

superwiki/templates/wiki/tags/index.html

+{% load wiki_extra %}
+
+<ul>
+{% for page in roots %}
+  {% wiki_tree_item page bind %}
+{% endfor %}
+</ul>

superwiki/templates/wiki/tags/index_tree_item.html

+{% load wiki_extra %}
+
+<li>
+  <a href="{% wiki_reverse bind page.url %}">
+    {% if page.title %}{{ page.title }}{% else %}{{ page.url }}{% endif %}
+  </a>
+  {% if page.childnodes %}
+    <ul>
+      {% for subpage in page.childnodes %}
+        {% wiki_tree_item subpage bind %}
+      {% endfor %}
+    </ul>
+  {% endif %}
+</li>

superwiki/templatetags/__init__.py

Empty file added.

superwiki/templatetags/wiki_extra.py

+# -*- coding: utf-8 -*-
+
+from django import template
+from django.conf import settings
+from superwiki.common import url_getter
+from superwiki.views import filter_wiki_pages
+
+register = template.Library()
+
+
+@register.simple_tag
+def wiki_reverse(bind, url):
+    return url_getter(bind, url)
+
+
+@register.inclusion_tag('wiki/tags/index.html')
+def wiki_index(bind):
+    pages = sorted(filter_wiki_pages(bind), key=lambda page: len(page.url))
+    roots = []
+    for page in pages:
+        for possible_parent in reversed(pages):
+            if possible_parent == page:
+                continue
+            if page.url.startswith(possible_parent.url):
+                if len(page.url) > len(possible_parent.url):
+                    if page.url[len(possible_parent.url)] != '/':
+                        continue
+                    if not hasattr(possible_parent, 'childnodes'):
+                        setattr(possible_parent, 'childnodes', [])
+                    getattr(possible_parent, 'childnodes').append(page)
+                    break
+        else:
+            roots.append(page)
+    return {'roots': roots, 'bind': bind}
+
+
+@register.inclusion_tag('wiki/tags/index_tree_item.html')
+def wiki_tree_item(page, bind):
+    return {'page': page, 'bind': bind}

superwiki/urls.py

+# -*- coding: utf-8 -*-
+
+from django.conf.urls.defaults import *
+
+import views
+from common import URL_R
+
+
+urlpatterns = patterns('',
+    url('^:index$', views.index, name='wiki-index'),
+    url('^(?P<url>%s)$' % URL_R, views.show, name='wiki-show'),
+    url('^(?P<url>%s):edit$' % URL_R, views.edit, name='wiki-edit'),
+    url('^(?P<url>%s):create$' % URL_R, views.create, name='wiki-create'),
+    url('^(?P<url>%s):delete$' % URL_R, views.delete, name='wiki-delete'),
+    url('^(?P<url>%s):history$' % URL_R, views.history, name='wiki-history'),
+    url('^(?P<url>%s):diff:(?P<ver>\d+)$' % URL_R, views.diff, name='wiki-diff'),
+    url('^(?P<url>%s):diff:(?P<ver>\d+):(?P<ver_b>\d+)$' % URL_R, views.diff, name='wiki-diff2'),
+)

superwiki/views.py

+# -*- coding: utf-8 -*-
+
+from django.conf import settings
+from django.shortcuts import redirect, get_object_or_404, render_to_response
+from django.core.exceptions import ObjectDoesNotExist
+from django.http import Http404, HttpResponseForbidden, HttpResponse
+from django.utils.translation import ugettext as _
+from django.template import RequestContext
+
+from annoying.decorators import render_to
+from reversion.models import Version
+from notify import notify_user
+
+from common import bind_getter, url_getter, write_perm_check
+from models import WikiPage, ContentType
+from forms import WikiPageForm
+import signals
+from html_diff import html_diff
+
+
+def wiki_bind_getter(fn):
+    def wrap(request, *args, **kwargs):
+        if 'url' in kwargs:
+            if kwargs['url'].endswith('/'):
+                return redirect(request.path.rstrip('/'))
+        bind, kw = bind_getter(request, *args, **kwargs)
+        return fn(request, bind, **kw)
+    return wrap
+
+
+def get_wiki_page(bind, **kwargs):
+    bind_content_type = ContentType.objects.get_for_model(type(bind))
+    if 'url' in kwargs:
+        kwargs['url'] = kwargs['url'].lower()
+    return WikiPage.objects.get(content_type=bind_content_type,
+                                object_id=bind.pk, **kwargs)
+
+
+def filter_wiki_pages(bind, **kwargs):
+    bind_content_type = ContentType.objects.get_for_model(type(bind))
+    return WikiPage.objects.filter(content_type=bind_content_type,
+                                   object_id=bind.pk, **kwargs)
+
+
+def require_write_perm(fn):
+    def wrap(request, bind, **kwargs):
+        if not write_perm_check(request, bind):
+            return HttpResponseForbidden('forbidden')
+        return fn(request, bind, **kwargs)
+    return wrap
+
+
+@render_to('wiki/show.html')
+@wiki_bind_getter
+def show(request, bind, url):
+    exists = False
+    try:
+        page = get_wiki_page(bind, url=url)
+        exists = True
+    except ObjectDoesNotExist:
+        page = None
+    versions = []
+    if exists:
+        versions = Version.objects.get_for_object(page).order_by('-pk')[:5]
+    edit_allowed = write_perm_check(request, bind)
+    return {'page': page, 'versions': versions, 'exists': exists, 'url': url,
+            'bind': bind, 'edit_allowed': edit_allowed}
+
+
+@render_to('wiki/index.html')
+@wiki_bind_getter
+def index(request, bind):
+    return {'bind': bind}
+
+
+@render_to('wiki/edit.html')
+@wiki_bind_getter
+@require_write_perm
+def edit(request, bind, url):
+    try:
+        page = get_wiki_page(bind, url=url)
+    except ObjectDoesNotExist:
+        raise Http404, 'page not found'
+    if request.method == 'POST':
+        frm = WikiPageForm(request.POST, instance=page)
+        body, rendered_body, title = page.body, page.rendered_body, page.title
+        if frm.is_valid():
+            if title != frm.cleaned_data['title'] or body != frm.cleaned_data['body']:
+                frm.save()
+                signals.wikipage_changed.send(
+                                      sender=request.user,
+                                      page=page,
+                                      created=False,
+                                      old_data=dict(
+                                        rendered_body=rendered_body,
+                                        title=title),
+                                      new_data=dict(
+                                        rendered_body=page.rendered_body,
+                                        title=page.title
+                                      ))
+                msg = _('Page saved')
+            else:
+                msg = _('Nothing changed')
+            return notify_user.success(request, url_getter(bind, url), msg)
+
+    else:
+        frm = WikiPageForm(instance=page)
+    return {'page': page, 'bind': bind, 'frm': frm}
+
+
+@render_to('wiki/create.html')
+@wiki_bind_getter
+@require_write_perm
+def create(request, bind, url):
+    if request.method == 'POST':
+        frm = WikiPageForm(request.POST)
+        if frm.is_valid():
+            page = frm.save(commit=False)
+            page.author = request.user
+            page.bind_to = bind
+            page.url = url.lower()
+            page.save()
+            signals.wikipage_changed.send(
+                                  sender=request.user,
+                                  page=page,
+                                  created=True,
+                                  old_data=None,
+                                  new_data=dict(
+                                    rendered_body=page.rendered_body,
+                                    title=page.title
+                                  ))
+            return notify_user.success(request, url_getter(bind, url),
+                                       _('Page successfully created'))
+    else:
+        frm = WikiPageForm()
+    return {'frm': frm, 'url': url, 'bind': bind}
+
+
+@render_to('wiki/delete.html')
+@wiki_bind_getter
+def delete(request, bind, url):
+    try:
+        page = get_wiki_page(bind, url=url)
+    except ObjectDoesNotExist:
+        raise Http404, 'page not found'
+    if request.method == 'POST' and 'confirm' in request.POST:
+        page.delete()
+        return notify_user.success(request, url_getter(bind, 'home'),
+                                   _('Page successfully deleted'))
+    return {'page': page, 'bind': bind}
+
+
+@render_to('wiki/history.html')
+@wiki_bind_getter
+def history(request, bind, url):
+    try:
+        page = get_wiki_page(bind, url=url)
+    except ObjectDoesNotExist:
+        raise Http404, 'page not found'
+    versions = Version.objects.get_for_object(page).order_by('pk')
+    return {'page': page, 'versions': versions, 'url': url, 'bind': bind}
+
+
+@wiki_bind_getter
+def diff(request, bind, url, ver, ver_b=None):
+    try:
+        page = get_wiki_page(bind, url=url)
+    except ObjectDoesNotExist:
+        raise Http404, 'page not found'
+    if not ver_b:
+        versions = list(Version.objects.get_for_object(page).filter(pk=ver))
+        if not versions:
+            raise Http404, 'version not found'
+        version = versions[0]
+        previous_version = None
+        pk_lt = version.pk
+        while not previous_version:
+            v = list(Version.objects.get_for_object(page).filter(pk__lt=pk_lt).order_by('-pk')[:1])
+            if not v:
+                break
+            pk_lt = v[0].pk
+            if v[0].object_version.object.rendered_body != version.object_version.object.rendered_body:
+                previous_version = v[0]
+        template = 'wiki/diff-inline.html'
+    else:
+        try:
+            previous_version = Version.objects.get_for_object(page).get(pk=ver)
+            version =  Version.objects.get_for_object(page).get(pk=ver_b)
+        except ObjectDoesNotExist:
+            raise Http404, 'version not found'
+        template = 'wiki/diff2.html'
+    if not previous_version:
+        diff, text_changes = _('There is no previous version with different body'), True
+    else:
+        diff, text_changes = html_diff(previous_version.object_version.object.rendered_body, version.object_version.object.rendered_body)
+        if not text_changes:
+            diff = _('Only formatting differences')
+    
+    return render_to_response(template, RequestContext(request, {'diff': diff}))