Commits

David Chambers committed 4f32521

Added `Document` model (for posts and pages). Added code to convert settings from `str` to `unicode`.

Comments (0)

Files changed (19)

examples/4=>mango-markdown-syntax.text

 "||", this will cause the updated section to be parsed by the update.dhtml template. The default template will simply
 wrap this in a div with a class of "update", allowing you to style it with css however you choose
 
-|| Update - see it in action here (Look at the source html, there is no styling by default at the moment)
+|| See it in action here (Look at the source html, there is no styling by default at the moment)
         return kwargs
 
     required = {
-        'title':        lambda: unicode(SITE_TITLE, 'utf-8'),
-        'link':         lambda: unicode(BASE_URL, 'utf-8'),
+        'title':        lambda: SITE_TITLE,
+        'link':         lambda: BASE_URL,
         'description':  lambda: u'', # required by constructor, but does not affect output
     }
     optional = {
-        'author_name':  lambda: unicode(PRIMARY_AUTHOR_NAME, 'utf-8'),
-        'author_email': lambda: unicode(PRIMARY_AUTHOR_EMAIL, 'utf-8'),
-        'author_link':  lambda: unicode(PRIMARY_AUTHOR_URL, 'utf-8'),
+        'author_name':  lambda: PRIMARY_AUTHOR_NAME,
+        'author_email': lambda: PRIMARY_AUTHOR_EMAIL,
+        'author_link':  lambda: PRIMARY_AUTHOR_URL,
         'feed_url':     lambda: feed_url,
-        'feed_guid':    lambda: unicode(BASE_URL, 'utf-8'),
+        'feed_guid':    lambda: BASE_URL,
     }
 
     kwargs = all_kwargs(required, optional)
     todo = FEED_MAX_POSTS
     all = todo == 0
 
-    for post in utils.posts():
+    for post in utils.documents():
         if todo or all:
             required = {
-                'title':       lambda: post['meta']['title'],
-                'link':        lambda: post['canon_urls']['abs'],
-                'description': lambda: post['html'],
+                'title':       lambda: post.title,
+                'link':        lambda: post.urls['canon']['abs'],
+                'description': lambda: post.html,
             }
             optional = {
-                'author_name': lambda: post['meta']['author'],
-                'pubdate':     lambda: post['meta']['datetime'],
-                'unique_id':   lambda: post['canon_urls']['abs'],
+                'author_name': lambda: post.meta['author'],
+                'pubdate':     lambda: post.datetime,
+                'unique_id':   lambda: post.urls['canon']['abs'],
             }
             kwargs = all_kwargs(required, optional, post)
             if kwargs:
-# This file is blank, it must be here as django fails to run tests if there is no model file present (see ticket 7189)
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
 
+import datetime
+import re
 
-"""
+import markdown
+import pytz
 
->>> 1 + 1 == 2
-True
+from django.conf import settings
+from django.template import Context, loader
 
-"""
+from mango.settings import *
+
+block = r'(?m)^(%s(?=[ \n])[^\n]*(\n|$))+'
+match = r'(?m)^%s(?=[ \n]) ?'
+
+RE = {
+    '\r\n?': re.compile(r'\r\n?'),
+    'alias=>canon': re.compile(r'^(0*(?P<alias>.*?)=>)?(?P<canon>.+)$'),
+    'excerpt': re.compile(block % r'\|'),
+    'excerpt_pipes': re.compile(match % r'\|'),
+    'fragment': re.compile(r'(?s)(<code>.*?</code>|<pre>.*?</pre>)'),
+    'hand-crafted': re.compile(r' {,3}\S+:.*(\n[ \t]*\S+:.*)*\n{2,}(?P<excerpt>(\|(?=[ \n])[^\n]*\n)+)'),
+    'heading': re.compile(r'(?m)\s*<(h[1-6])[^>]*>(?P<title>.+?)</\1>$(?P<html>[\s\S]*)'),
+    'inline': re.compile(r'(\[[^\]]+\]\(/(?P<path>\S+?)\)\s*){{\s*filesize\s*}}'),
+    'ref-style': re.compile(r'(\[(?P<id>[^\]]+)\]\s*){{\s*filesize\s*}}'),
+    'replacements': (
+        (re.compile(r'(?<!\\)\.\.\.(?!\.)'), u'\u2026'),     # ... -> ellipsis
+        (re.compile(r' -- '), u'\u2009\u2014\u2009'),        # [space][hyphen][hyphen][space] -> [thin space][em dash][thin space]
+        (re.compile(r'(?<!\\)&lt;&lt;(?!&lt;)'), u'\u00AB'), # << -> «
+        (re.compile(r'(?<!\\)&gt;&gt;(?!&gt;)'), u'\u00BB'), # >> -> »
+    ),
+    'snippet': re.compile(r'(?s)^<(code|pre)>.*?</\1>$'),
+    'update': re.compile(block % r'\|\|'),
+    'update_pipes': re.compile(match % r'\|\|'),
+}
+
+md = markdown.Markdown(extensions=('meta',) + MARKDOWN_EXTENSIONS)
+update_template = loader.get_template('update.dhtml')
+
+class Document:
+    def __init__(self,
+                 body,
+                 urls=None,
+                 kind=None,
+                 title=None,
+                 utc_datetime=None,
+                 excerpt=None,
+                 html=None):
+
+        self.body = body
+        self.urls = urls
+        self.type = kind
+        self.title = title
+        self.datetime = utc_datetime
+        self.excerpt = excerpt
+        self.html = html
+
+    def convert(self):
+        self.body = re.sub(RE['\r\n?'], '\n', self.body) + '\n' # keep regular expressions as simple as possible
+        self.body = re.sub(RE['ref-style'], # e.g. Download the [icon set][1] {{ filesize }}.
+                lambda m: m.group(1) + print_filesize(id_to_path(m.group('id'), text), plaintext=plaintext), self.body)
+        self.body = re.sub(RE['inline'], # e.g. Download the [icon set](/downloads/icon-set.zip) {{ filesize }}.
+                lambda m: m.group(1) + print_filesize(m.group('path'), plaintext=plaintext), self.body)
+
+        # don't touch self.body beyond this point
+        body = self.body
+
+        # excerpts
+        snippets = []
+        match = re.match(RE['hand-crafted'], body)
+        if match:
+            capture = match.group('excerpt')
+            snippets.append(re.sub(RE['excerpt_pipes'], u'', capture))
+            body = body.replace(capture, u'')
+        for match in re.finditer(RE['excerpt'], body):
+            capture = match.group(0)
+            snippets.append(re.sub(RE['excerpt_pipes'], u'', capture))
+            body = body.replace(capture, u'')
+        self.excerpt = md.convert('\n\n'.join(snippets))
+
+        # updates
+        for match in re.finditer(RE['update'], body):
+            capture = match.group(0)
+            update = Document(body=re.sub(RE['update_pipes'], u'', capture))
+            context = Context({'update': update.convert()})
+            body = body.replace(capture, update_template.render(context))
+
+        self.html = md.convert(body)
+        self.meta = getattr(md, 'Meta', {})
+        for key, value in self.meta.items():
+            self.meta[key] = value
+            if len(value) == 1: # note: `value` is always a list
+                if key in META_LISTS:
+                    self.meta[key] = value[0].split(', ')
+                else:
+                    self.meta[key] = value[0]
+
+        if 'date' in self.meta and 'time' in self.meta:
+            tz = pytz.timezone(settings.TIME_ZONE)
+            dt_format = u'%s %s' % (MARKDOWN_DATE_FORMAT, MARKDOWN_TIME_FORMAT)
+            try:
+                self.datetime = tz.localize(datetime.datetime.strptime('%s %s' % (
+                        self.meta['date'], self.meta['time']), dt_format)).astimezone(pytz.utc)
+            except ValueError: # date and/or time incorrectly formatted
+                pass
+
+        self.title = self.meta.get('title', u'')
+        if not self.title:
+            match = re.match(RE['heading'], self.html)
+            if match:
+                self.title = match.group('title')
+                self.html = match.group('html')
+
+        if REPLACEMENTS: # perform replacements on HTML so that code snippets are not affected
+            fragments = re.split(RE['fragment'], self.html)
+            self.html = u''
+            for fragment in fragments:
+                if not re.match(RE['snippet'], fragment):
+                    for pattern, replacement in RE['replacements']:
+                        fragment = re.sub(pattern, replacement, fragment)
+                self.html += fragment
+
+        self.excerpt = self.excerpt or self.html
+        self.type = self.meta.get('type', 'post' if self.datetime else 'page')
+
+        return self
+
+    def set_urls(self, filepath):
+
+        canon_fragments = [u'', u'']
+        alias_fragments = [u'', u'']
+
+        head, tail = os.path.split(os.path.realpath(filepath))
+        match = re.match(RE['alias=>canon'], tail)
+        if match:
+            canon = os.path.splitext(match.group('canon'))[0] # strip extension
+            canon_fragments.insert(1, canon)
+            alias_fragments.insert(1, match.group('alias') or canon)
+
+        while tail:
+            head, tail = os.path.split(head)
+            match = re.match(RE['alias=>canon'], tail)
+            if match:
+                canon = match.group('canon')
+                canon_fragments.insert(1, canon)
+                alias_fragments.insert(1, match.group('alias') or canon)
+
+        canon_path = u'/'.join(canon_fragments).replace(UNIX_PATH_TO_POSTS, u'', 1)
+        short_path = u'/'.join(alias_fragments).replace(UNIX_PATH_TO_POSTS, u'', 1)
+
+        self.urls = {
+            'canon': {'rel': canon_path, 'abs': BASE_URL + canon_path},
+            'short': {'rel': short_path, 'abs': SHORT_URL_BASE + short_path},
+        }
+
+        return self
+
+    def __unicode__(self):
+        return self.title

