From 646fc0b18f86c40d2524497e737e8917250d7534 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Fri, 30 Dec 2011 00:09:16 -0500 Subject: [PATCH] simplewiki --- settings.py | 3 +- simplewiki/__init__.py | 6 + simplewiki/admin.py | 59 ++ simplewiki/mdx_camelcase.py | 136 +++ simplewiki/mdx_image.py | 61 ++ simplewiki/mdx_mathjax.py | 21 + simplewiki/mdx_video.py | 271 +++++ .../media/css/autosuggest_inquisitor.css | 177 ++++ simplewiki/media/css/base.css | 281 +++++ simplewiki/media/css/base_print.css | 6 + .../css/img_inquisitor/_source/as_pointer.png | Bin 0 -> 27142 bytes .../css/img_inquisitor/_source/li_corner.png | Bin 0 -> 28442 bytes .../css/img_inquisitor/_source/ul_corner.png | Bin 0 -> 27649 bytes .../media/css/img_inquisitor/as_pointer.gif | Bin 0 -> 66 bytes .../media/css/img_inquisitor/hl_corner_bl.gif | Bin 0 -> 73 bytes .../media/css/img_inquisitor/hl_corner_br.gif | Bin 0 -> 73 bytes .../media/css/img_inquisitor/hl_corner_tl.gif | Bin 0 -> 73 bytes .../media/css/img_inquisitor/hl_corner_tr.gif | Bin 0 -> 73 bytes .../media/css/img_inquisitor/ul_corner_bl.gif | Bin 0 -> 49 bytes .../media/css/img_inquisitor/ul_corner_br.gif | Bin 0 -> 49 bytes .../media/css/img_inquisitor/ul_corner_tl.gif | Bin 0 -> 50 bytes .../media/css/img_inquisitor/ul_corner_tr.gif | Bin 0 -> 50 bytes simplewiki/media/img/box_corner_bl.gif | Bin 0 -> 49 bytes simplewiki/media/img/box_corner_br.gif | Bin 0 -> 49 bytes simplewiki/media/img/box_corner_tl.gif | Bin 0 -> 50 bytes simplewiki/media/img/box_corner_tr.gif | Bin 0 -> 50 bytes simplewiki/media/img/delete.gif | Bin 0 -> 130 bytes simplewiki/media/img/delete_grey.gif | Bin 0 -> 130 bytes simplewiki/media/js/bsn.AutoSuggest_c_2.0.js | 961 ++++++++++++++++++ simplewiki/models.py | 335 ++++++ simplewiki/settings.py | 111 ++ simplewiki/templates/simplewiki_base.html | 197 ++++ simplewiki/templates/simplewiki_create.html | 16 + simplewiki/templates/simplewiki_edit.html | 17 + simplewiki/templates/simplewiki_error.html | 98 ++ simplewiki/templates/simplewiki_history.html | 57 ++ .../templates/simplewiki_searchresults.html | 21 + .../simplewiki_updateprogressbar.html | 32 + simplewiki/templates/simplewiki_view.html | 10 + simplewiki/templatetags/__init__.py | 0 simplewiki/templatetags/simplewiki_utils.py | 17 + simplewiki/tests.py | 23 + simplewiki/urls.py | 18 + simplewiki/usage.txt | 800 +++++++++++++++ simplewiki/views.py | 400 ++++++++ simplewiki/views_attachments.py | 145 +++ urls.py | 1 + 47 files changed, 4279 insertions(+), 1 deletion(-) create mode 100644 simplewiki/__init__.py create mode 100644 simplewiki/admin.py create mode 100644 simplewiki/mdx_camelcase.py create mode 100644 simplewiki/mdx_image.py create mode 100644 simplewiki/mdx_mathjax.py create mode 100644 simplewiki/mdx_video.py create mode 100644 simplewiki/media/css/autosuggest_inquisitor.css create mode 100644 simplewiki/media/css/base.css create mode 100644 simplewiki/media/css/base_print.css create mode 100644 simplewiki/media/css/img_inquisitor/_source/as_pointer.png create mode 100644 simplewiki/media/css/img_inquisitor/_source/li_corner.png create mode 100644 simplewiki/media/css/img_inquisitor/_source/ul_corner.png create mode 100644 simplewiki/media/css/img_inquisitor/as_pointer.gif create mode 100644 simplewiki/media/css/img_inquisitor/hl_corner_bl.gif create mode 100644 simplewiki/media/css/img_inquisitor/hl_corner_br.gif create mode 100644 simplewiki/media/css/img_inquisitor/hl_corner_tl.gif create mode 100644 simplewiki/media/css/img_inquisitor/hl_corner_tr.gif create mode 100644 simplewiki/media/css/img_inquisitor/ul_corner_bl.gif create mode 100644 simplewiki/media/css/img_inquisitor/ul_corner_br.gif create mode 100644 simplewiki/media/css/img_inquisitor/ul_corner_tl.gif create mode 100644 simplewiki/media/css/img_inquisitor/ul_corner_tr.gif create mode 100644 simplewiki/media/img/box_corner_bl.gif create mode 100644 simplewiki/media/img/box_corner_br.gif create mode 100644 simplewiki/media/img/box_corner_tl.gif create mode 100644 simplewiki/media/img/box_corner_tr.gif create mode 100644 simplewiki/media/img/delete.gif create mode 100644 simplewiki/media/img/delete_grey.gif create mode 100644 simplewiki/media/js/bsn.AutoSuggest_c_2.0.js create mode 100644 simplewiki/models.py create mode 100644 simplewiki/settings.py create mode 100644 simplewiki/templates/simplewiki_base.html create mode 100644 simplewiki/templates/simplewiki_create.html create mode 100644 simplewiki/templates/simplewiki_edit.html create mode 100644 simplewiki/templates/simplewiki_error.html create mode 100644 simplewiki/templates/simplewiki_history.html create mode 100644 simplewiki/templates/simplewiki_searchresults.html create mode 100644 simplewiki/templates/simplewiki_updateprogressbar.html create mode 100644 simplewiki/templates/simplewiki_view.html create mode 100644 simplewiki/templatetags/__init__.py create mode 100644 simplewiki/templatetags/simplewiki_utils.py create mode 100644 simplewiki/tests.py create mode 100644 simplewiki/urls.py create mode 100644 simplewiki/usage.txt create mode 100644 simplewiki/views.py create mode 100644 simplewiki/views_attachments.py diff --git a/settings.py b/settings.py index 2de81fe2be..46c0d3e04e 100644 --- a/settings.py +++ b/settings.py @@ -92,13 +92,14 @@ INSTALLED_APPS = ( 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', - 'django.contrib.admin', +# 'django.contrib.admin', 'courseware', 'auth', 'django.contrib.humanize', 'static_template_view', 'textbook', 'staticbook', + 'simplewiki', # Uncomment the next line to enable the admin: # 'django.contrib.admin', # Uncomment the next line to enable admin documentation: diff --git a/simplewiki/__init__.py b/simplewiki/__init__.py new file mode 100644 index 0000000000..cdbd556dad --- /dev/null +++ b/simplewiki/__init__.py @@ -0,0 +1,6 @@ +import sys, os + +# allow mdx_* parsers to be just dropped in the simplewiki folder +module_path = os.path.abspath(os.path.dirname(__file__)) +if module_path not in sys.path: + sys.path.append(module_path) diff --git a/simplewiki/admin.py b/simplewiki/admin.py new file mode 100644 index 0000000000..573da56d6d --- /dev/null +++ b/simplewiki/admin.py @@ -0,0 +1,59 @@ +from django.contrib import admin +from django import forms +from django.utils.translation import ugettext as _ +from models import Article, Revision, Permission, ArticleAttachment + +class RevisionInline(admin.TabularInline): + model = Revision + extra = 1 + +class RevisionAdmin(admin.ModelAdmin): + list_display = ('article', '__unicode__', 'revision_date', 'revision_user', 'revision_text') + search_fields = ('article', 'counter') + +class AttachmentAdmin(admin.ModelAdmin): + list_display = ('article', '__unicode__', 'uploaded_on', 'uploaded_by') + +class ArticleAdminForm(forms.ModelForm): + def clean(self): + cleaned_data = self.cleaned_data + if cleaned_data.get("slug").startswith('_'): + raise forms.ValidationError(_('Slug cannot start with _ character.' + 'Reserved for internal use.')) + if not self.instance.pk: + parent = cleaned_data.get("parent") + slug = cleaned_data.get("slug") + if Article.objects.filter(slug__exact=slug, parent__exact=parent): + raise forms.ValidationError(_('Article slug and parent must be ' + 'unique together.')) + return cleaned_data + class Meta: + model = Article + +class ArticleAdmin(admin.ModelAdmin): + list_display = ('created_by', 'slug', 'modified_on', 'parent') + search_fields = ('slug',) + prepopulated_fields = {'slug': ('title',) } + inlines = [RevisionInline] + form = ArticleAdminForm + save_on_top = True + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == 'current_revision': + # Try to determine the id of the article being edited + id = request.path.split('/') + import re + if len(id) > 0 and re.match(r"\d+", id[-2]): + kwargs["queryset"] = Revision.objects.filter(article=id[-2]) + return db_field.formfield(**kwargs) + else: + db_field.editable = False + return db_field.formfield(**kwargs) + return super(ArticleAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) + +class PermissionAdmin(admin.ModelAdmin): + search_fields = ('article', 'counter') + +admin.site.register(Article, ArticleAdmin) +admin.site.register(Revision, RevisionAdmin) +admin.site.register(Permission, PermissionAdmin) +admin.site.register(ArticleAttachment, AttachmentAdmin) \ No newline at end of file diff --git a/simplewiki/mdx_camelcase.py b/simplewiki/mdx_camelcase.py new file mode 100644 index 0000000000..1b979520ab --- /dev/null +++ b/simplewiki/mdx_camelcase.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python + +''' +WikiLink Extention for Python-Markdown +====================================== + +Converts CamelCase words to relative links. Requires Python-Markdown 1.6+ + +Basic usage: + + >>> import markdown + >>> text = "Some text with a WikiLink." + >>> md = markdown.markdown(text, ['wikilink']) + >>> md + '\\n

Some text with a WikiLink.\\n

\\n\\n\\n' + +To define custom settings the simple way: + + >>> md = markdown.markdown(text, + ... ['wikilink(base_url=/wiki/,end_url=.html,html_class=foo)'] + ... ) + >>> md + '\\n

Some text with a WikiLink.\\n

\\n\\n\\n' + +Custom settings the complex way: + + >>> md = markdown.Markdown(text, + ... extensions = ['wikilink'], + ... extension_configs = {'wikilink': [ + ... ('base_url', 'http://example.com/'), + ... ('end_url', '.html'), + ... ('html_class', '') ]}, + ... encoding='utf8', + ... safe_mode = True) + >>> str(md) + '\\n

Some text with a WikiLink.\\n

\\n\\n\\n' + +Use MetaData with mdx_meta.py (Note the blank html_class in MetaData): + + >>> text = """wiki_base_url: http://example.com/ + ... wiki_end_url: .html + ... wiki_html_class: + ... + ... Some text with a WikiLink.""" + >>> md = markdown.Markdown(text, ['meta', 'wikilink']) + >>> str(md) + '\\n

Some text with a WikiLink.\\n

\\n\\n\\n' + +From the command line: + + python markdown.py -x wikilink(base_url=http://example.com/,end_url=.html,html_class=foo) src.txt + +By [Waylan Limberg](http://achinghead.com/). + +Project website: http://achinghead.com/markdown-wikilinks/ +Contact: waylan [at] gmail [dot] com + +License: [BSD](http://www.opensource.org/licenses/bsd-license.php) + +Version: 0.4 (Oct 14, 2006) + +Dependencies: +* [Python 2.3+](http://python.org) +* [Markdown 1.6+](http://www.freewisdom.org/projects/python-markdown/) +* For older dependencies use [WikiLink Version 0.3] +(http://code.limberg.name/svn/projects/py-markdown-ext/wikilinks/tags/release-0.3/) +''' + +import markdown + +class CamelCaseExtension(markdown.Extension): + def __init__(self, configs): + # set extension defaults + self.config = { + 'base_url' : ['/', 'String to append to beginning or URL.'], + 'end_url' : ['/', 'String to append to end of URL.'], + 'html_class' : ['wikilink', 'CSS hook. Leave blank for none.'] + } + + # Override defaults with user settings + for key, value in configs : + # self.config[key][0] = value + self.setConfig(key, value) + + def add_inline(self, md, name, klass, re): + pattern = klass(re) + pattern.md = md + pattern.ext = self + md.inlinePatterns.add(name, pattern, "\\|\b)(?P([A-Z]+[a-z-_]+){2,})(?:"")?\b''') + +class CamelCaseLinks(markdown.inlinepatterns.Pattern): + def handleMatch(self, m) : + if m.group('escape') == '\\': + a = markdown.etree.Element('a')#doc.createTextNode(m.group('camelcase')) + else : + url = m.group('camelcase') + #'%s%s%s'% (self.md.wiki_config['base_url'][0], \ + #m.group('camelcase'), \ + #self.md.wiki_config['end_url'][0]) + label = m.group('camelcase').replace('_', ' ') + a = markdown.etree.Element('a') + a.set('href', url) + a.text = label + a.set('class', 'wikilink') + return a + +class CamelCasePreprocessor(markdown.preprocessors.Preprocessor) : + + def run(self, lines) : + ''' + Updates WikiLink Extension configs with Meta Data. + Passes "lines" through unchanged. + + Run as a preprocessor because must run after the + MetaPreprocessor runs and only needs to run once. + ''' + if hasattr(self.md, 'Meta'): + if self.md.Meta.has_key('wiki_base_url'): + self.md.wiki_config['base_url'][0] = self.md.Meta['wiki_base_url'][0] + if self.md.Meta.has_key('wiki_end_url'): + self.md.wiki_config['end_url'][0] = self.md.Meta['wiki_end_url'][0] + if self.md.Meta.has_key('wiki_html_class'): + self.md.wiki_config['html_class'][0] = self.md.Meta['wiki_html_class'][0] + + return lines + +def makeExtension(configs=None) : + return CamelCaseExtension(configs=configs) + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/simplewiki/mdx_image.py b/simplewiki/mdx_image.py new file mode 100644 index 0000000000..7f74a07550 --- /dev/null +++ b/simplewiki/mdx_image.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +''' +Image Embedding Extension for Python-Markdown +====================================== + +Converts lone links to embedded images, provided the file extension is allowed. + +Ex: + http://www.ericfehse.net/media/img/ef/blog/django-pony.jpg + becomes + + + mypic.jpg becomes + +Requires Python-Markdown 1.6+ +''' + +import simplewiki.settings as settings +import markdown + +class ImageExtension(markdown.Extension): + def __init__(self, configs): + for key, value in configs : + self.setConfig(key, value) + + def add_inline(self, md, name, klass, re): + pattern = klass(re) + pattern.md = md + pattern.ext = self + md.inlinePatterns.add(name, pattern, "([^:/?#])+://)?(?P([^/?#]*)/)?(?P[^?#]*\.(?P[^?#]{3,4}))(?:\?([^#]*))?(?:#(.*))?$') + +class ImageLink(markdown.inlinepatterns.Pattern): + def handleMatch(self, m): + img = markdown.etree.Element('img') + proto = m.group('proto') or "http://" + domain = m.group('domain') + path = m.group('path') + ext = m.group('ext') + + # A fixer upper + if ext.lower() in settings.WIKI_IMAGE_EXTENSIONS: + if domain: + src = proto+domain+path + elif path: + # We need a nice way to source local attachments... + src = "/wiki/media/" + path + ".upload" + else: + src = '' + img.set('src', src) + return img + +def makeExtension(configs=None) : + return ImageExtension(configs=configs) + +if __name__ == "__main__": + import doctest + doctest.testmod() \ No newline at end of file diff --git a/simplewiki/mdx_mathjax.py b/simplewiki/mdx_mathjax.py new file mode 100644 index 0000000000..1afb3c9ed9 --- /dev/null +++ b/simplewiki/mdx_mathjax.py @@ -0,0 +1,21 @@ +# Source: https://github.com/mayoff/python-markdown-mathjax + +import markdown + +class MathJaxPattern(markdown.inlinepatterns.Pattern): + + def __init__(self): + markdown.inlinepatterns.Pattern.__init__(self, r'(?>> import markdown + +Test Metacafe + +>>> s = "http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/" +>>> markdown.markdown(s, ['video']) +u'

' + + +Test Metacafe with arguments + +>>> markdown.markdown(s, ['video(metacafe_width=500,metacafe_height=425)']) +u'

' + + +Test Link To Metacafe + +>>> s = "[Metacafe link](http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/)" +>>> markdown.markdown(s, ['video']) +u'

Metacafe link

' + + +Test Markdown Escaping + +>>> s = "\\http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/" +>>> markdown.markdown(s, ['video']) +u'

http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/

' +>>> s = "`http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/`" +>>> markdown.markdown(s, ['video']) +u'

http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/

' + + +Test Youtube + +>>> s = "http://www.youtube.com/watch?v=u1mA-0w8XPo&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1" +>>> markdown.markdown(s, ['video']) +u'

' + + +Test Youtube with argument + +>>> markdown.markdown(s, ['video(youtube_width=200,youtube_height=100)']) +u'

' + + +Test Youtube Link + +>>> s = "[Youtube link](http://www.youtube.com/watch?v=u1mA-0w8XPo&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1)" +>>> markdown.markdown(s, ['video']) +u'

Youtube link

' + + +Test Dailymotion + +>>> s = "http://www.dailymotion.com/relevance/search/ut2004/video/x3kv65_ut2004-ownage_videogames" +>>> markdown.markdown(s, ['video']) +u'

' + + +Test Dailymotion again (Dailymotion and their crazy URLs) + +>>> s = "http://www.dailymotion.com/us/video/x8qak3_iron-man-vs-bruce-lee_fun" +>>> markdown.markdown(s, ['video']) +u'

' + + +Test Yahoo! Video + +>>> s = "http://video.yahoo.com/watch/1981791/4769603" +>>> markdown.markdown(s, ['video']) +u'

' + + +Test Veoh Video + +>>> s = "http://www.veoh.com/search/videos/q/mario#watch%3De129555XxCZanYD" +>>> markdown.markdown(s, ['video']) +u'

' + + +Test Veoh Video Again (More fun URLs) + +>>> s = "http://www.veoh.com/group/BigCatRescuers#watch%3Dv16771056hFtSBYEr" +>>> markdown.markdown(s, ['video']) +u'

' + + +Test Veoh Video Yet Again (Even more fun URLs) + +>>> s = "http://www.veoh.com/browse/videos/category/anime/watch/v181645607JyXPWcQ" +>>> markdown.markdown(s, ['video']) +u'

' + + +Test Vimeo Video + +>>> s = "http://www.vimeo.com/1496152" +>>> markdown.markdown(s, ['video']) +u'

' + +Test Vimeo Video with some GET values + +>>> s = "http://vimeo.com/1496152?test=test" +>>> markdown.markdown(s, ['video']) +u'

' + +Test Blip.tv + +>>> s = "http://blip.tv/file/get/Pycon-PlenarySprintIntro563.flv" +>>> markdown.markdown(s, ['video']) +u'

' + +Test Gametrailers + +>>> s = "http://www.gametrailers.com/video/console-comparison-borderlands/58079" +>>> markdown.markdown(s, ['video']) +u'

' +""" + +import markdown + +version = "0.1.6" + +class VideoExtension(markdown.Extension): + def __init__(self, configs): + self.config = { + 'bliptv_width': ['480', 'Width for Blip.tv videos'], + 'bliptv_height': ['300', 'Height for Blip.tv videos'], + 'dailymotion_width': ['480', 'Width for Dailymotion videos'], + 'dailymotion_height': ['405', 'Height for Dailymotion videos'], + 'gametrailers_width': ['480', 'Width for Gametrailers videos'], + 'gametrailers_height': ['392', 'Height for Gametrailers videos'], + 'metacafe_width': ['498', 'Width for Metacafe videos'], + 'metacafe_height': ['423', 'Height for Metacafe videos'], + 'veoh_width': ['410', 'Width for Veoh videos'], + 'veoh_height': ['341', 'Height for Veoh videos'], + 'vimeo_width': ['400', 'Width for Vimeo videos'], + 'vimeo_height': ['321', 'Height for Vimeo videos'], + 'yahoo_width': ['512', 'Width for Yahoo! videos'], + 'yahoo_height': ['322', 'Height for Yahoo! videos'], + 'youtube_width': ['425', 'Width for Youtube videos'], + 'youtube_height': ['344', 'Height for Youtube videos'], + } + + # Override defaults with user settings + for key, value in configs: + self.setConfig(key, value) + + def add_inline(self, md, name, klass, re): + pattern = klass(re) + pattern.md = md + pattern.ext = self + md.inlinePatterns.add(name, pattern, "\S+.flv)') + self.add_inline(md, 'dailymotion', Dailymotion, + r'([^(]|^)http://www\.dailymotion\.com/(?P\S+)') + self.add_inline(md, 'gametrailers', Gametrailers, + r'([^(]|^)http://www.gametrailers.com/video/[a-z0-9-]+/(?P\d+)') + self.add_inline(md, 'metacafe', Metacafe, + r'([^(]|^)http://www\.metacafe\.com/watch/(?P\S+)/') + self.add_inline(md, 'veoh', Veoh, + r'([^(]|^)http://www\.veoh\.com/\S*(#watch%3D|watch/)(?P\w+)') + self.add_inline(md, 'vimeo', Vimeo, + r'([^(]|^)http://(www.|)vimeo\.com/(?P\d+)\S*') + self.add_inline(md, 'yahoo', Yahoo, + r'([^(]|^)http://video\.yahoo\.com/watch/(?P\d+)/(?P\d+)') + self.add_inline(md, 'youtube', Youtube, + r'([^(]|^)http://www\.youtube\.com/watch\?\S*v=(?P[A-Za-z0-9_&=-]+)\S*') + +class Bliptv(markdown.inlinepatterns.Pattern): + def handleMatch(self, m): + url = 'http://blip.tv/scripts/flash/showplayer.swf?file=http://blip.tv/file/get/%s' % m.group('bliptvfile') + width = self.ext.config['bliptv_width'][0] + height = self.ext.config['bliptv_height'][0] + return flash_object(url, width, height) + +class Dailymotion(markdown.inlinepatterns.Pattern): + def handleMatch(self, m): + url = 'http://www.dailymotion.com/swf/%s' % m.group('dailymotionid').split('/')[-1] + width = self.ext.config['dailymotion_width'][0] + height = self.ext.config['dailymotion_height'][0] + return flash_object(url, width, height) + +class Gametrailers(markdown.inlinepatterns.Pattern): + def handleMatch(self, m): + url = 'http://www.gametrailers.com/remote_wrap.php?mid=%s' % \ + m.group('gametrailersid').split('/')[-1] + width = self.ext.config['gametrailers_width'][0] + height = self.ext.config['gametrailers_height'][0] + return flash_object(url, width, height) + +class Metacafe(markdown.inlinepatterns.Pattern): + def handleMatch(self, m): + url = 'http://www.metacafe.com/fplayer/%s.swf' % m.group('metacafeid') + width = self.ext.config['metacafe_width'][0] + height = self.ext.config['metacafe_height'][0] + return flash_object(url, width, height) + +class Veoh(markdown.inlinepatterns.Pattern): + def handleMatch(self, m): + url = 'http://www.veoh.com/videodetails2.swf?permalinkId=%s' % m.group('veohid') + width = self.ext.config['veoh_width'][0] + height = self.ext.config['veoh_height'][0] + return flash_object(url, width, height) + +class Vimeo(markdown.inlinepatterns.Pattern): + def handleMatch(self, m): + url = 'http://vimeo.com/moogaloop.swf?clip_id=%s&server=vimeo.com' % m.group('vimeoid') + width = self.ext.config['vimeo_width'][0] + height = self.ext.config['vimeo_height'][0] + return flash_object(url, width, height) + +class Yahoo(markdown.inlinepatterns.Pattern): + def handleMatch(self, m): + url = "http://d.yimg.com/static.video.yahoo.com/yep/YV_YEP.swf?ver=2.2.40" + width = self.ext.config['yahoo_width'][0] + height = self.ext.config['yahoo_height'][0] + obj = flash_object(url, width, height) + param = markdown.etree.Element('param') + param.set('name', 'flashVars') + param.set('value', "id=%s&vid=%s" % (m.group('yahooid'), + m.group('yahoovid'))) + obj.append(param) + return obj + +class Youtube(markdown.inlinepatterns.Pattern): + def handleMatch(self, m): + url = 'http://www.youtube.com/v/%s' % m.group('youtubeargs') + width = self.ext.config['youtube_width'][0] + height = self.ext.config['youtube_height'][0] + return flash_object(url, width, height) + +def flash_object(url, width, height): + obj = markdown.etree.Element('object') + obj.set('type', 'application/x-shockwave-flash') + obj.set('width', width) + obj.set('height', height) + obj.set('data', url) + param = markdown.etree.Element('param') + param.set('name', 'movie') + param.set('value', url) + obj.append(param) + param = markdown.etree.Element('param') + param.set('name', 'allowFullScreen') + param.set('value', 'true') + obj.append(param) + #param = markdown.etree.Element('param') + #param.set('name', 'allowScriptAccess') + #param.set('value', 'sameDomain') + #obj.append(param) + return obj + +def makeExtension(configs=None) : + return VideoExtension(configs=configs) + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/simplewiki/media/css/autosuggest_inquisitor.css b/simplewiki/media/css/autosuggest_inquisitor.css new file mode 100644 index 0000000000..fc407f6f26 --- /dev/null +++ b/simplewiki/media/css/autosuggest_inquisitor.css @@ -0,0 +1,177 @@ +/* +================================================ +autosuggest, inquisitor style +================================================ +*/ + +body +{ + position: relative; +} + + +div.autosuggest +{ + position: absolute; + background-image: url(img_inquisitor/as_pointer.gif); + background-position: top; + background-repeat: no-repeat; + padding: 10px 0 0 0; +} + +div.autosuggest div.as_header, +div.autosuggest div.as_footer +{ + position: relative; + height: 6px; + padding: 0 6px; + background-image: url(img_inquisitor/ul_corner_tr.gif); + background-position: top right; + background-repeat: no-repeat; + overflow: hidden; +} +div.autosuggest div.as_footer +{ + background-image: url(img_inquisitor/ul_corner_br.gif); +} + +div.autosuggest div.as_header div.as_corner, +div.autosuggest div.as_footer div.as_corner +{ + position: absolute; + top: 0; + left: 0; + height: 6px; + width: 6px; + background-image: url(img_inquisitor/ul_corner_tl.gif); + background-position: top left; + background-repeat: no-repeat; +} +div.autosuggest div.as_footer div.as_corner +{ + background-image: url(img_inquisitor/ul_corner_bl.gif); +} +div.autosuggest div.as_header div.as_bar, +div.autosuggest div.as_footer div.as_bar +{ + height: 6px; + overflow: hidden; + background-color: #333; +} + + +div.autosuggest ul +{ + list-style: none; + margin: 0 0 -4px 0; + padding: 0; + overflow: hidden; + background-color: #333; +} + +div.autosuggest ul li +{ + color: #ccc; + padding: 0; + margin: 0 4px 4px; + text-align: left; +} + +div.autosuggest ul li a +{ + color: #ccc; + display: block; + text-decoration: none; + background-color: transparent; + text-shadow: #000 0px 0px 5px; + position: relative; + padding: 0; + width: 100%; +} +div.autosuggest ul li a:hover +{ + background-color: #444; +} +div.autosuggest ul li.as_highlight a:hover +{ + background-color: #1B5CCD; +} + +div.autosuggest ul li a span +{ + display: block; + padding: 3px 6px; + font-weight: bold; +} + +div.autosuggest ul li a span small +{ + font-weight: normal; + color: #999; +} + +div.autosuggest ul li.as_highlight a span small +{ + color: #ccc; +} + +div.autosuggest ul li.as_highlight a +{ + color: #fff; + background-color: #1B5CCD; + background-image: url(img_inquisitor/hl_corner_br.gif); + background-position: bottom right; + background-repeat: no-repeat; +} + +div.autosuggest ul li.as_highlight a span +{ + background-image: url(img_inquisitor/hl_corner_bl.gif); + background-position: bottom left; + background-repeat: no-repeat; +} + +div.autosuggest ul li a .tl, +div.autosuggest ul li a .tr +{ + background-image: transparent; + background-repeat: no-repeat; + width: 6px; + height: 6px; + position: absolute; + top: 0; + padding: 0; + margin: 0; +} +div.autosuggest ul li a .tr +{ + right: 0; +} + +div.autosuggest ul li.as_highlight a .tl +{ + left: 0; + background-image: url(img_inquisitor/hl_corner_tl.gif); + background-position: bottom left; +} + +div.autosuggest ul li.as_highlight a .tr +{ + right: 0; + background-image: url(img_inquisitor/hl_corner_tr.gif); + background-position: bottom right; +} + + + +div.autosuggest ul li.as_warning +{ + font-weight: bold; + text-align: center; +} + +div.autosuggest ul em +{ + font-style: normal; + color: #6EADE7; +} \ No newline at end of file diff --git a/simplewiki/media/css/base.css b/simplewiki/media/css/base.css new file mode 100644 index 0000000000..8c55d0c2f4 --- /dev/null +++ b/simplewiki/media/css/base.css @@ -0,0 +1,281 @@ +body +{ + font-family: 'Lucida Sans', 'Sans'; +} + +a img +{ + border: 0; +} + +div#wiki_article a { + color: #06d; + text-decoration: none; +} + +div#wiki_article a:hover { + color: #f82; + text-decoration: underline; +} + +hr +{ + background-color: #def; + height: 2px; + border: 0; +} + +div#wiki_article .toc a +{ + color: #025 +} + +div#wiki_article p +{ + /* font-size: 90%; looks funny when combined with lists/tables */ + line-height: 140%; +} +div#wiki_article h1 +{ + font-size: 200%; + font-weight: normal; + color: #048; +} + +div#wiki_article h2 +{ + font-size: 150%; + font-weight: normal; + color: #025; +} + +div#wiki_article h3 +{ + font-size: 120%; + font-weight: bold; + color: #000; +} + +table +{ + border: 1px solid black; + border-collapse: collapse; + margin: 12px; +} + +table tr.dark +{ + background-color: #F3F3F3; +} + +table thead tr +{ + background-color: #def; + border-bottom: 2px solid black; +} + +table td, th +{ + padding: 6px 10px 6px 10px; + border: 1px solid black; +} + +table thead th +{ + padding-bottom: 8px; + padding-top: 8px; +} + +div#wiki_panel +{ + float: right; +} + +div.wiki_box +{ + width: 230px; + padding: 10px; + color: #fff; + font-size: 80%; +} + +div.wiki_box div.wiki_box_contents +{ background-color: #222; + padding: 5px 10px;} + +div.wiki_box div.wiki_box_header, +div.wiki_box div.wiki_box_footer +{ + position: relative; + height: 6px; + padding: 0 6px; + background-image: url(../img/box_corner_tr.gif); + background-position: top right; + background-repeat: no-repeat; + overflow: hidden; +} + +div.wiki_box div.wiki_box_footer +{ + background-image: url(../img/box_corner_br.gif); +} + +div.wiki_box div.wiki_box_header div.wiki_box_corner, +div.wiki_box div.wiki_box_footer div.wiki_box_corner +{ + position: absolute; + top: 0; + left: 0; + height: 6px; + width: 6px; + background-image: url(../img/box_corner_tl.gif); + background-position: top left; + background-repeat: no-repeat; +} + +div.wiki_box div.wiki_box_footer div.wiki_box_corner +{ + background-image: url(../img/box_corner_bl.gif); +} + +div.wiki_box div.wiki_box_header div.wiki_box_bar, +div.wiki_box div.wiki_box_footer div.wiki_box_bar +{ + height: 6px; + overflow: hidden; + background-color: #222; +} + + +div.wiki_box a +{ + color: #acf; +} + +div.wiki_box p +{ + margin: 5px 0; +} + +div.wiki_box ul +{ + padding-left: 20px; + margin-left: 0; +} + +div.wiki_box div.wiki_box_title +{ + margin-bottom: 5px; + font-size: 140%; +} + +form#wiki_revision #id_contents +{ + width:500px; + height: 400px; + font-family: monospace; +} + +form#wiki_revision #id_title +{ + width: 500px; +} + +form#wiki_revision #id_revision_text +{ + width: 500px; +} + +table#wiki_revision_table +{ + border: none; + border-collapse: collapse; + padding-right: 250px; +} + +table#wiki_revision_table th +{ + border: none; + text-align: left; + vertical-align: top; +} + +table#wiki_revision_table td +{ + border: none; +} + +table#wiki_history_table +{ + border-collapse: collapse; + border-spacing: 0; + padding-right: 250px; +} + +table#wiki_history_table th#modified +{ + width: 220px; +} + +table#wiki_history_table td +{ + border: none; +} + +table#wiki_history_table tbody tr +{ + border-bottom: 1px solid black; +} + +table#wiki_history_table tbody td +{ + vertical-align: top; + padding: 5px; +} + +table#wiki_history_table tfoot td +{ + border: none; +} + +table#wiki_history_table tbody td.diff +{ + font-family: monospace; + overflow: hidden; + border-left: 1px dotted black; + border-right: 1px dotted black; +} + +table#wiki_history_table th +{ + text-align: left; +} + +div#wiki_attach_progress_container +{ + background-color: #333; + width: 100%; + height: 20px; + display: none; +} + +div#wiki_attach_progress +{ + width: 25%; + background-color: #999; +} + +blockquote { + margin-top: 15px; + margin-bottom: 15px; + margin-left: 50px; + padding-left: 15px; + border-left: 3px solid #666; + color: #999; + max-width: 400px ; +} + +blockquote p { + margin-top: 8px; + margin-bottom: 8px; +} diff --git a/simplewiki/media/css/base_print.css b/simplewiki/media/css/base_print.css new file mode 100644 index 0000000000..4c887c8aa8 --- /dev/null +++ b/simplewiki/media/css/base_print.css @@ -0,0 +1,6 @@ +div#wiki_panel +{ + display:none; +} + + diff --git a/simplewiki/media/css/img_inquisitor/_source/as_pointer.png b/simplewiki/media/css/img_inquisitor/_source/as_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..a0d43aaf0e9d4b9700eac95499f5614ccbd91cd4 GIT binary patch literal 27142 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

