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 0000000000..a0d43aaf0e Binary files /dev/null and b/simplewiki/media/css/img_inquisitor/_source/as_pointer.png differ diff --git a/simplewiki/media/css/img_inquisitor/_source/li_corner.png b/simplewiki/media/css/img_inquisitor/_source/li_corner.png new file mode 100644 index 0000000000..ba5d875fbd Binary files /dev/null and b/simplewiki/media/css/img_inquisitor/_source/li_corner.png differ diff --git a/simplewiki/media/css/img_inquisitor/_source/ul_corner.png b/simplewiki/media/css/img_inquisitor/_source/ul_corner.png new file mode 100644 index 0000000000..44da270877 Binary files /dev/null and b/simplewiki/media/css/img_inquisitor/_source/ul_corner.png differ 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 0000000000..dbc21220f6 Binary files /dev/null and b/simplewiki/media/css/img_inquisitor/as_pointer.gif differ 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 0000000000..b701d01c93 Binary files /dev/null and b/simplewiki/media/css/img_inquisitor/hl_corner_bl.gif differ 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 0000000000..11debd7fec Binary files /dev/null and b/simplewiki/media/css/img_inquisitor/hl_corner_br.gif differ 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 0000000000..1c2bbaf7a7 Binary files /dev/null and b/simplewiki/media/css/img_inquisitor/hl_corner_tl.gif differ diff --git a/simplewiki/media/css/img_inquisitor/hl_corner_tr.gif b/simplewiki/media/css/img_inquisitor/hl_corner_tr.gif new file mode 100644 index 0000000000..38b1eff9ad Binary files /dev/null and b/simplewiki/media/css/img_inquisitor/hl_corner_tr.gif differ 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 0000000000..4fa8c8e11b Binary files /dev/null and b/simplewiki/media/css/img_inquisitor/ul_corner_bl.gif differ 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 0000000000..f589431b49 Binary files /dev/null and b/simplewiki/media/css/img_inquisitor/ul_corner_br.gif differ diff --git a/simplewiki/media/css/img_inquisitor/ul_corner_tl.gif b/simplewiki/media/css/img_inquisitor/ul_corner_tl.gif new file mode 100644 index 0000000000..1fb65ec324 Binary files /dev/null and b/simplewiki/media/css/img_inquisitor/ul_corner_tl.gif differ diff --git a/simplewiki/media/css/img_inquisitor/ul_corner_tr.gif b/simplewiki/media/css/img_inquisitor/ul_corner_tr.gif new file mode 100644 index 0000000000..e49fb2e92d Binary files /dev/null and b/simplewiki/media/css/img_inquisitor/ul_corner_tr.gif differ diff --git a/simplewiki/media/img/box_corner_bl.gif b/simplewiki/media/img/box_corner_bl.gif new file mode 100644 index 0000000000..484047b80a Binary files /dev/null and b/simplewiki/media/img/box_corner_bl.gif differ diff --git a/simplewiki/media/img/box_corner_br.gif b/simplewiki/media/img/box_corner_br.gif new file mode 100644 index 0000000000..524d800d99 Binary files /dev/null and b/simplewiki/media/img/box_corner_br.gif differ diff --git a/simplewiki/media/img/box_corner_tl.gif b/simplewiki/media/img/box_corner_tl.gif new file mode 100644 index 0000000000..2d3225319e Binary files /dev/null and b/simplewiki/media/img/box_corner_tl.gif differ diff --git a/simplewiki/media/img/box_corner_tr.gif b/simplewiki/media/img/box_corner_tr.gif new file mode 100644 index 0000000000..ec4363f5ed Binary files /dev/null and b/simplewiki/media/img/box_corner_tr.gif differ diff --git a/simplewiki/media/img/delete.gif b/simplewiki/media/img/delete.gif new file mode 100644 index 0000000000..e39a406d52 Binary files /dev/null and b/simplewiki/media/img/delete.gif differ diff --git a/simplewiki/media/img/delete_grey.gif b/simplewiki/media/img/delete_grey.gif new file mode 100644 index 0000000000..aaba7aa1c8 Binary files /dev/null and b/simplewiki/media/img/delete_grey.gif differ 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'),