settings/__init__.py

 dirname = os.path.dirname(__file__)
 
 execfile(os.path.join(dirname, 'defaults.py'))
-
 try:
     execfile(os.path.join(dirname, 'custom.py'))
 except IOError:
     pass
 
-BASE_URL = BASE_URL.rstrip('/')
-PATH_TO_POSTS = os.path.expanduser(PATH_TO_POSTS.rstrip('/'))
-PATH_TO_STATIC = os.path.expanduser(PATH_TO_STATIC.rstrip('/'))
+BASE_URL = unicode(BASE_URL.rstrip('/'), 'utf-8')
+
+DISPLAY_DATE_FORMAT = unicode(DISPLAY_DATE_FORMAT, 'utf-8')
+DISPLAY_TIME_FORMAT = unicode(DISPLAY_TIME_FORMAT, 'utf-8')
+
+DISQUS_API_VERSION = unicode(DISQUS_API_VERSION, 'utf-8')
+
+MARKDOWN_DATE_FORMAT = unicode(MARKDOWN_DATE_FORMAT, 'utf-8')
+MARKDOWN_TIME_FORMAT = unicode(MARKDOWN_TIME_FORMAT, 'utf-8')
+
+PATH_TO_POSTS = os.path.expanduser(unicode(PATH_TO_POSTS.rstrip('/'), 'utf-8'))
+PATH_TO_STATIC = os.path.expanduser(unicode(PATH_TO_STATIC.rstrip('/'), 'utf-8'))
+
+PRIMARY_AUTHOR_NAME = unicode(PRIMARY_AUTHOR_NAME, 'utf-8')
+PRIMARY_AUTHOR_EMAIL = unicode(PRIMARY_AUTHOR_EMAIL, 'utf-8')
+PRIMARY_AUTHOR_URL = unicode(PRIMARY_AUTHOR_URL, 'utf-8')
+
+SHORT_URL_BASE = unicode(SHORT_URL_BASE.rstrip('/'), 'utf-8') if SHORT_URL_BASE else BASE_URL
+
+SITE_TITLE = unicode(SITE_TITLE, 'utf-8')
+
+UNIX_PATH_TO_POSTS = PATH_TO_POSTS
+if not UNIX_PATH_TO_POSTS.startswith('/'):
+    UNIX_PATH_TO_POSTS = os.path.join(os.path.split(os.path.split(dirname)[0])[0],
+            *UNIX_PATH_TO_POSTS.split('/'))
+    fragments = []
+    head, tail = os.path.split(UNIX_PATH_TO_POSTS)
+    while tail:
+        fragments.insert(0, tail)
+        head, tail = os.path.split(head)
+    UNIX_PATH_TO_POSTS = u'/%s' % '/'.join(fragments)
+    del fragments, head, tail
+
+del dirname

settings/defaults.py

 CONTACT_FORM = True
 
 # http://docs.python.org/library/datetime.html#strftime-and-strptime-behavior
-DISPLAY_DATE_FORMAT = MARKDOWN_DATE_FORMAT = '%d %B %Y' # e.g. 2 April 2010
-DISPLAY_TIME_FORMAT = MARKDOWN_TIME_FORMAT = '%I:%M%p'  # e.g. 6:50pm
+DISPLAY_DATE_FORMAT = '%d %B %Y' # e.g. 2 April 2010
+DISPLAY_TIME_FORMAT = '%i:%M\u2009%p' # e.g. 6:50|pm (pipe = thin space)
 
 DISQUS_API_VERSION = '1.1'
 
 
 KILOBYTE_SIZE = 1000
 