+|Jyh+BejmcwL=I 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 literal 0 HcmV?d00001 diff --git a/simplewiki/media/css/img_inquisitor/as_pointer.gif b/simplewiki/media/css/img_inquisitor/as_pointer.gif new file mode 100644 index 0000000000000000000000000000000000000000..dbc21220f6ff01769188756f12651a07d6b4c309 GIT binary patch literal 66 zcmZ?wbhEHb6k*_EXkcJ4Ha7nM|G(l-7DjdkMg|=QAOOiQFiG~*Gn~j>HK!%O$m{Ll T%Az+l_f`9%Ef-}lGFSruI~EhJ literal 0 HcmV?d00001 diff --git a/simplewiki/media/css/img_inquisitor/hl_corner_bl.gif b/simplewiki/media/css/img_inquisitor/hl_corner_bl.gif new file mode 100644 index 0000000000000000000000000000000000000000..b701d01c93cc7746369c52cba6c05e5d65946822 GIT binary patch literal 73 zcmZ?wbhEHbWMg1sSj4~}9dlO8DMuk>gORDt|Ns9PU_kLF3nM!N3xf_w0Hl_InUBLu T=k&vg4@D%dJQ9pyVz34PYIYFP literal 0 HcmV?d00001 diff --git a/simplewiki/media/css/img_inquisitor/hl_corner_br.gif b/simplewiki/media/css/img_inquisitor/hl_corner_br.gif new file mode 100644 index 0000000000000000000000000000000000000000..11debd7fec2c4fe5442211abfead3fd107d2f4c8 GIT binary patch literal 73 zcmZ?wbhEHbWMg1sSj4~}9dlO8DMuk>gORDt|Ns9PU_kLF3nM!N3xf_w0Hl_Ina_i3 T&5=VJ4?I|r6eIY>gTWdAc$pGy literal 0 HcmV?d00001 diff --git a/simplewiki/media/css/img_inquisitor/hl_corner_tl.gif b/simplewiki/media/css/img_inquisitor/hl_corner_tl.gif new file mode 100644 index 0000000000000000000000000000000000000000..1c2bbaf7a7430c4a574a83f2ca27bd7df5770879 GIT binary patch literal 73 zcmZ?wbhEHbWMg1sSj4~}9dlO8DMuk>gQ1!2|Ns9PU_kLF3nM!N3xf_w0Hl_Ina@Is T>DGaR5=uKiOyVeEVz34PW|RgQ1!2|Ns9PU_kLF3nM!N3xf_w0Hl_InU6#0 T@`ppVrX95hg;u09F<1itYmX4% literal 0 HcmV?d00001 diff --git a/simplewiki/media/css/img_inquisitor/ul_corner_bl.gif b/simplewiki/media/css/img_inquisitor/ul_corner_bl.gif new file mode 100644 index 0000000000000000000000000000000000000000..4fa8c8e11b7111278b74b750c6c1fb19cf00c0a4 GIT binary patch literal 49 zcmZ?wbhEHbWMg1sXkcV8Ha2EpU{L(Y!pOkD$e;sc1I5`G7??O(`o(ritc}xVum%9L CT?d>1 literal 0 HcmV?d00001 diff --git a/simplewiki/media/css/img_inquisitor/ul_corner_br.gif b/simplewiki/media/css/img_inquisitor/ul_corner_br.gif new file mode 100644 index 0000000000000000000000000000000000000000..f589431b49668999d9394d1c1f0c30e4c8b3381d GIT binary patch literal 49 zcmZ?wbhEHbWMg1sXkcV8Ha2EpU{L(Y!pOkD$e;sc1I5`G7??O(`ZFiiJXFwOum%9P CLv0}%XHFqXYemi;cx9!_M&ztx2^5y^k|9_f1 zdFs@u3`junCkrD312=;XNDO2K1B-sa+Qf5)PI>`T!d(nRIUgxqqjev@<|JIi^PEuX0%AE`h)&Qu)FYy2X literal 0 HcmV?d00001 diff --git a/simplewiki/media/js/bsn.AutoSuggest_c_2.0.js b/simplewiki/media/js/bsn.AutoSuggest_c_2.0.js new file mode 100644 index 0000000000..b202f3abb2 --- /dev/null +++ b/simplewiki/media/js/bsn.AutoSuggest_c_2.0.js @@ -0,0 +1,961 @@ +/** + * author: Timothy Groves - http://www.brandspankingnew.net + * version: 1.2 - 2006-11-17 + * 1.3 - 2006-12-04 + * 2.0 - 2007-02-07 + * + */ + +var useBSNns; + +if (useBSNns) +{ + if (typeof(bsn) == "undefined") + bsn = {} + _bsn = bsn; +} +else +{ + _bsn = this; +} + + + +if (typeof(_bsn.Autosuggest) == "undefined") + _bsn.Autosuggest = {} + + + + + + + + + + + + + +_bsn.AutoSuggest = function (fldID, param) +{ + // no DOM - give up! + // + if (!document.getElementById) + return false; + + + + + // get field via DOM + // + this.fld = _bsn.DOM.getElement(fldID); + + if (!this.fld) + return false; + + + + + // init variables + // + this.sInput = ""; + this.nInputChars = 0; + this.aSuggestions = []; + this.iHighlighted = 0; + + + + + // parameters object + // + this.oP = (param) ? param : {}; + + // defaults + // + if (!this.oP.minchars) this.oP.minchars = 1; + if (!this.oP.method) this.oP.meth = "get"; + if (!this.oP.varname) this.oP.varname = "input"; + if (!this.oP.className) this.oP.className = "autosuggest"; + if (!this.oP.timeout) this.oP.timeout = 2500; + if (!this.oP.delay) this.oP.delay = 500; + if (!this.oP.offsety) this.oP.offsety = -5; + if (!this.oP.shownoresults) this.oP.shownoresults = true; + if (!this.oP.noresults) this.oP.noresults = "No results!"; + if (!this.oP.maxheight && this.oP.maxheight !== 0) this.oP.maxheight = 250; + if (!this.oP.cache && this.oP.cache != false) this.oP.cache = true; + + + + + + // set keyup handler for field + // and prevent autocomplete from client + // + var pointer = this; + + // NOTE: not using addEventListener because UpArrow fired twice in Safari + //_bsn.DOM.addEvent( this.fld, 'keyup', function(ev){ return pointer.onKeyPress(ev); } ); + + this.fld.onkeypress = function(ev){ return pointer.onKeyPress(ev); } + this.fld.onkeyup = function(ev){ return pointer.onKeyUp(ev); } + + this.fld.setAttribute("autocomplete","off"); +} + + + + + + + + + + + + + + + + +_bsn.AutoSuggest.prototype.onKeyPress = function(ev) +{ + + var key = (window.event) ? window.event.keyCode : ev.keyCode; + + + + // set responses to keydown events in the field + // this allows the user to use the arrow keys to scroll through the results + // ESCAPE clears the list + // TAB sets the current highlighted value + // + var RETURN = 13; + var TAB = 9; + var ESC = 27; + + var bubble = true; + + switch(key) + { + + case RETURN: + this.setHighlightedValue(); + bubble = false; + break; + + + case ESC: + this.clearSuggestions(); + break; + } + + return bubble; +} + + + +_bsn.AutoSuggest.prototype.onKeyUp = function(ev) +{ + var key = (window.event) ? window.event.keyCode : ev.keyCode; + + + + // set responses to keydown events in the field + // this allows the user to use the arrow keys to scroll through the results + // ESCAPE clears the list + // TAB sets the current highlighted value + // + + var ARRUP = 38; + var ARRDN = 40; + + var bubble = true; + + switch(key) + { + + + case ARRUP: + this.changeHighlight(key); + bubble = false; + break; + + + case ARRDN: + this.changeHighlight(key); + bubble = false; + break; + + + default: + this.getSuggestions(this.fld.value); + } + + return bubble; + + +} + + + + + + + + +_bsn.AutoSuggest.prototype.getSuggestions = function (val) +{ + + // if input stays the same, do nothing + // + if (val == this.sInput) + return false; + + + // input length is less than the min required to trigger a request + // reset input string + // do nothing + // + if (val.length < this.oP.minchars) + { + this.sInput = ""; + return false; + } + + + // if caching enabled, and user is typing (ie. length of input is increasing) + // filter results out of aSuggestions from last request + // + if (val.length>this.nInputChars && this.aSuggestions.length && this.oP.cache) + { + var arr = []; + for (var i=0;i" + val.substring(st, st+this.sInput.length) + "" + val.substring(st+this.sInput.length); + + + var span = _bsn.DOM.createElement("span", {}, output, true); + if (arr[i].info != "") + { + var br = _bsn.DOM.createElement("br", {}); + span.appendChild(br); + var small = _bsn.DOM.createElement("small", {}, arr[i].info); + span.appendChild(small); + } + + var a = _bsn.DOM.createElement("a", { href:"#" }); + + var tl = _bsn.DOM.createElement("span", {className:"tl"}, " "); + var tr = _bsn.DOM.createElement("span", {className:"tr"}, " "); + a.appendChild(tl); + a.appendChild(tr); + + a.appendChild(span); + + a.name = i+1; + a.onclick = function () { pointer.setHighlightedValue(); return false; } + a.onmouseover = function () { pointer.setHighlight(this.name); } + + var li = _bsn.DOM.createElement( "li", {}, a ); + + ul.appendChild( li ); + } + + + // no results + // + if (arr.length == 0) + { + var li = _bsn.DOM.createElement( "li", {className:"as_warning"}, this.oP.noresults ); + + ul.appendChild( li ); + } + + + div.appendChild( ul ); + + + var fcorner = _bsn.DOM.createElement("div", {className:"as_corner"}); + var fbar = _bsn.DOM.createElement("div", {className:"as_bar"}); + var footer = _bsn.DOM.createElement("div", {className:"as_footer"}); + footer.appendChild(fcorner); + footer.appendChild(fbar); + div.appendChild(footer); + + + + // get position of target textfield + // position holding div below it + // set width of holding div to width of field + // + var pos = _bsn.DOM.getPos(this.fld); + + div.style.left = pos.x + "px"; + div.style.top = ( pos.y + this.fld.offsetHeight + this.oP.offsety ) + "px"; + div.style.width = this.fld.offsetWidth + "px"; + + + + // set mouseover functions for div + // when mouse pointer leaves div, set a timeout to remove the list after an interval + // when mouse enters div, kill the timeout so the list won't be removed + // + div.onmouseover = function(){ pointer.killTimeout() } + div.onmouseout = function(){ pointer.resetTimeout() } + + + // add DIV to document + // + document.getElementsByTagName("body")[0].appendChild(div); + + + + // currently no item is highlighted + // + this.iHighlighted = 0; + + + + + + + // remove list after an interval + // + var pointer = this; + this.toID = setTimeout(function () { pointer.clearSuggestions() }, this.oP.timeout); +} + + + + + + + + + + + + + + + +_bsn.AutoSuggest.prototype.changeHighlight = function(key) +{ + var list = _bsn.DOM.getElement("as_ul"); + if (!list) + return false; + + var n; + + if (key == 40) + n = this.iHighlighted + 1; + else if (key == 38) + n = this.iHighlighted - 1; + + + if (n > list.childNodes.length) + n = list.childNodes.length; + if (n < 1) + n = 1; + + + this.setHighlight(n); +} + + + +_bsn.AutoSuggest.prototype.setHighlight = function(n) +{ + var list = _bsn.DOM.getElement("as_ul"); + if (!list) + return false; + + if (this.iHighlighted > 0) + this.clearHighlight(); + + this.iHighlighted = Number(n); + + list.childNodes[this.iHighlighted-1].className = "as_highlight"; + + + this.killTimeout(); +} + + +_bsn.AutoSuggest.prototype.clearHighlight = function() +{ + var list = _bsn.DOM.getElement("as_ul"); + if (!list) + return false; + + if (this.iHighlighted > 0) + { + list.childNodes[this.iHighlighted-1].className = ""; + this.iHighlighted = 0; + } +} + + +_bsn.AutoSuggest.prototype.setHighlightedValue = function () +{ + if (this.iHighlighted) + { + this.sInput = this.fld.value = this.aSuggestions[ this.iHighlighted-1 ].value; + + // move cursor to end of input (safari) + // + this.fld.focus(); + if (this.fld.selectionStart) + this.fld.setSelectionRange(this.sInput.length, this.sInput.length); + + + this.clearSuggestions(); + + // pass selected object to callback function, if exists + // + if (typeof(this.oP.callback) == "function") + this.oP.callback( this.aSuggestions[this.iHighlighted-1] ); + } +} + + + + + + + + + + + + + +_bsn.AutoSuggest.prototype.killTimeout = function() +{ + clearTimeout(this.toID); +} + +_bsn.AutoSuggest.prototype.resetTimeout = function() +{ + clearTimeout(this.toID); + var pointer = this; + this.toID = setTimeout(function () { pointer.clearSuggestions() }, 1000); +} + + + + + + + +_bsn.AutoSuggest.prototype.clearSuggestions = function () +{ + + this.killTimeout(); + + var ele = _bsn.DOM.getElement(this.idAs); + var pointer = this; + if (ele) + { + var fade = new _bsn.Fader(ele,1,0,250,function () { _bsn.DOM.removeElement(pointer.idAs) }); + } +} + + + + + + + + + + +// AJAX PROTOTYPE _____________________________________________ + + +if (typeof(_bsn.Ajax) == "undefined") + _bsn.Ajax = {} + + + +_bsn.Ajax = function () +{ + this.req = {}; + this.isIE = false; +} + + + +_bsn.Ajax.prototype.makeRequest = function (url, meth, onComp, onErr) +{ + + if (meth != "POST") + meth = "GET"; + + this.onComplete = onComp; + this.onError = onErr; + + var pointer = this; + + // branch for native XMLHttpRequest object + if (window.XMLHttpRequest) + { + this.req = new XMLHttpRequest(); + this.req.onreadystatechange = function () { pointer.processReqChange() }; + this.req.open("GET", url, true); // + this.req.send(null); + // branch for IE/Windows ActiveX version + } + else if (window.ActiveXObject) + { + this.req = new ActiveXObject("Microsoft.XMLHTTP"); + if (this.req) + { + this.req.onreadystatechange = function () { pointer.processReqChange() }; + this.req.open(meth, url, true); + this.req.send(); + } + } +} + + +_bsn.Ajax.prototype.processReqChange = function() +{ + + // only if req shows "loaded" + if (this.req.readyState == 4) { + // only if "OK" + if (this.req.status == 200) + { + this.onComplete( this.req ); + } else { + this.onError( this.req.status ); + } + } +} + + + + + + + + + + +// DOM PROTOTYPE _____________________________________________ + + +if (typeof(_bsn.DOM) == "undefined") + _bsn.DOM = {} + + + + +_bsn.DOM.createElement = function ( type, attr, cont, html ) +{ + var ne = document.createElement( type ); + if (!ne) + return false; + + for (var a in attr) + ne[a] = attr[a]; + + if (typeof(cont) == "string" && !html) + ne.appendChild( document.createTextNode(cont) ); + else if (typeof(cont) == "string" && html) + ne.innerHTML = cont; + else if (typeof(cont) == "object") + ne.appendChild( cont ); + + return ne; +} + + + + + +_bsn.DOM.clearElement = function ( id ) +{ + var ele = this.getElement( id ); + + if (!ele) + return false; + + while (ele.childNodes.length) + ele.removeChild( ele.childNodes[0] ); + + return true; +} + + + + + + + + + +_bsn.DOM.removeElement = function ( ele ) +{ + var e = this.getElement(ele); + + if (!e) + return false; + else if (e.parentNode.removeChild(e)) + return true; + else + return false; +} + + + + + +_bsn.DOM.replaceContent = function ( id, cont, html ) +{ + var ele = this.getElement( id ); + + if (!ele) + return false; + + this.clearElement( ele ); + + if (typeof(cont) == "string" && !html) + ele.appendChild( document.createTextNode(cont) ); + else if (typeof(cont) == "string" && html) + ele.innerHTML = cont; + else if (typeof(cont) == "object") + ele.appendChild( cont ); +} + + + + + + + + + +_bsn.DOM.getElement = function ( ele ) +{ + if (typeof(ele) == "undefined") + { + return false; + } + else if (typeof(ele) == "string") + { + var re = document.getElementById( ele ); + if (!re) + return false; + else if (typeof(re.appendChild) != "undefined" ) { + return re; + } else { + return false; + } + } + else if (typeof(ele.appendChild) != "undefined") + return ele; + else + return false; +} + + + + + + + +_bsn.DOM.appendChildren = function ( id, arr ) +{ + var ele = this.getElement( id ); + + if (!ele) + return false; + + + if (typeof(arr) != "object") + return false; + + for (var i=0;i 1 and fname[-1].lower() in WIKI_IMAGE_EXTENSIONS: + return True + return False + + def get_thumb(self): + return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE) + + def get_thumb_small(self): + return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE_SMALL) + + def mk_thumbs(self): + self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE, **{'force':True}) + self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE_SMALL, **{'force':True}) + + def mk_thumb(self, width, height, force=False): + """Requires Python Imaging Library (PIL)""" + if not self.get_size(): + return False + + if not self.is_image(): + return False + + base_path = os.path.dirname(self.file.path) + orig_name = self.filename().split('.') + thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1]) + thumb_filepath = "%s%s%s" % (base_path, os.sep, thumb_filename) + + if force or not os.path.exists(thumb_filepath): + try: + import Image + img = Image.open(self.file.path) + img.thumbnail((width,height), Image.ANTIALIAS) + img.save(thumb_filepath) + except IOError: + return False + + return True + + def get_thumb_impl(self, width, height): + """Requires Python Imaging Library (PIL)""" + + if not self.get_size(): + return False + + if not self.is_image(): + return False + + self.mk_thumb(width, height) + + orig_name = self.filename().split('.') + thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1]) + thumb_url = settings.MEDIA_URL + WIKI_ATTACHMENTS + self.article.get_url() +'/' + thumb_filename + + return thumb_url + + def __unicode__(self): + return self.filename() + +class Revision(models.Model): + + article = models.ForeignKey(Article, verbose_name=_('Article')) + revision_text = models.CharField(max_length=255, blank=True, null=True, + verbose_name=_('Description of change')) + revision_user = models.ForeignKey(User, verbose_name=_('Modified by'), + blank=True, null=True, related_name='wiki_revision_user') + revision_date = models.DateTimeField(auto_now_add = True, verbose_name=_('Revision date')) + contents = models.TextField(verbose_name=_('Contents (Use MarkDown format)')) + contents_parsed = models.TextField(editable=False, blank=True, null=True) + counter = models.IntegerField(verbose_name=_('Revision#'), default=1, editable=False) + previous_revision = models.ForeignKey('self', blank=True, null=True, editable=False) + + def get_user(self): + return self.revision_user if self.revision_user else _('Anonymous') + + def save(self, **kwargs): + # Check if contents have changed... if not, silently ignore save + if self.article and self.article.current_revision: + if self.article.current_revision.contents == self.contents: + return + else: + import datetime + self.article.modified_on = datetime.datetime.now() + self.article.save() + + # Increment counter according to previous revision + previous_revision = Revision.objects.filter(article=self.article).order_by('-counter') + if previous_revision.count() > 0: + if previous_revision.count() > previous_revision[0].counter: + self.counter = previous_revision.count() + 1 + else: + self.counter = previous_revision[0].counter + 1 + else: + self.counter = 1 + self.previous_revision = self.article.current_revision + + # Create pre-parsed contents - no need to parse on-the-fly + ext = WIKI_MARKDOWN_EXTENSIONS + ext += ["wikilinks(base_url=%s/)" % reverse('wiki_view', args=('',))] + self.contents_parsed = markdown(self.contents, + extensions=ext, + safe_mode='escape',) + super(Revision, self).save(**kwargs) + + def delete(self, **kwargs): + """If a current revision is deleted, then regress to the previous + revision or insert a stub, if no other revisions are available""" + article = self.article + if article.current_revision == self: + prev_revision = Revision.objects.filter(article__exact = article, + pk__not = self.pk).order_by('-counter') + if prev_revision: + article.current_revision = prev_revision[0] + article.save() + else: + r = Revision(article=article, + revision_user = article.created_by) + r.contents = unicode(_('Auto-generated stub')) + r.revision_text= unicode(_('Auto-generated stub')) + r.save() + article.current_revision = r + article.save() + super(Revision, self).delete(**kwargs) + + def get_diff(self): + if self.previous_revision: + previous = self.previous_revision.contents.splitlines(1) + else: + previous = [] + + # Todo: difflib.HtmlDiff would look pretty for our history pages! + diff = difflib.unified_diff(previous, self.contents.splitlines(1)) + # let's skip the preamble + diff.next(); diff.next(); diff.next() + + for d in diff: + yield d + + def __unicode__(self): + return "r%d" % self.counter + + class Meta: + verbose_name = _('article revision') + verbose_name_plural = _('article revisions') + +class Permission(models.Model): + permission_name = models.CharField(max_length = 255, verbose_name=_('Permission name')) + can_write = models.ManyToManyField(User, blank=True, null=True, related_name='write', + help_text=_('Select none to grant anonymous access.')) + can_read = models.ManyToManyField(User, blank=True, null=True, related_name='read', + help_text=_('Select none to grant anonymous access.')) + def __unicode__(self): + return self.permission_name + class Meta: + verbose_name = _('Article permission') + verbose_name_plural = _('Article permissions') + +class RevisionForm(forms.ModelForm): + contents = forms.CharField(label=_('Contents'), widget=forms.Textarea(attrs={'rows':8, 'cols':50})) + class Meta: + model = Revision + fields = ['contents', 'revision_text'] +class RevisionFormWithTitle(forms.ModelForm): + title = forms.CharField(label=_('Title')) + class Meta: + model = Revision + fields = ['title', 'contents', 'revision_text'] +class CreateArticleForm(RevisionForm): + title = forms.CharField(label=_('Title')) + class Meta: + model = Revision + fields = ['title', 'contents',] + +def set_revision(sender, *args, **kwargs): + """Signal handler to ensure that a new revision is always chosen as the + current revision - automatically. It simplifies stuff greatly. Also + stores previous revision for diff-purposes""" + instance = kwargs['instance'] + created = kwargs['created'] + if created and instance.article: + instance.article.current_revision = instance + instance.article.save() + +signals.post_save.connect(set_revision, Revision) diff --git a/simplewiki/settings.py b/simplewiki/settings.py new file mode 100644 index 0000000000..9a2f35c0eb --- /dev/null +++ b/simplewiki/settings.py @@ -0,0 +1,111 @@ +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings + +# Default settings.. overwrite in your own settings.py + +# Planned feature. +WIKI_USE_MARKUP_WIDGET = True + +#################### +# LOGIN PROTECTION # +#################### +# Before setting the below parameters, please note that permissions can +# be set in the django permission system on individual articles and their +# child articles. In this way you can add a user group and give them +# special permissions, be it on the root article or some other. Permissions +# are inherited on lower levels. + +# Adds standard django login protection for viewing +WIKI_REQUIRE_LOGIN_VIEW = getattr(settings, 'SIMPLE_WIKI_REQUIRE_LOGIN_VIEW', + False) + +# Adds standard django login protection for editing +WIKI_REQUIRE_LOGIN_EDIT = getattr(settings, 'SIMPLE_WIKI_REQUIRE_LOGIN_EDIT', + True) + +#################### +# ATTACHMENTS # +#################### + +# This should be a directory that's writable for the web server. +# It's relative to the MEDIA_ROOT. +WIKI_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS', + 'simplewiki/attachments/') + +# If false, attachments will completely disappear +WIKI_ALLOW_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ALLOW_ATTACHMENTS', + True) + +# If WIKI_REQUIRE_LOGIN_EDIT is False, then attachments can still be disallowed +WIKI_ALLOW_ANON_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ALLOW_ANON_ATTACHMENTS', False) + +# Attachments are automatically stored with a dummy extension and delivered +# back to the user with their original extension. +# This setting does not add server security, but might add user security +# if set -- or force users to use standard formats, which might also +# be a good idea. +# Example: ('pdf', 'doc', 'gif', 'jpeg', 'jpg', 'png') +WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS', + None) + +# At the moment this variable should not be modified, because +# it breaks compatibility with the normal Django FileField and uploading +# from the admin interface. +WIKI_ATTACHMENTS_ROOT = settings.MEDIA_ROOT + +# Bytes! Default: 1 MB. +WIKI_ATTACHMENTS_MAX = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS_MAX', + 1 * 1024 * 1024) + +# Allow users to edit titles of pages +# (warning! titles are not maintained in the revision system.) +WIKI_ALLOW_TITLE_EDIT = getattr(settings, 'SIMPLE_WIKI_ALLOW_TITLE_EDIT', False) + +# Global context processors +# These are appended to TEMPLATE_CONTEXT_PROCESSORS in your Django settings +# whenever the wiki is in use. It can be used as a simple, but effective +# way of extending simplewiki without touching original code (and thus keeping +# everything easily maintainable) +WIKI_CONTEXT_PREPROCESSORS = getattr(settings, 'SIMPLE_WIKI_CONTEXT_PREPROCESSORS', + ()) + +#################### +# AESTHETICS # +#################### + +# List of extensions to be used by Markdown. Custom extensions (i.e., with file +# names of mdx_*.py) can be dropped into the simplewiki (or project) directory +# and then added to this list to be utilized. Wikilinks is always enabled. +# +# For more information, see +# http://www.freewisdom.org/projects/python-markdown/Available_Extensions +WIKI_MARKDOWN_EXTENSIONS = getattr(settings, 'SIMPLE_WIKI_MARKDOWN_EXTENSIONS', + ['footnotes', + 'tables', + 'headerid', + 'fenced_code', + 'def_list', + 'codehilite', + 'abbr', + 'toc', + 'camelcase', # CamelCase-style wikilinks + 'video', # In-line embedding for YouTube, etc. + #'image' # In-line embedding for images - too many bugs. It has a failed REG EXP. + ]) + + +WIKI_IMAGE_EXTENSIONS = getattr(settings, + 'SIMPLE_WIKI_IMAGE_EXTENSIONS', + ('jpg','jpeg','gif','png','tiff','bmp')) +# Planned features +WIKI_PAGE_WIDTH = getattr(settings, + 'SIMPLE_WIKI_PAGE_WIDTH', "100%") + +WIKI_PAGE_ALIGN = getattr(settings, + 'SIMPLE_WIKI_PAGE_ALIGN', "center") + +WIKI_IMAGE_THUMB_SIZE = getattr(settings, + 'SIMPLE_WIKI_IMAGE_THUMB_SIZE', (200,150)) + +WIKI_IMAGE_THUMB_SIZE_SMALL = getattr(settings, + 'SIMPLE_WIKI_IMAGE_THUMB_SIZE_SMALL', (100,100)) diff --git a/simplewiki/templates/simplewiki_base.html b/simplewiki/templates/simplewiki_base.html new file mode 100644 index 0000000000..f8d6a65761 --- /dev/null +++ b/simplewiki/templates/simplewiki_base.html @@ -0,0 +1,197 @@ +{% load i18n simplewiki_utils %} + + + +{{ wiki_title }} + + + + + +{% block wiki_head %} +{% endblock %} + + + +

{% block wiki_page_title %}{% endblock %}

+
+ +{% block wiki_panel %} + +
+ +
+
+
+
+
+
+
{% trans "Search" %}
+
{% csrf_token %} + + +
+
+ +
+ +
+
+
+
+
+
+ + +

+ + {% if wiki_article %} +
+
+ + {% endif %} + + +

+ {% if wiki_article %} + {% if wiki_article.locked %} +

{% trans "This article has been locked" %}

+ {% endif %} +

+ {% trans "Last modified" %}: {{ wiki_article.modified_on|date }}, {{ wiki_article.modified_on|time }} +

+ {% endif %} +
+ +
+ + + {% if wiki_article %} +
+
+
+
+
+
+ +
{% trans "Related articles" %}
+ {% if wiki_article.related.all %} +

+ {% for rel in wiki_article.related.all %} + + {% if wiki_write %} + + {% trans + + {% endif %} + {{rel.title}} + + {% endfor %} +

+ {% else %} +

({% trans "none" %})

+ {% endif %} + {% if wiki_write %} +
{% csrf_token %} + + + +
+ {% endif %} + +
+ +
+ +
+
+
+
+
+
+ +
{% trans "Attachments" %}
+ {% if wiki_article.attachments %} + + {% else %} +

({% trans "none" %})

+ {% endif %} + + {% if wiki_attachments_write %} +
{% csrf_token %} +
+ Overwrite same filename +

+
+

+
+
+ + {% endif %} + +
+ +
+ {% endif %} + +
+ +{% endblock %} + +{% block wiki_body %} + +{% endblock %} + + + diff --git a/simplewiki/templates/simplewiki_create.html b/simplewiki/templates/simplewiki_create.html new file mode 100644 index 0000000000..b9b6eabedb --- /dev/null +++ b/simplewiki/templates/simplewiki_create.html @@ -0,0 +1,16 @@ +{% extends "simplewiki_base.html" %} +{% load i18n simplewiki_utils %} +{% block wiki_page_title %} +Create article +{% endblock %} +{% block wiki_body %} +
{% csrf_token %} + + {{ wiki_form }} + + + +
+
+
+{% endblock %} diff --git a/simplewiki/templates/simplewiki_edit.html b/simplewiki/templates/simplewiki_edit.html new file mode 100644 index 0000000000..b0a7766d5d --- /dev/null +++ b/simplewiki/templates/simplewiki_edit.html @@ -0,0 +1,17 @@ +{% extends "simplewiki_base.html" %} +{% load i18n simplewiki_utils %} +{% block wiki_page_title %} + {{wiki_article.title}} +{% endblock %} + +{% block wiki_body %} +
{% csrf_token %} + + {{ wiki_form }} + + + +
+
+
+{% endblock %} diff --git a/simplewiki/templates/simplewiki_error.html b/simplewiki/templates/simplewiki_error.html new file mode 100644 index 0000000000..7b7fb9bbe7 --- /dev/null +++ b/simplewiki/templates/simplewiki_error.html @@ -0,0 +1,98 @@ +{% extends "simplewiki_base.html" %} +{% load i18n simplewiki_utils %} +{% block wiki_page_title %} +Oops... +{% endblock %} + +{% block wiki_body %} +
+{{ wiki_error|safe }} + +{% if wiki_err_notfound %} +{% if wiki_url %} +

+The page you requested could not be found. +Click here to create it. +

+{% else %} +

+Or maybe rather: Congratulations! It seems that there's no root +article, which is probably because you just installed simple-wiki +and your installation is working. Now you can create the root article. +Click here to create it. +

+{% endif %} +{% else %} + +{% if wiki_err_noparent %} +

+You cannot create this page, because its parent +does not exist. Click here +to create it. +

+{% else %} + +{% if wiki_err_keyword %} +

+The page you're trying to create {{wiki_url}} starts with _, which is reserved for internal use. +

+{% else %} + +{% if wiki_err_locked %} +

+The article you are trying to modify is locked. +

+{% else %} + +{% if wiki_err_noread %} +

+You do not have access to read this article. +

+{% else %} + +{% if wiki_err_nowrite %} +

+You do not have access to edit this article. +

+{% else %} + +{% if wiki_err_noanon %} +

+Anonymous attachments are not allowed. Try logging in. +

+{% else %} + +{% if wiki_err_create %} +

+You do not have access to create this article. +

+{% else %} + +{% if wiki_err_encode %} +

+The url you requested could not be handled by the wiki. +Probably you used a bad character in the URL. +Only use digits, English letters, underscore and dash. For instance +/wiki/An_Article-1 +

+ + +{% else %} +

+An error has occured. +

+ + +{% endif %} +{% endif %} +{% endif %} +{% endif %} +{% endif %} +{% endif %} +{% endif %} +{% endif %} +{% endif %} + +
+{% endblock %} + diff --git a/simplewiki/templates/simplewiki_history.html b/simplewiki/templates/simplewiki_history.html new file mode 100644 index 0000000000..aacbd3b5f6 --- /dev/null +++ b/simplewiki/templates/simplewiki_history.html @@ -0,0 +1,57 @@ +{% extends "simplewiki_base.html" %} +{% load i18n simplewiki_utils %} +{% block wiki_page_title %} + {{ wiki_article.title }} +{% endblock %} +{% block wiki_body %} +
{% csrf_token %} + + + + + + + + + + + {% for revision in wiki_history %} + + + + + + + {% endfor %} + + {% if wiki_prev_page or wiki_next_page %} + + + + + + {% endif %} +
RevisionCommentDiffModified
+ + + {% if revision.revision_text %}{{ revision.revision_text}}{% else %}None{% endif %}{% for x in revision.get_diff %}{{x|escape}}
{% endfor %}
{{ revision.get_user}} +
+ {{ revision.revision_date|date}} {{ revision.revision_date|time}} +
+ {% if wiki_prev_page %} + {% trans "Previous page" %} + {% endif %} + {% if wiki_next_page %} + {% trans "Next page" %} + {% endif %} +
+ +
+{% endblock %} diff --git a/simplewiki/templates/simplewiki_searchresults.html b/simplewiki/templates/simplewiki_searchresults.html new file mode 100644 index 0000000000..a60487c4ea --- /dev/null +++ b/simplewiki/templates/simplewiki_searchresults.html @@ -0,0 +1,21 @@ +{% extends "simplewiki_base.html" %} +{% load i18n simplewiki_utils %} +{% block wiki_page_title %} + {% if wiki_search_query %} + {% trans "Search results for" %} '{{ wiki_search_query|escape }}' + {% else %} + {% trans "Displaying all articles" %} + {% endif %} +{% endblock %} + +{% block wiki_body %} + {% for article in wiki_search_results %} + {% if article.get_url %} + {{ article.get_url }}
+ {% else %} + /
+ {% endif %} + {% empty %} + {% trans "No articles were found!" %} + {% endfor %} +{% endblock %} diff --git a/simplewiki/templates/simplewiki_updateprogressbar.html b/simplewiki/templates/simplewiki_updateprogressbar.html new file mode 100644 index 0000000000..65f455518d --- /dev/null +++ b/simplewiki/templates/simplewiki_updateprogressbar.html @@ -0,0 +1,32 @@ +{% load i18n simplewiki_utils %} + +{% if started %} + +{% else %} +{% if finished %} + +{% else %} +{% if overwrite_warning %} + +{% else %} +{% if too_big %} + +{% else %} + +{% endif %} +{% endif %} +{% endif %} +{% endif %} diff --git a/simplewiki/templates/simplewiki_view.html b/simplewiki/templates/simplewiki_view.html new file mode 100644 index 0000000000..1c480d44fa --- /dev/null +++ b/simplewiki/templates/simplewiki_view.html @@ -0,0 +1,10 @@ +{% extends "simplewiki_base.html" %} +{% load i18n simplewiki_utils %} +{% block wiki_page_title %} +{{ wiki_article.title }} +{% endblock %} +{% block wiki_body %} +
+ {{ wiki_article.current_revision.contents_parsed|safe }} +
+{% endblock %} diff --git a/simplewiki/templatetags/__init__.py b/simplewiki/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/simplewiki/templatetags/simplewiki_utils.py b/simplewiki/templatetags/simplewiki_utils.py new file mode 100644 index 0000000000..5b8eccf910 --- /dev/null +++ b/simplewiki/templatetags/simplewiki_utils.py @@ -0,0 +1,17 @@ +from django import template +from django.template.defaultfilters import stringfilter +from simplewiki.settings import * +from django.conf import settings +from django.utils.http import urlquote as django_urlquote + +register = template.Library() + +@register.filter() +def prepend_media_url(value): + """Prepend user defined media root to url""" + return settings.MEDIA_URL + value + +@register.filter() +def urlquote(value): + """Prepend user defined media root to url""" + return django_urlquote(value) \ No newline at end of file diff --git a/simplewiki/tests.py b/simplewiki/tests.py new file mode 100644 index 0000000000..2247054b35 --- /dev/null +++ b/simplewiki/tests.py @@ -0,0 +1,23 @@ +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/simplewiki/urls.py b/simplewiki/urls.py new file mode 100644 index 0000000000..1d8d3c246c --- /dev/null +++ b/simplewiki/urls.py @@ -0,0 +1,18 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('', + url(r'^$', 'simplewiki.views.root_redirect', name='wiki_root'), + url(r'^/?([a-zA-Z\d/_-]*)/_edit/$', 'simplewiki.views.edit', name='wiki_edit'), + url(r'^/?([a-zA-Z\d/_-]*)/_create/$', 'simplewiki.views.create', name='wiki_create'), + url(r'^/?([a-zA-Z\d/_-]*)/_history/([0-9]*)/$', 'simplewiki.views.history', name='wiki_history'), + url(r'^/?([a-zA-Z\d/_-]*)/_random/$', 'simplewiki.views.random_article', name='wiki_random'), + url(r'^/?([a-zA-Z\d/_-]*)/_search/articles/$', 'simplewiki.views.search_articles', name='wiki_search_articles'), + url(r'^/?([a-zA-Z\d/_-]*)/_search/related/$', 'simplewiki.views.search_add_related', name='search_related'), + url(r'^/?([a-zA-Z\d/_-]*)/_related/add/$', 'simplewiki.views.add_related', name='add_related'), + url(r'^/?([a-zA-Z\d/_-]*)/_related/remove/(\d+)$', 'simplewiki.views.remove_related', name='wiki_remove_relation'), + url(r'^/?([a-zA-Z\d/_-]*)/_add_attachment/$', 'simplewiki.views_attachments.add_attachment', name='add_attachment'), + url(r'^/?([a-zA-Z\d/_-]*)/_view_attachment/(.+)?$', 'simplewiki.views_attachments.view_attachment', name='wiki_view_attachment'), +# url(r'^/?([a-zA-Z\d/_-]*)/_view_attachment/?$', 'simplewiki.views_attachments.list_attachments', name='wiki_list_attachments'), + url(r'^/?([a-zA-Z\d/_-]*)$', 'simplewiki.views.view', name='wiki_view'), + url(r'^(.*)$', 'simplewiki.views.encode_err', name='wiki_encode_err') +) diff --git a/simplewiki/usage.txt b/simplewiki/usage.txt new file mode 100644 index 0000000000..4a74ffaf8e --- /dev/null +++ b/simplewiki/usage.txt @@ -0,0 +1,800 @@ +# Markdown: Syntax + +[TOC] + +## Overview + +### Philosophy + +Markdown is intended to be as easy-to-read and easy-to-write as is feasible. + +Readability, however, is emphasized above all else. A Markdown-formatted +document should be publishable as-is, as plain text, without looking +like it's been marked up with tags or formatting instructions. While +Markdown's syntax has been influenced by several existing text-to-HTML +filters -- including [Setext] [1], [atx] [2], [Textile] [3], [reStructuredText] [4], +[Grutatext] [5], and [EtText] [6] -- the single biggest source of +inspiration for Markdown's syntax is the format of plain text email. + + [1]: http://docutils.sourceforge.net/mirror/setext.html + [2]: http://www.aaronsw.com/2002/atx/ + [3]: http://textism.com/tools/textile/ + [4]: http://docutils.sourceforge.net/rst.html + [5]: http://www.triptico.com/software/grutatxt.html + [6]: http://ettext.taint.org/doc/ + +To this end, Markdown's syntax is comprised entirely of punctuation +characters, which punctuation characters have been carefully chosen so +as to look like what they mean. E.g., asterisks around a word actually +look like \*emphasis\*. Markdown lists look like, well, lists. Even +blockquotes look like quoted passages of text, assuming you've ever +used email. + +### Automatic Escaping for Special Characters + +In HTML, there are two characters that demand special treatment: `<` +and `&`. Left angle brackets are used to start tags; ampersands are +used to denote HTML entities. If you want to use them as literal +characters, you must escape them as entities, e.g. `<`, and +`&`. + +Ampersands in particular are bedeviling for web writers. If you want to +write about 'AT&T', you need to write '`AT&T`'. You even need to +escape ampersands within URLs. Thus, if you want to link to: + + http://images.google.com/images?num=30&q=larry+bird + +you need to encode the URL as: + + http://images.google.com/images?num=30&q=larry+bird + +in your anchor tag `href` attribute. Needless to say, this is easy to +forget, and is probably the single most common source of HTML validation +errors in otherwise well-marked-up web sites. + +Markdown allows you to use these characters naturally, taking care of +all the necessary escaping for you. If you use an ampersand as part of +an HTML entity, it remains unchanged; otherwise it will be translated +into `&`. + +So, if you want to include a copyright symbol in your article, you can write: + + © + +and Markdown will leave it alone. But if you write: + + AT&T + +Markdown will translate it to: + + AT&T + +Similarly, because Markdown supports [inline HTML](#html), if you use +angle brackets as delimiters for HTML tags, Markdown will treat them as +such. But if you write: + + 4 < 5 + +Markdown will translate it to: + + 4 < 5 + +However, inside Markdown code spans and blocks, angle brackets and +ampersands are *always* encoded automatically. This makes it easy to use +Markdown to write about HTML code. (As opposed to raw HTML, which is a +terrible format for writing about HTML syntax, because every single `<` +and `&` in your example code needs to be escaped.) + + +* * * + + +## Block Elements + +### Paragraphs and Line Breaks + +A paragraph is simply one or more consecutive lines of text, separated +by one or more blank lines. (A blank line is any line that looks like a +blank line -- a line containing nothing but spaces or tabs is considered +blank.) Normal paragraphs should not be indented with spaces or tabs. + +The implication of the "one or more consecutive lines of text" rule is +that Markdown supports "hard-wrapped" text paragraphs. This differs +significantly from most other text-to-HTML formatters (including Movable +Type's "Convert Line Breaks" option) which translate every line break +character in a paragraph into a `
` tag. + +When you *do* want to insert a `
` break tag using Markdown, you +end a line with two or more spaces, then type return. + +Yes, this takes a tad more effort to create a `
`, but a simplistic +"every line break is a `
`" rule wouldn't work for Markdown. +Markdown's email-style [blockquoting][bq] and multi-paragraph [list items][l] +work best -- and look better -- when you format them with hard breaks. + + [bq]: #blockquote + [l]: #list + +### Headers + +Markdown supports two styles of headers, [Setext] [1] and [atx] [2]. + +Setext-style headers are "underlined" using equal signs (for first-level +headers) and dashes (for second-level headers). For example: + + This is an H1 + ============= + + This is an H2 + ------------- + + This is an H3 + _____________ + +Any number of underlining `=`'s or `-`'s will work. + +Atx-style headers use 1-6 hash characters at the start of the line, +corresponding to header levels 1-6. For example: + + # This is an H1 + + ## This is an H2 + + ###### This is an H6 + +Optionally, you may "close" atx-style headers. This is purely +cosmetic -- you can use this if you think it looks better. The +closing hashes don't even need to match the number of hashes +used to open the header. (The number of opening hashes +determines the header level.) : + + # This is an H1 # + + ## This is an H2 ## + + ### This is an H3 ###### + + +### Blockquotes + +Markdown uses email-style `>` characters for blockquoting. If you're +familiar with quoting passages of text in an email message, then you +know how to create a blockquote in Markdown. It looks best if you hard +wrap the text and put a `>` before every line: + + > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, + > consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. + > Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. + > + > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse + > id sem consectetuer libero luctus adipiscing. + +Markdown allows you to be lazy and only put the `>` before the first +line of a hard-wrapped paragraph: + + > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, + consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. + Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. + + > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse + id sem consectetuer libero luctus adipiscing. + +Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by +adding additional levels of `>`: + + > This is the first level of quoting. + > + > > This is nested blockquote. + > + > Back to the first level. + +Blockquotes can contain other Markdown elements, including headers, lists, +and code blocks: + + > ## This is a header. + > + > 1. This is the first list item. + > 2. This is the second list item. + > + > Here's some example code: + > + > return shell_exec("echo $input | $markdown_script"); + +Any decent text editor should make email-style quoting easy. For +example, with BBEdit, you can make a selection and choose Increase +Quote Level from the Text menu. + + +### Lists + +Markdown supports ordered (numbered) and unordered (bulleted) lists. + +Unordered lists use asterisks, pluses, and hyphens -- interchangably +-- as list markers: + + * Red + * Green + * Blue + +is equivalent to: + + + Red + + Green + + Blue + +and: + + - Red + - Green + - Blue + +Ordered lists use numbers followed by periods: + + 1. Bird + 2. McHale + 3. Parish + +It's important to note that the actual numbers you use to mark the +list have no effect on the HTML output Markdown produces. The HTML +Markdown produces from the above list is: + +
    +
  1. Bird
  2. +
  3. McHale
  4. +
  5. Parish
  6. +
+ +If you instead wrote the list in Markdown like this: + + 1. Bird + 1. McHale + 1. Parish + +or even: + + 3. Bird + 1. McHale + 8. Parish + +you'd get the exact same HTML output. The point is, if you want to, +you can use ordinal numbers in your ordered Markdown lists, so that +the numbers in your source match the numbers in your published HTML. +But if you want to be lazy, you don't have to. + +If you do use lazy list numbering, however, you should still start the +list with the number 1. At some point in the future, Markdown may support +starting ordered lists at an arbitrary number. + +List markers typically start at the left margin, but may be indented by +up to three spaces. List markers must be followed by one or more spaces +or a tab. + +To make lists look nice, you can wrap items with hanging indents: + + * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. + Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, + viverra nec, fringilla in, laoreet vitae, risus. + * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. + Suspendisse id sem consectetuer libero luctus adipiscing. + +But if you want to be lazy, you don't have to: + + * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. + Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, + viverra nec, fringilla in, laoreet vitae, risus. + * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. + Suspendisse id sem consectetuer libero luctus adipiscing. + +If list items are separated by blank lines, Markdown will wrap the +items in `

` tags in the HTML output. For example, this input: + + * Bird + * Magic + +will turn into: + +

    +
  • Bird
  • +
  • Magic
  • +
+ +But this: + + * Bird + + * Magic + +will turn into: + +
    +
  • Bird

  • +
  • Magic

  • +
+ +List items may consist of multiple paragraphs. Each subsequent +paragraph in a list item must be indented by either 4 spaces +or one tab: + + 1. This is a list item with two paragraphs. Lorem ipsum dolor + sit amet, consectetuer adipiscing elit. Aliquam hendrerit + mi posuere lectus. + + Vestibulum enim wisi, viverra nec, fringilla in, laoreet + vitae, risus. Donec sit amet nisl. Aliquam semper ipsum + sit amet velit. + + 2. Suspendisse id sem consectetuer libero luctus adipiscing. + +It looks nice if you indent every line of the subsequent +paragraphs, but here again, Markdown will allow you to be +lazy: + + * This is a list item with two paragraphs. + + This is the second paragraph in the list item. You're + only required to indent the first line. Lorem ipsum dolor + sit amet, consectetuer adipiscing elit. + + * Another item in the same list. + +To put a blockquote within a list item, the blockquote's `>` +delimiters need to be indented: + + * A list item with a blockquote: + + > This is a blockquote + > inside a list item. + +To put a code block within a list item, the code block needs +to be indented *twice* -- 8 spaces or two tabs: + + * A list item with a code block: + + + + +It's worth noting that it's possible to trigger an ordered list by +accident, by writing something like this: + + 1986. What a great season. + +In other words, a *number-period-space* sequence at the beginning of a +line. To avoid this, you can backslash-escape the period: + + 1986\. What a great season. + + + +### Code Blocks + +Pre-formatted code blocks are used for writing about programming or +markup source code. Rather than forming normal paragraphs, the lines +of a code block are interpreted literally. Markdown wraps a code block +in both `
` and `` tags.
+
+To produce a code block in Markdown, simply indent every line of the
+block by at least 4 spaces or 1 tab. For example, given this input:
+
+    This is a normal paragraph:
+
+        This is a code block.
+
+Markdown will generate:
+
+    

This is a normal paragraph:

+ +
This is a code block.
+    
+ +One level of indentation -- 4 spaces or 1 tab -- is removed from each +line of the code block. For example, this: + + Here is an example of AppleScript: + + tell application "Foo" + beep + end tell + +will turn into: + +

Here is an example of AppleScript:

+ +
tell application "Foo"
+        beep
+    end tell
+    
+ +A code block continues until it reaches a line that is not indented +(or the end of the article). + +Within a code block, ampersands (`&`) and angle brackets (`<` and `>`) +are automatically converted into HTML entities. This makes it very +easy to include example HTML source code using Markdown -- just paste +it and indent it, and Markdown will handle the hassle of encoding the +ampersands and angle brackets. For example, this: + + + +will turn into: + +
<div class="footer">
+        &copy; 2004 Foo Corporation
+    </div>
+    
+ +Regular Markdown syntax is not processed within code blocks. E.g., +asterisks are just literal asterisks within a code block. This means +it's also easy to use Markdown to write about Markdown's own syntax. + + + +### Horizontal Rules + +You can produce a horizontal rule tag (`
`) by placing three or +more hyphens, asterisks, or underscores on a line by themselves. If you +wish, you may use spaces between the hyphens or asterisks. Each of the +following lines will produce a horizontal rule: + + * * * + + *** + + ***** + + - - - + + --------------------------------------- + + +## Span Elements + +### Links + +Markdown supports two style of links: *inline* and *reference*. + +In both styles, the link text is delimited by [square brackets]. + +To create an inline link, use a set of regular parentheses immediately +after the link text's closing square bracket. Inside the parentheses, +put the URL where you want the link to point, along with an *optional* +title for the link, surrounded in quotes. For example: + + This is [an example](http://example.com/ "Title") inline link. + + [This link](http://example.net/) has no title attribute. + +Will produce: + +

This is + an example inline link.

+ +

This link has no + title attribute.

+ +If you're referring to a local resource on the same server, you can +use relative paths: + + See my [About](/about/) page for details. + +Reference-style links use a second set of square brackets, inside +which you place a label of your choosing to identify the link: + + This is [an example][id] reference-style link. + +You can optionally use a space to separate the sets of brackets: + + This is [an example] [id] reference-style link. + +Then, anywhere in the document, you define your link label like this, +on a line by itself: + + [id]: http://example.com/ "Optional Title Here" + +That is: + +* Square brackets containing the link identifier (optionally + indented from the left margin using up to three spaces); +* followed by a colon; +* followed by one or more spaces (or tabs); +* followed by the URL for the link; +* optionally followed by a title attribute for the link, enclosed + in double or single quotes, or enclosed in parentheses. + +The following three link definitions are equivalent: + + [foo]: http://example.com/ "Optional Title Here" + [foo]: http://example.com/ 'Optional Title Here' + [foo]: http://example.com/ (Optional Title Here) + +**Note:** There is a known bug in Markdown.pl 1.0.1 which prevents +single quotes from being used to delimit link titles. + +The link URL may, optionally, be surrounded by angle brackets: + + [id]: "Optional Title Here" + +You can put the title attribute on the next line and use extra spaces +or tabs for padding, which tends to look better with longer URLs: + + [id]: http://example.com/longish/path/to/resource/here + "Optional Title Here" + +Link definitions are only used for creating links during Markdown +processing, and are stripped from your document in the HTML output. + +Link definition names may consist of letters, numbers, spaces, and +punctuation -- but they are *not* case sensitive. E.g. these two +links: + + [link text][a] + [link text][A] + +are equivalent. + +The *implicit link name* shortcut allows you to omit the name of the +link, in which case the link text itself is used as the name. +Just use an empty set of square brackets -- e.g., to link the word +"Google" to the google.com web site, you could simply write: + + [Google][] + +And then define the link: + + [Google]: http://google.com/ + +Because link names may contain spaces, this shortcut even works for +multiple words in the link text: + + Visit [Daring Fireball][] for more information. + +And then define the link: + + [Daring Fireball]: http://daringfireball.net/ + +Link definitions can be placed anywhere in your Markdown document. I +tend to put them immediately after each paragraph in which they're +used, but if you want, you can put them all at the end of your +document, sort of like footnotes. + +Here's an example of reference links in action: + + I get 10 times more traffic from [Google] [1] than from + [Yahoo] [2] or [MSN] [3]. + + [1]: http://google.com/ "Google" + [2]: http://search.yahoo.com/ "Yahoo Search" + [3]: http://search.msn.com/ "MSN Search" + +Using the implicit link name shortcut, you could instead write: + + I get 10 times more traffic from [Google][] than from + [Yahoo][] or [MSN][]. + + [google]: http://google.com/ "Google" + [yahoo]: http://search.yahoo.com/ "Yahoo Search" + [msn]: http://search.msn.com/ "MSN Search" + +Both of the above examples will produce the following HTML output: + +

I get 10 times more traffic from Google than from + Yahoo + or MSN.

+ +For comparison, here is the same paragraph written using +Markdown's inline link style: + + I get 10 times more traffic from [Google](http://google.com/ "Google") + than from [Yahoo](http://search.yahoo.com/ "Yahoo Search") or + [MSN](http://search.msn.com/ "MSN Search"). + +The point of reference-style links is not that they're easier to +write. The point is that with reference-style links, your document +source is vastly more readable. Compare the above examples: using +reference-style links, the paragraph itself is only 81 characters +long; with inline-style links, it's 176 characters; and as raw HTML, +it's 234 characters. In the raw HTML, there's more markup than there +is text. + +With Markdown's reference-style links, a source document much more +closely resembles the final output, as rendered in a browser. By +allowing you to move the markup-related metadata out of the paragraph, +you can add links without interrupting the narrative flow of your +prose. + +### Emphasis + +Markdown treats asterisks (`*`) and underscores (`_`) as indicators of +emphasis. Text wrapped with one `*` or `_` will be wrapped with an +HTML `` tag; double `*`'s or `_`'s will be wrapped with an HTML +`` tag. E.g., this input: + + *single asterisks* + + _single underscores_ + + **double asterisks** + + __double underscores__ + +will produce: + + single asterisks + + single underscores + + double asterisks + + double underscores + +You can use whichever style you prefer; the lone restriction is that +the same character must be used to open and close an emphasis span. + +Emphasis can be used in the middle of a word: + + un*frigging*believable + +But if you surround an `*` or `_` with spaces, it'll be treated as a +literal asterisk or underscore. + +To produce a literal asterisk or underscore at a position where it +would otherwise be used as an emphasis delimiter, you can backslash +escape it: + + \*this text is surrounded by literal asterisks\* + + +### Code + +To indicate a span of code, wrap it with backtick quotes (`` ` ``). +Unlike a pre-formatted code block, a code span indicates code within a +normal paragraph. For example: + + Use the `printf()` function. + +will produce: + +

Use the printf() function.

+ +To include a literal backtick character within a code span, you can use +multiple backticks as the opening and closing delimiters: + + ``There is a literal backtick (`) here.`` + +which will produce this: + +

There is a literal backtick (`) here.

+ +The backtick delimiters surrounding a code span may include spaces -- +one after the opening, one before the closing. This allows you to place +literal backtick characters at the beginning or end of a code span: + + A single backtick in a code span: `` ` `` + + A backtick-delimited string in a code span: `` `foo` `` + +will produce: + +

A single backtick in a code span: `

+ +

A backtick-delimited string in a code span: `foo`

+ +With a code span, ampersands and angle brackets are encoded as HTML +entities automatically, which makes it easy to include example HTML +tags. Markdown will turn this: + + Please don't use any `` tags. + +into: + +

Please don't use any <blink> tags.

+ +You can write this: + + `—` is the decimal-encoded equivalent of `—`. + +to produce: + +

&#8212; is the decimal-encoded + equivalent of &mdash;.

+ + +### Images + +Admittedly, it's fairly difficult to devise a "natural" syntax for +placing images into a plain text document format. + +Markdown uses an image syntax that is intended to resemble the syntax +for links, allowing for two styles: *inline* and *reference*. + +Inline image syntax looks like this: + + ![Alt text](/path/to/img.jpg) + + ![Alt text](/path/to/img.jpg "Optional title") + +That is: + +* An exclamation mark: `!`; +* followed by a set of square brackets, containing the `alt` + attribute text for the image; +* followed by a set of parentheses, containing the URL or path to + the image, and an optional `title` attribute enclosed in double + or single quotes. + +Reference-style image syntax looks like this: + + ![Alt text][id] + +Where "id" is the name of a defined image reference. Image references +are defined using syntax identical to link references: + + [id]: url/to/image "Optional title attribute" + +As of this writing, Markdown has no syntax for specifying the +dimensions of an image; if this is important to you, you can simply +use regular HTML `` tags. + + +## Miscellaneous + +### Automatic Links + +Markdown supports a shortcut style for creating "automatic" links for URLs and email addresses: simply surround the URL or email address with angle brackets. What this means is that if you want to show the actual text of a URL or email address, and also have it be a clickable link, you can do this: + + + +Markdown will turn this into: + + http://example.com/ + +Automatic links for email addresses work similarly, except that +Markdown will also perform a bit of randomized decimal and hex +entity-encoding to help obscure your address from address-harvesting +spambots. For example, Markdown will turn this: + + + +into something like this: + + address@exa + mple.com + +which will render in a browser as a clickable link to "address@example.com". + +(This sort of entity-encoding trick will indeed fool many, if not +most, address-harvesting bots, but it definitely won't fool all of +them. It's better than nothing, but an address published in this way +will probably eventually start receiving spam.) + + + +### Backslash Escapes + +Markdown allows you to use backslash escapes to generate literal +characters which would otherwise have special meaning in Markdown's +formatting syntax. For example, if you wanted to surround a word +with literal asterisks (instead of an HTML `` tag), you can use +backslashes before the asterisks, like this: + + \*literal asterisks\* + +Markdown provides backslash escapes for the following characters: + + \ backslash + ` backtick + * asterisk + _ underscore + {} curly braces + [] square brackets + () parentheses + # hash mark + + plus sign + - minus sign (hyphen) + . dot + ! exclamation mark + diff --git a/simplewiki/views.py b/simplewiki/views.py new file mode 100644 index 0000000000..970731bc5f --- /dev/null +++ b/simplewiki/views.py @@ -0,0 +1,400 @@ +# -*- coding: utf-8 -*- +import types +from django.core.urlresolvers import get_callable +from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseServerError, HttpResponseForbidden, HttpResponseNotAllowed +from django.utils import simplejson +from django.shortcuts import get_object_or_404, render_to_response +from django.template import RequestContext, Context, loader +from django.utils.translation import ugettext_lazy as _ +from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required +from django.db.models import Q +from django.conf import settings + +from models import * +from settings import * + +def view(request, wiki_url): + + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + perm_err = check_permissions(request, article, check_read=True) + if perm_err: + return perm_err + c = RequestContext(request, {'wiki_article': article, + 'wiki_write': article.can_write_l(request.user), + 'wiki_attachments_write': article.can_attach(request.user), + } ) + return render_to_response('simplewiki_view.html', c) + +def root_redirect(request): + """ + Reason for redirecting: + The root article needs to to have a specific slug + in the URL, otherwise pattern matching in urls.py will get confused. + I've tried various methods to avoid this, but depending on Django/Python + versions, regexps have been greedy in two different ways.. so I just + skipped having problematic URLs like '/wiki/_edit' for editing the main page. + #benjaoming + """ + try: + root = Article.get_root() + except: + err = not_found(request, 'mainpage') + return err + + return HttpResponseRedirect(reverse('wiki_view', args=(root.slug,))) + +def create(request, wiki_url): + + url_path = get_url_path(wiki_url) + + if url_path != [] and url_path[0].startswith('_'): + c = RequestContext(request, {'wiki_err_keyword': True, + 'wiki_url': '/'.join(url_path) }) + return render_to_response('simplewiki_error.html', c) + + # Lookup path + try: + # Ensure that the path exists... + root = Article.get_root() + # Remove root slug if present in path + if url_path and root.slug == url_path[0]: + url_path = url_path[1:] + + path = Article.get_url_reverse(url_path[:-1], root) + if not path: + c = RequestContext(request, {'wiki_err_noparent': True, + 'wiki_url_parent': '/'.join(url_path[:-1]) }) + return render_to_response('simplewiki_error.html', c) + + perm_err = check_permissions(request, path[-1], check_locked=False, check_write=True) + if perm_err: + return perm_err + # Ensure doesn't already exist + article = Article.get_url_reverse(url_path, root) + if article: + return HttpResponseRedirect(reverse('wiki_view', args=(article[-1].get_url(),))) + + # TODO: Somehow this doesnt work... + #except ShouldHaveExactlyOneRootSlug, (e): + except: + if Article.objects.filter(parent=None).count() > 0: + return HttpResponseRedirect(reverse('wiki_view', args=('',))) + # Root not found... + path = [] + url_path = [""] + + if request.method == 'POST': + f = CreateArticleForm(request.POST) + if f.is_valid(): + article = Article() + article.slug = url_path[-1] + if not request.user.is_anonymous(): + article.created_by = request.user + article.title = f.cleaned_data.get('title') + if path != []: + article.parent = path[-1] + a = article.save() + new_revision = f.save(commit=False) + if not request.user.is_anonymous(): + new_revision.revision_user = request.user + new_revision.article = article + new_revision.save() + import django.db as db + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + else: + f = CreateArticleForm(initial={'title':request.GET.get('wiki_article_name', url_path[-1]), + 'contents':_('Headline\n===\n\n')}) + + c = RequestContext(request, {'wiki_form': f, + 'wiki_write': True, + }) + + return render_to_response('simplewiki_create.html', c) + +def edit(request, wiki_url): + + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + # Check write permissions + perm_err = check_permissions(request, article, check_write=True, check_locked=True) + if perm_err: + return perm_err + + if WIKI_ALLOW_TITLE_EDIT: + EditForm = RevisionFormWithTitle + else: + EditForm = RevisionForm + + if request.method == 'POST': + f = EditForm(request.POST) + if f.is_valid(): + new_revision = f.save(commit=False) + new_revision.article = article + # Check that something has actually been changed... + if not new_revision.get_diff(): + return (None, HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))) + if not request.user.is_anonymous(): + new_revision.revision_user = request.user + new_revision.save() + if WIKI_ALLOW_TITLE_EDIT: + new_revision.article.title = f.cleaned_data['title'] + new_revision.article.save() + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + else: + f = EditForm({'contents': article.current_revision.contents, 'title': article.title}) + c = RequestContext(request, {'wiki_form': f, + 'wiki_write': True, + 'wiki_article': article, + 'wiki_attachments_write': article.can_attach(request.user), + }) + + return render_to_response('simplewiki_edit.html', c) + +def history(request, wiki_url, page=1): + + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + perm_err = check_permissions(request, article, check_read=True) + if perm_err: + return perm_err + + page_size = 10 + + try: + p = int(page) + except ValueError: + p = 1 + + history = Revision.objects.filter(article__exact = article).order_by('-counter') + + if request.method == 'POST': + if request.POST.__contains__('revision'): + perm_err = check_permissions(request, article, check_write=True, check_locked=True) + if perm_err: + return perm_err + try: + r = int(request.POST['revision']) + article.current_revision = Revision.objects.get(id=r) + article.save() + except: + pass + finally: + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + + page_count = (history.count()+(page_size-1)) / page_size + if p > page_count: + p = 1 + beginItem = (p-1) * page_size + + next_page = p + 1 if page_count > p else None + prev_page = p - 1 if p > 1 else None + + c = RequestContext(request, {'wiki_page': p, + 'wiki_next_page': next_page, + 'wiki_prev_page': prev_page, + 'wiki_write': article.can_write_l(request.user), + 'wiki_attachments_write': article.can_attach(request.user), + 'wiki_article': article, + 'wiki_history': history[beginItem:beginItem+page_size],}) + + return render_to_response('simplewiki_history.html', c) + +def search_articles(request, wiki_url): + # blampe: We should check for the presence of other popular django search + # apps and use those if possible. Only fall back on this as a last resort. + # Adding some context to results (eg where matches were) would also be nice. + + # todo: maybe do some perm checking here + + if request.method == 'POST': + querystring = request.POST['value'].strip() + if querystring: + results = Article.objects.all() + for queryword in querystring.split(): + # Basic negation is as fancy as we get right now + if queryword[0] == '-' and len(queryword) > 1: + results._search = lambda x: results.exclude(x) + queryword = queryword[1:] + else: + results._search = lambda x: results.filter(x) + + results = results._search(Q(current_revision__contents__icontains = queryword) | \ + Q(title = queryword)) + else: + # Need to throttle results by splitting them into pages... + results = Article.objects.all() + + if results.count() == 1: + return HttpResponseRedirect(reverse('wiki_view', args=(results[0].get_url(),))) + else: + c = RequestContext(request, {'wiki_search_results': results, + 'wiki_search_query': querystring}) + return render_to_response('simplewiki_searchresults.html', c) + + return view(request, wiki_url) + +def search_add_related(request, wiki_url): + + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + perm_err = check_permissions(request, article, check_read=True) + if perm_err: + return perm_err + + search_string = request.GET.get('query', None) + self_pk = request.GET.get('self', None) + if search_string: + results = [] + related = Article.objects.filter(title__istartswith = search_string) + others = article.related.all() + if self_pk: + related = related.exclude(pk=self_pk) + if others: + related = related.exclude(related__in = others) + related = related.order_by('title')[:10] + for item in related: + results.append({'id': str(item.id), + 'value': item.title, + 'info': item.get_url()}) + else: + results = [] + + json = simplejson.dumps({'results': results}) + return HttpResponse(json, mimetype='application/json') + +def add_related(request, wiki_url): + + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + perm_err = check_permissions(request, article, check_write=True, check_locked=True) + if perm_err: + return perm_err + + try: + related_id = request.POST['id'] + rel = Article.objects.get(id=related_id) + has_already = article.related.filter(id=related_id).count() + if has_already == 0 and not rel == article: + article.related.add(rel) + article.save() + except: + pass + finally: + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + +def remove_related(request, wiki_url, related_id): + + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + perm_err = check_permissions(request, article, check_write=True, check_locked=True) + if perm_err: + return perm_err + + try: + rel_id = int(related_id) + rel = Article.objects.get(id=rel_id) + article.related.remove(rel) + article.save() + except: + pass + finally: + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + +def random_article(request, wiki_url): + from random import randint + num_arts = Article.objects.count() + article = Article.objects.all()[randint(0, num_arts-1)] + return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + +def encode_err(request, url): + return render_to_response('simplewiki_error.html', + RequestContext(request, {'wiki_err_encode': True})) + +def not_found(request, wiki_url): + """Generate a NOT FOUND message for some URL""" + return render_to_response('simplewiki_error.html', + RequestContext(request, {'wiki_err_notfound': True, + 'wiki_url': wiki_url})) + +def get_url_path(url): + """Return a list of all actual elements of a url, safely ignoring + double-slashes (//) """ + return filter(lambda x: x!='', url.split('/')) + +def fetch_from_url(request, url): + """Analyze URL, returning the article and the articles in its path + If something goes wrong, return an error HTTP response""" + + err = None + article = None + path = None + + url_path = get_url_path(url) + + try: + root = Article.get_root() + except: + err = not_found(request, '') + return (article, path, err) + + if url_path and root.slug == url_path[0]: + url_path = url_path[1:] + + path = Article.get_url_reverse(url_path, root) + if not path: + err = not_found(request, '/' + '/'.join(url_path)) + else: + article = path[-1] + return (article, path, err) + + +def check_permissions(request, article, check_read=False, check_write=False, check_locked=False): + read_err = check_read and not article.can_read(request.user) + write_err = check_write and not article.can_write(request.user) + locked_err = check_locked and article.locked + + if read_err or write_err or locked_err: + c = RequestContext(request, {'wiki_article': article, + 'wiki_err_noread': read_err, + 'wiki_err_nowrite': write_err, + 'wiki_err_locked': locked_err,}) + # TODO: Make this a little less jarring by just displaying an error + # on the current page? (no such redirect happens for an anon upload yet) + # benjaoming: I think this is the nicest way of displaying an error, but + # these errors shouldn't occur, but rather be prevented on the other pages. + return render_to_response('simplewiki_error.html', c) + else: + return None + +#################### +# LOGIN PROTECTION # +#################### + +if WIKI_REQUIRE_LOGIN_VIEW: + view = login_required(view) + history = login_required(history) + search_related = login_required(search_related) + wiki_encode_err = login_required(wiki_encode_err) + +if WIKI_REQUIRE_LOGIN_EDIT: + create = login_required(create) + edit = login_required(edit) + add_related = login_required(add_related) + remove_related = login_required(remove_related) + +if WIKI_CONTEXT_PREPROCESSORS: + settings.TEMPLATE_CONTEXT_PROCESSORS = settings.TEMPLATE_CONTEXT_PROCESSORS + WIKI_CONTEXT_PREPROCESSORS diff --git a/simplewiki/views_attachments.py b/simplewiki/views_attachments.py new file mode 100644 index 0000000000..47eb09a0b2 --- /dev/null +++ b/simplewiki/views_attachments.py @@ -0,0 +1,145 @@ +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404 +from django.template import loader, Context +from django.db.models.fields.files import FieldFile +from django.core.servers.basehttp import FileWrapper +from django.contrib.auth.decorators import login_required + +from settings import * +from models import Article, ArticleAttachment, get_attachment_filepath +from views import not_found, check_permissions, get_url_path, fetch_from_url + +import os +from simplewiki.settings import WIKI_ALLOW_ANON_ATTACHMENTS + + +def add_attachment(request, wiki_url): + + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + perm_err = check_permissions(request, article, check_write=True, check_locked=True) + if perm_err: + return perm_err + + if not WIKI_ALLOW_ATTACHMENTS or (not WIKI_ALLOW_ANON_ATTACHMENTS and request.user.is_anonymous()): + return HttpResponseForbidden() + + if request.method == 'POST': + if request.FILES.__contains__('attachment'): + attachment = ArticleAttachment() + if not request.user.is_anonymous(): + attachment.uploaded_by = request.user + attachment.article = article + + file = request.FILES['attachment'] + file_rel_path = get_attachment_filepath(attachment, file.name) + chunk_size = request.upload_handlers[0].chunk_size + + filefield = FieldFile(attachment, attachment.file, file_rel_path) + attachment.file = filefield + + file_path = WIKI_ATTACHMENTS_ROOT + attachment.file.name + + if not request.POST.__contains__('overwrite') and os.path.exists(file_path): + c = Context({'overwrite_warning' : True, + 'wiki_article': article, + 'filename': file.name}) + t = loader.get_template('simplewiki_updateprogressbar.html') + return HttpResponse(t.render(c)) + + if file.size > WIKI_ATTACHMENTS_MAX: + c = Context({'too_big' : True, + 'max_size': WIKI_ATTACHMENTS_MAX, + 'wiki_article': article, + 'file': file}) + t = loader.get_template('simplewiki_updateprogressbar.html') + return HttpResponse(t.render(c)) + + def get_extension(fname): + return attachment.file.name.split('.')[-2] + if WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS and not \ + get_extension(attachment.file.name) in WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS: + c = Context({'extension_err' : True, + 'extensions': WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS, + 'wiki_article': article, + 'file': file}) + t = loader.get_template('simplewiki_updateprogressbar.html') + return HttpResponse(t.render(c)) + + # Remove existing attachments + # TODO: Move this until AFTER having removed file. + # Current problem is that Django's FileField delete() method + # automatically deletes files + for a in article.attachments(): + if file_rel_path == a.file.name: + a.delete() + def receive_file(): + destination = open(file_path, 'wb+') + size = file.size + cnt = 0 + c = Context({'started' : True,}) + t = loader.get_template('simplewiki_updateprogressbar.html') + yield t.render(c) + for chunk in file.chunks(): + cnt += 1 + destination.write(chunk) + c = Context({'progress_width' : (cnt*chunk_size) / size, + 'wiki_article': article,}) + t = loader.get_template('simplewiki_updateprogressbar.html') + yield t.render(c) + c = Context({'finished' : True, + 'wiki_article': article,}) + t = loader.get_template('simplewiki_updateprogressbar.html') + destination.close() + attachment.save() + yield t.render(c) + + return HttpResponse(receive_file()) + + return HttpResponse('') + +# Taken from http://www.djangosnippets.org/snippets/365/ +def send_file(request, filepath): + """ + Send a file through Django without loading the whole file into + memory at once. The FileWrapper will turn the file object into an + iterator for chunks of 8KB. + """ + filename = filepath + wrapper = FileWrapper(file(filename)) + response = HttpResponse(wrapper, content_type='text/plain') + response['Content-Length'] = os.path.getsize(filename) + return response + +def view_attachment(request, wiki_url, file_name): + + (article, path, err) = fetch_from_url(request, wiki_url) + if err: + return err + + perm_err = check_permissions(request, article, check_read=True) + if perm_err: + return perm_err + + attachment = None + for a in article.attachments(): + if get_attachment_filepath(a, file_name) == a.file.name: + attachment = a + + if attachment: + filepath = WIKI_ATTACHMENTS_ROOT + attachment.file.name + if os.path.exists(filepath): + return send_file(request, filepath) + + raise Http404() + +#################### +# LOGIN PROTECTION # +#################### + +if WIKI_REQUIRE_LOGIN_VIEW: + view_attachment = login_required(view_attachment) + +if WIKI_REQUIRE_LOGIN_EDIT or not WIKI_ALLOW_ANON_ATTACHMENTS: + add_attachment = login_required(add_attachment) diff --git a/urls.py b/urls.py index a1ddcbf87b..823326432c 100644 --- a/urls.py +++ b/urls.py @@ -6,6 +6,7 @@ import django.contrib.auth.views # admin.autodiscover() urlpatterns = patterns('', + (r'^wiki/', include('simplewiki.urls')), url(r'^courseware/(?P[^/]*)/(?P[^/]*)/(?P
[^/]*)/$', 'courseware.views.index'), url(r'^courseware/(?P[^/]*)/(?P[^/]*)/$', 'courseware.views.index'), url(r'^courseware/(?P[^/]*)/$', 'courseware.views.index'),