+MARKDOWN_DATE_FORMAT = DISPLAY_DATE_FORMAT
+MARKDOWN_TIME_FORMAT = '%I:%M%p' # e.g. 6:50pm
+
 # http://www.freewisdom.org/projects/python-markdown/Available_Extensions
 MARKDOWN_EXTENSIONS = (
     'def_list', # definition lists

templates/archives.dhtml

 {% load mango_extras %}
 {% block title %}Archives{% endblock %}
 {% block content %}
-				<h1>Archives</h1>
-			{% if archives %}
-				<ol class="archives">
-				{% for year, month, these_posts in archives %}
+				<h1>Archives</h1>{% if archives %}
+				<ol class="archives">{% for year, month, posts in archives %}
 					<li>
 						<h2>{{ month|month }} {{ year }}</h2>
-						<ol>
-					{% for post in these_posts %}
-							<li><a href="{{ post.canon_urls.abs }}">{{ post.meta.title }}</a> (<time datetime="{{ post.meta.datetime|datetime }}">{{ post.meta.datetime|display_date }}</time>)</li>
-					{% endfor %}
+						<ol>{% for post in posts %}
+							<li><a href="{{ post.urls.canon.abs }}">{{ post.title }}</a> (<time datetime="{{ post.datetime|datetime }}">{{ post.datetime|display_date }}</time>)</li>{% endfor %}
 						</ol>
-					</li>
-				{% endfor %}
-				</ol>
-			{% endif %}
+					</li>{% endfor %}
+				</ol>{% endif %}
 {% endblock %}

templates/base.dhtml

 <html>
 <head>
 	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
-	<title>{% block title %}{{ settings.SITE_TITLE }}{% endblock %}</title>
-{% for stylesheet in stylesheets %}
-	<link rel="stylesheet" type="text/css" href="{{ stylesheet.href }}" media="{{ stylesheet.media }}" />
-{% endfor %}
+	<title>{% block title %}{{ settings.SITE_TITLE }}{% endblock %}</title>{% for stylesheet in stylesheets %}
+	<link rel="stylesheet" type="text/css" href="{{ stylesheet.href }}" media="{{ stylesheet.media }}" />{% endfor %}
 </head>
 <body>
 	<div id="skip">
 		<header>
 			{% block header_title %}<a href="/">{{ settings.SITE_TITLE }}</a>{% endblock %}
 		</header>
-		<div id="main">
-{% block content %}
-{% endblock %}
-		</div><!--/main-->
-		<div id="scripts">
-{% for script in scripts %}
-			<script src="{{ script.src }}"></script>
-{% endfor %}
-		</div><!--/scripts-->
+		<div id="main">{% block content %}{% endblock %}
+		</div><!--/main-->{% if scripts %}
+		<div id="scripts">{% for script in scripts %}
+			<script src="{{ script.src }}"></script>{% endfor %}
+		</div><!--/scripts-->{% endif %}
 	</div><!--/wrap-->
 	<footer>
 		<p>Powered by <a href="http://mango.io/">Mango</a>.</p>

templates/category.dhtml

 {% extends 'base.dhtml' %}
 {% load mango_extras %}
 {% block content %}
-			<h1>{{ name|capfirst }}</h1>
-	{% for post in category %}{% include 'excerpt.dhtml' %}{% empty %}
-			<p>This category is currently empty.</p>
-	{% endfor %}
+			<h1>{{ name|capfirst }}</h1>{% for document in category %}{% include 'excerpt.dhtml' %}{% empty %}
+			<p>This category is currently empty.</p>{% endfor %}
 {% endblock %}

templates/comment.dhtml

 {% load mango_extras %}
 					<article>
 						<div>
-							{{ comment.message|html|safe }}
+							{{ comment.message|convert|safe }}
 						</div>
-						<footer>
-						{% if comment.is_anonymous %}
-							<img alt="" src="{{ comment.anonymous_author.email_hash|gravatar }}" />
-							{% if comment.anonymous_author.url %}
-							<strong><a href="{{ comment.anonymous_author.url }}">{{ comment.anonymous_author.name }}</a></strong>
-							{% else %}
-							<strong>{{ comment.anonymous_author.name }}</strong>
-							{% endif %}
-						{% else %}
-							<img alt="" src="{{ comment.author.email_hash|gravatar }}" />
-							{% if comment.author.url %}
-							<strong><a href="{{ comment.author.url }}">{% firstof comment.author.display_name comment.author.username %}</a></strong>
-							{% else %}
-							<strong>{{ comment.author.name }}</strong>
-							{% endif %}
-						{% endif %}
+						<footer>{% if comment.is_anonymous %}
+							<img alt="" src="{{ comment.anonymous_author.email_hash|gravatar }}" />{% if comment.anonymous_author.url %}
+							<strong><a href="{{ comment.anonymous_author.url }}">{{ comment.anonymous_author.name }}</a></strong>{% else %}
+							<strong>{{ comment.anonymous_author.name }}</strong>{% endif %}{% else %}
+							<img alt="" src="{{ comment.author.email_hash|gravatar }}" />{% if comment.author.url %}
+							<strong><a href="{{ comment.author.url }}">{% firstof comment.author.display_name comment.author.username %}</a></strong>{% else %}
+							<strong>{{ comment.author.name }}</strong>{% endif %}{% endif %}
 							<time datetime="{{ comment.created_at|isoformat }}">
 								<span>{{ comment.created_at|display_date }}</span>
 								<span>{{ comment.created_at|display_time }}</span>

templates/contact.dhtml

 {% extends 'base.dhtml' %}
 {% block title %}Contact{% endblock %}
 {% block content %}
-			<h1>Contact</h1>
-	{% if form %}
+			<h1>Contact</h1>{% if form %}
 			<form method="post">
 				<fieldset>
 					<div>{% with form.sender_name as field %}{% with form.fields.sender_name as attributes %}
 					<input type="checkbox" id="cc_sender" name="cc_sender"{% if field.data %} checked="checked"{% endif %} />
 				</div>{% endwith %}
 				<div><input type="submit" value="Send message" /></div>
-			</form>
-	{% else %}
-			<p>Thanks for your message. I'll do my best to respond in the not too distant future.</p>
-	{% endif %}
+			</form>{% else %}
+			<p>Thanks for your message. I'll do my best to respond in the not too distant future.</p>{% endif %}
 {% endblock %}

templates/excerpt.dhtml

 {% load mango_extras %}
 			<article>
 				<header>
-					<h2><a href="{{ post.canon_urls.abs }}">{{ post.meta.title|safe }}</a></h2>{% if post.meta.datetime %}
-					<time datetime="{{ post.meta.datetime|datetime }}">{{ post.meta.datetime|display_date }}</time>{% endif %}
+					<h2><a href="{{ document.urls.canon.abs }}">{{ document.title|safe }}</a></h2>{% if document.datetime %}
+					<time datetime="{{ document.datetime|datetime }}">{{ document.datetime|display_date }}</time>{% endif %}
 				</header>
-				{{ post.excerpt|safe }}
+				{{ document.excerpt|safe }}
 			</article>

templates/index.dhtml

 {% extends 'base.dhtml' %}
 {% load mango_extras %}
 {% block header_title %}<h1><a href="/">{{ settings.SITE_TITLE }}</a></h1>{% endblock %}
-{% block content %}
-	{% for post in posts %}{% if forloop.counter <= 5 %}{% include 'excerpt.dhtml' %}{% else %}{% if forloop.last %}
+{% block content %}{% for document in documents %}{% if forloop.counter <= 5 %}{% include 'excerpt.dhtml' %}{% else %}{% if forloop.last %}
 			<h2>Want more?</h2>
-			<p>Check out the <a href="{% url mango.views.archives %}">archives</a>.</p>
-	{% endif %}{% endif %}{% empty %}
+			<p>Check out the <a href="{% url mango.views.archives %}">archives</a>.</p>{% endif %}{% endif %}{% empty %}
 			<h2>Welcome to your new blog</h2>
 			<p>It's lookin' a bit empty, though. Get to work!</p>
-	{% endfor %}
-{% endblock %}
+{% endfor %}{% endblock %}

templates/post.dhtml

 {% block content %}
 				<article>
 					<header>
-						<h1>{{ meta.title }}</h1>
-					{% if meta.datetime %}
-						<time datetime="{{ meta.datetime|datetime }}">{{ meta.datetime|display_date }}</time>
-					{% endif %}{% ifequal type 'post' %}
+						<h1>{{ document.title }}</h1>{% if document.datetime %}
+						<time datetime="{{ document.datetime|datetime }}">{{ document.datetime|display_date }}</time>{% endif %}{% ifnotequal document.type 'page' %}
 						<dl>
 							<dt>Short URL</dt>
-							<dd><a href="{{ short_urls.abs }}">{{ short_urls.abs }}</a></dd>
+							<dd><a href="{{ document.urls.short.abs }}">{{ document.urls.short.abs }}</a></dd>
 							<dt>Markdown</dt>
-							<dd><a href="{{ short_urls.abs }}m/">{{ short_urls.abs }}m/</a></dd>
-						</dl>
-					{% endifequal %}
+							<dd><a href="{{ document.urls.short.abs }}m/">{{ document.urls.short.abs }}m/</a></dd>
+						</dl>{% endifnotequal %}
 					</header>
-					{{ html|safe }}
-					{% if meta.tags %}
+					{{ document.html|safe }}{% if document.meta.tags %}
 					<footer class="tags">
 						<h4 class="structural">This post has the following tags:</h4>
-						<ol>
-						{% for tag in meta.tags %}
-							<li><a href="{% url mango.views.tagged_as tag|slugify %}">{{ tag }}</a></li>
-						{% endfor %}
+						<ol>{% for tag in document.meta.tags %}
+							<li><a href="{% url mango.views.tagged_as tag|slugify %}">{{ tag }}</a></li>{% endfor %}
 						</ol>
-					</footer>
-					{% endif %}{% if comments or new_comment %}
-					<h2 id="comments">Comments</h2>
-					{% for comment in comments %}{% include 'comment.dhtml' %}{% endfor %}{% if new_comment %}{{ new_comment }}
-					<p>Your comment is awaiting moderation.</p>
-					{% endif %}{% endif %}{% if thread.allow_comments %}
+					</footer>{% endif %}{% if comments or new_comment %}
+					<h2 id="comments">Comments</h2>{% for comment in comments %}{% include 'comment.dhtml' %}{% endfor %}{% if new_comment %}{{ new_comment }}
+					<p>Your comment is awaiting moderation.</p>{% endif %}{% endif %}{% if thread.allow_comments %}
 					<h3 id="respond">Respond</h3>
 					<form method="post">
 						<fieldset id="author-details">
 							<textarea id="message" name="message" cols="50" rows="10">{% if this.data %}{{ this.data }}{% endif %}</textarea>
 						</div>{% endwith %}{% endwith %}
 						<div><input type="submit" value="Submit comment" /></div>
-					</form>
-					{% endif %}
+					</form>{% endif %}
 				</article>
 {% endblock %}

templates/tag.dhtml

 {% extends 'base.dhtml' %}
 {% load mango_extras %}
 {% block content %}
-			<h1>Posts tagged "{{ tag }}"</h1>
-	{% for post in tagged %}{% include 'excerpt.dhtml' %}{% empty %}
-			<p>There are currently no posts tagged "{{ tag }}".</p>
-	{% endfor %}
+			<h1>Posts tagged "{{ tag }}"</h1>{% for document in tagged %}{% include 'excerpt.dhtml' %}{% empty %}
+			<p>There are currently no posts tagged "{{ tag }}".</p>{% endfor %}
 {% endblock %}

templates/tags.dhtml

 {% load mango_extras %}
 {% block title %}Tags{% endblock %}
 {% block header_title %}<h1>Tags</h1>{% endblock %}
-{% block content %}
-	{% if tags %}
-			<dl id="tags">
-		{% for tag, count in tags %}
+{% block content %}{% if tags %}
+			<dl id="tags">{% for tag, count in tags %}
 				<dt><a href="{% url mango.views.tagged_as tag|slugify %}">{{ tag }}</a></dt>
-				<dd>{{ count }}</dd>
-		{% endfor %}
-			</dl>
-	{% else %}
-			<p>There are no tags to display.</p>
-	{% endif %}
+				<dd>{{ count }}</dd>{% endfor %}
+			</dl>{% else %}
+			<p>There are no tags to display.</p>{% endif %}
 {% endblock %}

templates/update.dhtml

 {% load mango_extras %}
 <div class="update">
-<h4>Update{% if meta.datetime %} — <time datetime="{{ meta.datetime|datetime }}">{{ meta.datetime|display_date }}</time>{% endif %}</h4>
-{{ html|safe }}
+<h4>Update{% if update.datetime %} — <time datetime="{{ update.datetime|datetime }}">{{ update.datetime|display_date }}</time>{% endif %}</h4>
+{{ update.html|safe }}
 </div>

templatetags/mango_extras.py

 import time
 import urllib
 
+import markdown
 import pytz
 
 from django import template
 from django.template.defaultfilters import stringfilter
-from mango import utils
 from mango.settings import *
 
+md = markdown.Markdown() # don't use Markdown extensions for comments
+
 register = template.Library()
 
 @register.filter
 
 @register.filter
 @stringfilter
-def html(markdown):
-    return utils.parse_text(markdown)['html']
+def convert(string):
+    return md.convert(string)
 #!/usr/bin/python
 # -*- coding: utf-8 -*-
 
-import datetime
 import os
 import re
 
-import markdown
-import pytz
+from django.core.cache import cache
 
-from django.conf import settings
-from django.core.cache import cache
-from django.template import Context, loader
-import mango.settings
+from mango.models import Document
 from mango.settings import *
 
-block = r'(?m)^(%s(?=[ \n])[^\n]*(\n|$))+'
-match = r'(?m)^%s(?=[ \n]) ?'
-
-RE = {
-    '\r\n?':         re.compile(r'\r\n?'),
-
-    'replacements': (
-        (re.compile(r'(?<!\\)\.\.\.(?!\.)'), u'\u2026'),     # ... -> ellipsis
-        (re.compile(r' -- '), u'\u2009\u2014\u2009'),        # [space][hyphen][hyphen][space] -> [thin space][em dash][thin space]
-        (re.compile(r'(?<!\\)&lt;&lt;(?!&lt;)'), u'\u00AB'), # << -> «
-        (re.compile(r'(?<!\\)&gt;&gt;(?!&gt;)'), u'\u00BB'), # >> -> »
-    ),
-    'heading':       re.compile(r'(?m)\s*<(h[1-6])[^>]*>(?P<title>.+?)</\1>$(?P<html>[\s\S]*)'),
-
-    # excerpts
-    'hand-crafted':  re.compile(r' {,3}\S+:.*(\n[ \t]*\S+:.*)*\n{2,}(?P<excerpt>(\|(?=[ \n])[^\n]*\n)+)'),
-    'excerpt':       re.compile(block % r'\|'),
-    'excerpt_pipes': re.compile(match % r'\|'),
-
-    # updates
-    'update':        re.compile(block % r'\|\|'),
-    'update_pipes':  re.compile(match % r'\|\|'),
-
-    # {{ filesize }} following internal links
-    'ref-style':     re.compile(r'(\[(?P<id>[^\]]+)\]\s*){{\s*filesize\s*}}'),
-    'inline':        re.compile(r'(\[[^\]]+\]\(/(?P<path>\S+?)\)\s*){{\s*filesize\s*}}'),
-
-    'alias=>canon':  re.compile(r'^(0*(?P<alias>.*?)=>)?(?P<canon>.+)$'),
-}
-
 def id_to_path(identifier, text):
     """
     Finds the path after the [X] identifier in the text (used for filesize in our Markdown files)
     >>> path.endswith('/downloads/package.zip')
     True
     """
-    m = re.search(r'(?m)^ {,3}\[' + identifier + r'\]:\s+/(\S+)', text)
+    m = re.search(r'(?m)^ {,3}\[%s\]:\s+/(\S+)' % identifier, text)
     try:
         path = m.group(1)
     except AttributeError:
     u"date:\\t13 April ... **Congratulations!**\\n"
     """
     f = open(filepath)
-    u = f.read().decode('utf-8')
+    u = unicode(f.read(), 'utf-8')
     f.close()
     return u
 
     except:
         return u'' # fail silently
 
-def parse_text(text):
-    t = loader.get_template('update.dhtml')
-    for match in re.finditer(RE['update'], text):
-        capture = match.group(0)
-        c = Context(parse_text(re.sub(RE['update_pipes'], '', capture)))
-        text = text.replace(capture, t.render(c))
-    md = markdown.Markdown(extensions = ('meta',) + MARKDOWN_EXTENSIONS)
-    html = md.convert(text)
-
-    if REPLACEMENTS: # perform replacements on HTML so that code snippets are not affected
-        fragments = re.split(r'(?s)(<code>.*?</code>|<pre>.*?</pre>)', html)
-        html = ''
-        for fragment in fragments:
-            if not re.match(r'(?s)^<(code|pre)>.*?</\1>$', fragment):
-                for key, value in dict(RE['replacements']).items():
-                    fragment = re.sub(key, value, fragment)
-            html += fragment
-
-    meta = {}
-    if hasattr(md, 'Meta'): # this needs to be checked as this doesn't exist if the file was empty
-        for key, value in md.Meta.items(): # note: every item in md.Meta.items() is a list
-            meta[key] = value
-            if len(value) == 1:
-                if key in META_LISTS:
-                    meta[key] = value[0].split(', ')
-                else:
-                    meta[key] = value[0]
-
-    if meta.has_key('date') and meta.has_key('time'):
-        tz = pytz.timezone(settings.TIME_ZONE)
-        dt_format = ' '.join([MARKDOWN_DATE_FORMAT, MARKDOWN_TIME_FORMAT])
-        try:
-            meta['datetime'] = tz.localize(datetime.datetime.strptime(' '.join([meta['date'], meta['time']]), dt_format)).astimezone(pytz.utc)
-        except ValueError: # date and/or time incorrectly formatted
-            meta['datetime'] = None
-
-    # Changed to process the HTML as this will also detect if the Markdown has an HTML header tag (in legal Markdown format).
-    # It also allows the setting of the title in meta in which case nothing is altered.
-    if not meta.has_key('title'):
-        m = re.match(RE['heading'], html)
-        if m:
-            meta['title'] = m.group('title')
-            html = m.group('html')
-
-    return {'meta': meta, 'html': html}
-
-
-#TODO - look into whether updates can be placed inside excerpts correctly
-def parse_file(filepath, plaintext=False):
-    """
-    Returns the parsed text of the given string as a string. If plaintext only the mango replacements are done
-    
-    >>> parsed = parse_file('mango/examples/1=>my-first-post.text')
-    >>> parsed['html']
-    u"\\n<p>Welcome to Mango. ... <strong>Congratulations!</strong></p>"
-    >>> parsed['meta']['title']
-    u'My First Post'
-    >>> parsed['excerpt']
-    u"\\n<p>Welcome to Mango. ... <strong>Congratulations!</strong></p>"
-    >>> parse_file('mango/examples/1=>my-first-post.text', True)
-    u"date:\\t13 April ... **Congratulations!**\\n\\n"
-    """
-    # Looks to see if we have a cached entry for it.
-    # Cached data is in the form {'data': data, 'mod_time': time}.
-    cache_key = u'%s%s' % ('plaintext:' if plaintext else '', filepath) # ':' is illegal character in file names =)
-    c = cache.get(cache_key)
-    if c and c['mod_time'] == os.path.getmtime(filepath):
-        return c['data']
-
-    text = get_contents(filepath)
-    text = re.sub(RE['\r\n?'], '\n', text) + '\n' # keep regular expressions as simple as possible
-    text = re.sub(RE['ref-style'],
-                    lambda m: m.group(1) + print_filesize(id_to_path(m.group('id'), text), plaintext=plaintext),
-                    text) # e.g. Download the [tiny calendar icon set][1] {{ filesize }}.
-    text = re.sub(RE['inline'],
-                    lambda m: m.group(1) + print_filesize(m.group('path'), plaintext=plaintext),
-                    text) # e.g. Download the [tiny calendar icon set](/downloads/tiny-calendar-icon-set.zip) {{ filesize }}.
-
-    if plaintext:
-        cache.set(cache_key, {'data': text, 'mod_time': os.path.getmtime(filepath)}, POST_CACHE_SECONDS)
-        return text
-
-    excerpt = ''
-    match = re.match(RE['hand-crafted'], text)
-    if match:
-        capture = match.group('excerpt')
-        excerpt = parse_text(re.sub(RE['excerpt_pipes'], '', capture))['html'] + '\n'
-        text = text.replace(capture, '')
-    else:
-        for match in re.finditer(RE['excerpt'], text):
-            capture = match.group(0)
-            snippet = parse_text(re.sub(RE['excerpt_pipes'], '', capture))['html']
-            excerpt += snippet + '\n'
-            text = text.replace(capture, snippet)
-
-    data = parse_text(text)
-    data['excerpt'] = excerpt if excerpt else data['html']
-    cache.set(cache_key, {'data': data, 'mod_time': os.path.getmtime(filepath)}, POST_CACHE_SECONDS)
-    return data
-
-def absolute_path_to_posts():
-    """
-    Returns the Unix style absolute path to the posts directory
-    """
-    path_to_posts = mango.settings.PATH_TO_POSTS
-    if not path_to_posts.startswith('/'):
-        #TODO - comment this
-        path_to_this = os.path.split(__file__)[0] # strip /utils.py
-        project_path = os.path.split(path_to_this)[0] # strip /mango
-        path_to_posts = os.path.join(project_path, *path_to_posts.split(u'/'))
-        fragments = [u'']
-        head, tail = os.path.split(path_to_posts)
-        while tail:
-            fragments.insert(1, tail)
-            head, tail = os.path.split(head)
-        path_to_posts = u'/'.join(fragments)
-    return path_to_posts
-
-def post_urls(filepath):
-    """
-    Returns a post's short and canonical URLs
-    
-    >>> path_to_posts = mango.utils.absolute_path_to_posts()
-    >>> setattr(mango.settings, 'BASE_URL', 'http://example.com/')
-    >>> setattr(mango.settings, 'SHORT_URL_BASE', 'http://✪df.ws/')
-    >>> post_urls(os.path.join(path_to_posts, '01=>my-first-post.text'))
-    ({'abs': u'http://\u272adf.ws/1/', 'rel': u'/1/'}, {'abs': u'http://example.com/my-first-post/', 'rel': u'/my-first-post/'})
-    >>> setattr(mango.settings, 'SHORT_URL_BASE', '')
-    >>> post_urls(os.path.join(path_to_posts, '01=>my-first-post.text'))
-    ({'abs': u'http://example.com/1/', 'rel': u'/1/'}, {'abs': u'http://example.com/my-first-post/', 'rel': u'/my-first-post/'})
-    >>> post_urls(os.path.join(path_to_posts,
-    ...         'js=>javascript', 'libs=>libraries', 'prototype.js', '$.text'))
-    ({'abs': u'http://example.com/js/libs/prototype.js/$/', 'rel': u'/js/libs/prototype.js/$/'}, {'abs': u'http://example.com/javascript/libraries/prototype.js/$/', 'rel': u'/javascript/libraries/prototype.js/$/'})
-    """
-    canon_fragments = [u'', u'']
-    alias_fragments = [u'', u'']
-
-    head, tail = os.path.split(os.path.abspath(filepath))
-    match = re.match(RE['alias=>canon'], tail)
-    if match:
-        canon = os.path.splitext(match.group('canon'))[0] # strip extension
-        canon_fragments.insert(1, canon)
-        alias_fragments.insert(1, match.group('alias') or canon)
-
-    while tail:
-        head, tail = os.path.split(head)
-        match = re.match(RE['alias=>canon'], tail)
-        if match:
-            canon = match.group('canon')
-            canon_fragments.insert(1, canon)
-            alias_fragments.insert(1, match.group('alias') or canon)
-
-    path_to_posts = absolute_path_to_posts()
-    base_url = mango.settings.BASE_URL.decode('utf-8').rstrip(u'/')
-    short_url_base = mango.settings.SHORT_URL_BASE.decode('utf-8').rstrip(u'/') or base_url
-
-    short_path = u'/'.join(alias_fragments).replace(path_to_posts, u'', 1)
-    canon_path = u'/'.join(canon_fragments).replace(path_to_posts, u'', 1)
-
-    short_urls = {'rel': short_path, 'abs': short_url_base + short_path}
-    canon_urls = {'rel': canon_path, 'abs': base_url + canon_path}
-
-    return (short_urls, canon_urls)
-
 def get_posts(path_to_posts, include_pages=False, reverse=True):
     """
     Returns all of the posts in the directory and all directories below it
     
-    >>> get_posts('mango/examples')[1]['html']
+    >>> get_posts('mango/examples')[1].html
     u"\\n<p>Welcome to Mango. ... <strong>Congratulations!</strong></p>"
     """
     documents = []
             absolute_path = os.path.abspath(joined_path)
             # ignore symlink if it points to a file (post) in same directory
             if absolute_path == os.path.realpath(joined_path):
-                this = parse_file(absolute_path)
-                short_urls, canon_urls = post_urls(absolute_path)
-                this['short_urls'] = short_urls
-                this['canon_urls'] = canon_urls
-                documents.append(this)
+                document = Document(get_contents(absolute_path))
+                document.convert().set_urls(absolute_path)
+                documents.append(document)
 
     posts = []
     pages = []
     for document in documents:
-        if document['meta'].get('datetime'):
+        if document.type == 'page':
+            pages.append(document)
+        else:
             posts.append(document)
-        else:
-            pages.append(document)
 
-    posts.sort(key=lambda post: post['meta']['datetime'], reverse=reverse)
-    pages.sort(key=lambda page: page['meta'].get('title'))
+    posts.sort(key=lambda post: post.datetime, reverse=reverse)
+    pages.sort(key=lambda page: page.title)
 
     if include_pages:
         return pages + posts
 
     return posts
 
-def posts(path_to_posts=PATH_TO_POSTS, include_pages=False):
+def documents(path_to_posts=PATH_TO_POSTS, include_pages=False):
     """
     Simple wrapper for `get_posts` which returns cached posts if appropriate
     
-    >>> posts('mango/examples')[1]['html']
+    >>> documents('mango/examples')[1].html
     u"\\n<p>Welcome to Mango. ... <strong>Congratulations!</strong></p>"
     """
     cache_key = u'posts%s:%s' % ('+pages' if include_pages else '', path_to_posts)
     True
     >>> month == 4
     True
-    >>> these_posts[0]['html']
+    >>> these_posts[0].html
     u"\\n<p>Welcome to Mango. ... <strong>Congratulations!</strong></p>"
     """
     cache_key = u'archives:%s' % path_to_posts
     archives = []
     posts = get_posts(path_to_posts)
     if posts:
-        dt = posts[0]['meta']['datetime']
-        year, month = dt.year, dt.month
+        year = posts[0].datetime.year
+        month = posts[0].datetime.month
 
         these_posts = []
         for post in posts:
-            dt = post['meta']['datetime']
-            this_year, this_month = dt.year, dt.month
-
-            if this_year == year and this_month == month:
+            if post.datetime.year == year and post.datetime.month == month:
                 these_posts.append(post)
             else:
                 archives.append((year, month, these_posts))
-                year, month = this_year, this_month
+                year, month = post.datetime.year, post.datetime.month
                 these_posts = [post]
-
         archives.append((year, month, these_posts))
 
     cache.set(cache_key, archives, INDEX_CACHE_SECONDS)
     Returns the email address of the primary author as set in the settings file
     
     >>> primary_author_email()
-    '... <...@...>'
+    u'... <...@...>'
     """
     name = PRIMARY_AUTHOR_NAME
     email = PRIMARY_AUTHOR_EMAIL
 from django.core.urlresolvers import reverse
 from django.http import Http404, HttpResponse, HttpResponseRedirect
 from django.shortcuts import render_to_response
-from django.template import Context, loader
 from django.template.loader import render_to_string
 
 from mango import settings, utils
 from mango.forms import CommentForm, ContactForm
+from mango.models import Document, RE
 from mango.settings import *
 from mango.templatetags.mango_extras import slugify
-from mango.utils import convert_html_chars, RE
+from mango.utils import convert_html_chars
 
 MESSAGES = {
     'approve': {'do': 'Approve comment', 'done': 'Comment approved.'},
     return HttpResponseRedirect(reverse('mango.views.post', args=[path]))
 
 def context_defaults(request):
-    path_to_css, css = settings.__dict__.get('CSS', (None, []))
-    path_to_js, js = settings.__dict__.get('JS', (None, []))
+    path_to_css, css = getattr(settings, 'CSS', (None, []))
+    path_to_js, js = getattr(settings, 'JS', (None, []))
     return {
         'archives': utils.archives(),
-        'posts': utils.posts(),
+        'documents': utils.documents(),
         'settings': settings,
         'stylesheets': [dict(media=media, href=path_to_css.lstrip('.')+filename) for media, stylesheets in css for filename in stylesheets],
         'scripts': [dict(src=path_to_js.lstrip('.')+filename) for filename in js],
 
 def tags(request):
     tags = {}
-    for post in utils.posts():
-        for tag in post['meta'].get('tags', []):
-            if tag in tags:
-                tags[tag] += 1
-            else:
-                tags[tag] = 1
+    for document in utils.documents():
+        for tag in document.meta.get('tags', []):
+            tags[tag] = tags.get(tag, 0) + 1
     tags = [(key, value) for key, value in sorted(tags.items(), key=lambda pair: pair[0].lower())]
     return render_to_response('tags.dhtml', dict({'tags': tags}, **context_defaults(request)))
 
     tag = slugify(tag)
     return render_to_response('tag.dhtml', dict({
                 'tag': tag,
-                'tagged': [p for p in utils.posts() if tag in [slugify(t) for t in p['meta'].get('tags', [])]],
+                'tagged': [doc for doc in utils.documents() if tag in [slugify(t) for t in doc.meta.get('tags', [])]],
             }, **context_defaults(request)))
 
 #TODO - this name should be changed as it now is also an index page, should this be combined with the index post?
 def post(request, path, view_source=False):
-    def render_post(filepath):
-        context = utils.parse_file(filepath)
-        form = CommentForm()
-        thread = None
-        comment = request.session.get('comment')
-        if comment:
-            del request.session['comment']
-            t = loader.get_template('comment.dhtml')
-            comment = t.render(Context({'comment': comment}))
-
-        def get_forum(forums):
-            shortname = DISQUS['shortname']
-            for forum in forums:
-                if forum.shortname == shortname:
-                    return forum
-            raise disqus.APIError
-
-        comments = []
-        #TODO - This will need to be updated, I don't currently have a DISQUS account so can't test, but I've changed the identifier
-        if settings.__dict__.get('DISQUS'):
-            try:
-                dsq = disqus.DisqusService(DISQUS_API_VERSION)
-                dsq.login(DISQUS.get('api_key'))
-                forum = get_forum(dsq.get_forum_list())
-                #This call is only here for legacy and migration support, it will never return a value on Mango made threads
-                thread = dsq.get_thread_by_url(forum, canon_urls['abs'])
-                if not thread:
-                    thread = dsq.thread_by_identifier(forum, context['meta']['title'], canon_urls['rel'])['thread']
-
-                if request.method == 'POST':
-                    form = CommentForm(request.POST)
-                    if form.is_valid():
-                        author_name = form.cleaned_data['author_name']
-                        author_email = form.cleaned_data['author_email']
-                        author_url = form.cleaned_data['author_url']
-                        message = form.cleaned_data['message']
-                        subscribe = form.cleaned_data['subscribe']
-
-                        try:
-                            ip_address = urllib.urlopen('http://www.whatismyip.com/automation/n09230945.asp').readlines()[0]
-                        except:
-                            ip_address = None
-
-                        # send request to Disqus
-                        comment = dsq.create_post(forum, thread, message=message, ip_address=ip_address,
-                                author_name=author_name, author_email=author_email, author_url=author_url)
-
-                        # store comment so that it can be displayed to the author even if withheld for moderation
-                        request.session['comment'] = comment
-
-                        # send e-mail notification
-                        author = u'%s <%s>' % (author_name, author_email)
-                        subject = u'[%s] Comment: "%s"' % (SITE_TITLE.decode('utf-8'), context['meta']['title'])
-
-                        query_string = u'api_key=%s&post_id=%s' % (
-                                hashlib.sha1(DISQUS['api_key']).hexdigest(), comment.id)
-
-                        text_content = u'%s wrote:\n\n%s' % (author, message)
-                        html_content = u'<p>%s wrote:</p>' % convert_html_chars(author)
-                        html_content += u'<blockquote>%s</blockquote><ul>' % re.sub(r'([ \t]*\n){2,}', '</p><p>',
-                                convert_html_chars(message))
-                        for action, messages in sorted(MESSAGES.items()):
-                            text_content += u'\n\n* %s: %s/moderate/%s?%s' % (
-                                    messages['do'], BASE_URL, action, query_string)
-                            html_content += u'<li><a href="%s/moderate/%s?%s">%s</a></li>' % (
-                                    BASE_URL, action, convert_html_chars(query_string), messages['do'])
-                        text_content += u'\n\n* Respond: %s/%s/#respond' % (BASE_URL, path)
-                        html_content += u'<li><a href="%s/%s/#respond">Respond</a></li></ul>' % (BASE_URL, path)
-
-                        msg = EmailMultiAlternatives(subject, text_content, author, [utils.primary_author_email()])
-                        msg.attach_alternative(html_content, 'text/html')
-                        msg.send(fail_silently=False)
-
-                        return HttpResponseRedirect('redirect/')
-
-                for c in dsq.get_thread_posts(forum, thread, limit=9999, exclude='killed'):
-                    if c.has_been_moderated:
-                        comments.append(c)
-                if DISQUS.get('sort') == 'oldest_first':
-                    comments.sort(key=lambda comment: comment.created_at)
-            except disqus.APIError:
-                pass
-
-        template = context['meta'].get('type', 'post' if 'datetime' in context['meta'] else 'page')
-        return render_to_string('%s.dhtml' % template, dict(context, **dict({
-                    'comments': comments,
-                    'form': form,
-                    'new_comment': comment,
-                    'short_urls': short_urls,
-                    'thread': thread,
-                }, **context_defaults(request))))
-
-    filepath = os.path.join(os.sep, *utils.absolute_path_to_posts().split('/'))
+    filepath = UNIX_PATH_TO_POSTS
     is_short = False
     fragments = path.split('/')
     last_index = len(fragments) - 1
     if not found:
         raise Http404
 
-    short_urls, canon_urls = utils.post_urls(filepath)
-
-    if is_short:
-        if view_source:
-            canon_urls['abs'] += 'm/'
-        return HttpResponseRedirect(canon_urls['abs'])
-
     if os.path.isdir(filepath):
         #TODO - if view_source is true what do we do here?
         #TODO - caching
         match = re.match(RE['alias=>canon'], os.path.split(filepath)[1])
-        html = render_to_response('category.dhtml', dict({
+        return render_to_response('category.dhtml', dict({
             'name': match.group('canon'),
-            'category': utils.posts(filepath, include_pages=True),
+            'category': utils.documents(filepath, include_pages=True),
         }, **context_defaults(request)))
-        return HttpResponse(html)
+
+    document = Document(utils.get_contents(filepath))
+    document.convert().set_urls(filepath)
+
+    if is_short:
+        url = document.urls['canon']['abs']
+        if view_source:
+            url += 'm/'
+        return HttpResponseRedirect(url)
 
     if os.path.islink(filepath): # symbolic link - must appear before view source
-        short_urls, canon_urls = utils.post_urls(os.path.realpath(filepath))
-        return HttpResponseRedirect(canon_urls['rel'])
+        return HttpResponseRedirect(document.urls['canon']['rel'])
 
     if view_source: # Return the plain text of the Markdown page.
-        text = utils.parse_file(filepath, plaintext=True)
-        return HttpResponse(text, content_type='text/plain; charset=utf-8')
+        return HttpResponse(document.body, content_type='text/plain; charset=utf-8')
 
     # Parse the Markdown and return it through the Django template.
-    value = render_post(filepath)
-    if isinstance(value, HttpResponseRedirect):
-        return value
+    form = CommentForm()
+    thread = None
+    comment = request.session.get('comment')
+    if comment:
+        del request.session['comment']
+        comment = render_to_string('comment.dhtml', {'comment': comment})
 
-    return HttpResponse(value)
+    def get_forum(forums):
+        shortname = DISQUS['shortname']
+        for forum in forums:
+            if forum.shortname == shortname:
+                return forum
+        raise disqus.APIError
+
+    comments = []
+    if getattr(settings, 'DISQUS'):
+        try:
+            dsq = disqus.DisqusService(DISQUS_API_VERSION)
+            dsq.login(DISQUS.get('api_key'))
+            forum = get_forum(dsq.get_forum_list())
+            # This call is only here for legacy and migration support, it will never return a value on Mango made threads
+            thread = dsq.get_thread_by_url(forum, document.urls['canon']['abs'])
+            if not thread:
+                thread = dsq.thread_by_identifier(forum, document.title, document.urls['canon']['rel'])['thread']
+
+            if request.method == 'POST':
+                form = CommentForm(request.POST)
+                if form.is_valid():
+                    author_name = form.cleaned_data['author_name']
+                    author_email = form.cleaned_data['author_email']
+                    author_url = form.cleaned_data['author_url']
+                    message = form.cleaned_data['message']
+                    subscribe = form.cleaned_data['subscribe']
+
+                    try:
+                        ip_address = urllib.urlopen('http://www.whatismyip.com/automation/n09230945.asp').readlines()[0]
+                    except:
+                        ip_address = None
+
+                    # send request to Disqus
+                    comment = dsq.create_post(forum, thread, message=message, ip_address=ip_address,
+                            author_name=author_name, author_email=author_email, author_url=author_url)
+
+                    # store comment so that it can be displayed to the author even if withheld for moderation
+                    request.session['comment'] = comment
+
+                    # send e-mail notification
+                    author = u'%s <%s>' % (author_name, author_email)
+                    subject = u'[%s] Comment: "%s"' % (SITE_TITLE, document.title)
+
+                    query_string = u'api_key=%s&post_id=%s' % (
+                            hashlib.sha1(DISQUS['api_key']).hexdigest(), comment.id)
+
+                    text_content = u'%s wrote:\n\n%s' % (author, message)
+                    html_content = u'<p>%s wrote:</p>' % convert_html_chars(author)
+                    html_content += u'<blockquote>%s</blockquote><ul>' % re.sub(r'([ \t]*\n){2,}', '</p><p>',
+                            convert_html_chars(message))
+                    for action, messages in sorted(MESSAGES.items()):
+                        text_content += u'\n\n* %s: %s/moderate/%s?%s' % (
+                                messages['do'], BASE_URL, action, query_string)
+                        html_content += u'<li><a href="%s/moderate/%s?%s">%s</a></li>' % (
+                                BASE_URL, action, convert_html_chars(query_string), messages['do'])
+                    text_content += u'\n\n* Respond: %s/%s/#respond' % (BASE_URL, path)
+                    html_content += u'<li><a href="%s/%s/#respond">Respond</a></li></ul>' % (BASE_URL, path)
+
+                    msg = EmailMultiAlternatives(subject, text_content, author, [utils.primary_author_email()])
+                    msg.attach_alternative(html_content, 'text/html')
+                    msg.send(fail_silently=False)
+
+                    return HttpResponseRedirect('redirect/')
+
+            for c in dsq.get_thread_posts(forum, thread, limit=9999, exclude='killed'):
+                if c.has_been_moderated:
+                    comments.append(c)
+            if DISQUS.get('sort') == 'oldest_first':
+                comments.sort(key=lambda comment: comment.created_at)
+        except disqus.APIError:
+            pass
+
+    return render_to_response('%s.dhtml' % document.type, dict({
+                'document': document,
+                'comments': comments,
+                'form': form,
+                'new_comment': comment,
+                'thread': thread,
+            }, **context_defaults(request)))
 
 def contact(request, message_sent=False):
     if message_sent: