constance

Scripts for generating (an earlier obsolete version of) my personal web site
git clone https://code.djc.id.au/git/constance/
commit fd7fd94994a1a6ebfd0e7db25418fe90c58c9c16
parent 2c52314e76c8d826bbd0c89a415d88ef962a29f7
Author: Dan Callaghan <djc@djc.id.au>
Date:   Sun, 23 Nov 2008 10:52:21 +1000

getting closer to what I am thinking of

--HG--
rename : blog.py => itemtypes.py
rename : templates/_entry.xml => templates/BlogEntry.xml
rename : templates/_entry.xml => templates/ReadingLogEntry.xml

Diffstat:
Mapp.py | 31+++++++++++++++----------------
Dblog.py | 184-------------------------------------------------------------------------------
Mconfig.defaults | 8++------
Aitemtypes.py | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atemplates/BlogEntry.xml | 38++++++++++++++++++++++++++++++++++++++
Atemplates/ReadingLogEntry.xml | 35+++++++++++++++++++++++++++++++++++
Dtemplates/_entry.xml | 113-------------------------------------------------------------------------------
Mtemplates/_fragments.xml | 3---
8 files changed, 319 insertions(+), 322 deletions(-)
diff --git a/app.py b/app.py
@@ -5,17 +5,17 @@ import os, sys
 sys.path.insert(0, os.path.dirname(__file__))
 sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'lib'))
 
-import cgi, re, datetime
+import cgi, re, datetime, urllib
 from itertools import chain
-from genshi.template import TemplateLoader
+import genshi.template
 from webob import Request, Response
 from webob import exc
 from recaptcha.client import captcha
 
 import config
-import blog
+from itemtypes import *
 
-template_loader = TemplateLoader(
+template_loader = genshi.template.TemplateLoader(
         os.path.join(os.path.dirname(__file__), 'templates'), 
         variable_lookup='strict', 
         auto_reload=True)
@@ -31,13 +31,8 @@ class Constance(object):
 
         self.req = Request(environ)
         self.req.charset = self.encoding
-        
-        self.blog_entries = blog.BlogEntrySet(self.config.get('blog', 'dir'))
-        readinglog_filename = self.config.get('readinglog', 'filename')
-        if readinglog_filename:
-            self.readinglog_entries = blog.ReadingLogEntrySet(readinglog_filename)
-        else:
-            self.readinglog_entries = frozenset()
+
+        self.item_sets = eval(self.config.get('global', 'item_sets'))
 
     def __iter__(self):
         try:
@@ -57,11 +52,15 @@ class Constance(object):
             (r'/blog/([^/]+)/comments/\+new$', 'add_post_comment')]
     urls = [(re.compile(patt), method) for patt, method in urls]
     def dispatch(self, path_info):
-        for patt, method_name in self.urls:
-            match = patt.match(path_info)
-            if match:
-                return getattr(self, method_name)(
-                        *[x.decode(self.encoding, 'ignore') for x in match.groups()])
+        path_info = urllib.unquote(path_info).decode(self.encoding)
+        for item_set in self.item_sets:
+            try:
+                item = item_set.get(path_info)
+            except NotExistError, e:
+                pass
+            else:
+                rendered = item.render('text/html').render('xhtml')
+                return Response(rendered, content_type='text/html')
         # no matching URI found, so give a 404
         raise exc.HTTPNotFound().exception
 
diff --git a/blog.py b/blog.py
@@ -1,184 +0,0 @@
-import os, re, uuid, email
-from datetime import datetime
-import genshi
-import yaml
-
-
-def count(iterable):
-    count = 0
-    for _ in iterable:
-        count += 1
-    return count
-
-def cleanup_metadata(header_items):
-    cleaned = {}
-    for k, v in header_items:
-        k = k.lower()
-        if k.endswith('date'):
-            v = datetime.strptime(v, '%Y-%m-%d %H:%M:%S')
-        else:
-            v = v.decode('utf8') # XXX encoding
-        cleaned[k] = v
-    return cleaned
-
-IDIFY_WHITESPACE_PATT = re.compile(r'(?u)\s+')
-IDIFY_ACCEPT_PATT = re.compile(r'(?u)\w|[-_]')
-def idify(s):
-    # http://www.w3.org/TR/REC-xml/#NT-Name
-    s = s.lower()
-    s = IDIFY_WHITESPACE_PATT.sub(u'-', s)
-    return u''.join(c for c in s if IDIFY_ACCEPT_PATT.match(c))
-
-
-class EntryNotFoundError(ValueError): pass
-
-class EntryForbiddenError(ValueError): pass
-
-class CommentingForbiddenError(ValueError): pass # XXX why all the different types?
-
-class CommentNotFoundError(ValueError): pass
-
-class CommentForbiddenError(ValueError): pass
-
-
-class DirectoryEntrySet(object):
-
-    def __init__(self, base_dir):
-        self.base_dir = base_dir
-        assert os.path.isdir(self.base_dir), self.base_dir
-
-    def __contains__(self, key):
-        return os.path.exists(os.path.join(self.base_dir, key))
-
-    def __getitem__(self, key):
-        key = key.encode('utf8') # XXX don't hardcode
-        if key not in self: raise KeyError(key)
-        return self.entry_class(self.base_dir, key)
-
-    def __len__(self):
-        return count(filename 
-                for filename in os.listdir(self.base_dir) 
-                if not filename.startswith('.'))
-
-    def __iter__(self):
-        assert isinstance(self.base_dir, str)
-        return (self.entry_class(self.base_dir, filename)
-                for filename in os.listdir(self.base_dir)
-                if not filename.startswith('.'))
-
-
-class YamlEntrySet(object):
-
-    def __init__(self, filename):
-        self.filename = filename
-        assert os.path.isfile(self.filename), self.filename
-
-    def __iter__(self):
-        return (self.entry_class(d)
-                for d in yaml.load_all(open(self.filename, 'r')))
-
-
-class BlogEntry(object):
-
-    def __init__(self, entries_dir, id):
-        assert isinstance(id, str), id
-        self.id = id.decode('utf8') # XXX shouldn't hardcode the encoding
-        self.dir = os.path.join(entries_dir, id)
-        self.comments_dir = os.path.join(self.dir, 'comments')
-
-        if not os.path.exists(self.dir):
-            raise EntryNotFoundError()
-        if not os.access(self.dir, os.R_OK):
-            raise EntryForbiddenError()
-
-        # not really a MIME document, but parse it like one
-        msg = email.message_from_file(open(os.path.join(self.dir, 'content.txt'), 'r'))
-        self.metadata = cleanup_metadata(msg.items())
-        self.body = msg.get_payload().decode('utf8') # XXX encoding
-        self.title = self.metadata['title']
-
-        raw_tags = self.metadata.get('tags', '').strip()
-        if raw_tags:
-            self.tags = frozenset(tag.strip() for tag in raw_tags.split(','))
-        else:
-            self.tags = frozenset()
-
-        self.modified_date = datetime.fromtimestamp(os.path.getmtime(os.path.join(self.dir, 'content.txt')))
-        self.publication_date = self.metadata.get('publication-date', None) or self.modified_date
-        self.guid = self.metadata['guid']
-
-    def comments(self):
-        return CommentSet(self.comments_dir)
-
-    def has_comments(self):
-        """
-        Returns True if this Entry could *possibly* have comments, although it 
-        may still have no comments (yet).
-        """
-        return os.path.isdir(self.comments_dir) and \
-                os.access(self.comments_dir, os.R_OK)
-
-    def add_comment(self, metadata, content):
-        if not os.access(self.comments_dir, os.W_OK):
-            raise CommentingForbiddenError()
-        # XXX write to temp file
-        guid = uuid.uuid4().get_hex()
-        f = open(os.path.join(self.comments_dir, guid), 'w')
-        for k, v in metadata.iteritems():
-            f.write('%s: %s\n' % (k, v.encode('utf8'))) # XXX encoding
-        f.write('\n')
-        f.write(content.encode('utf8')) # XXX encoding
-
-
-class BlogEntrySet(DirectoryEntrySet):
-
-    entry_class = BlogEntry
-
-
-class ReadingLogEntry(object):
-
-    def __init__(self, yaml_dict):
-        self.title = yaml_dict['Title']
-        self.id = idify(self.title)
-        self.author = yaml_dict['Author']
-        self.publication_date = self.modified_date = self.date = yaml_dict['Date']
-        self.url = yaml_dict.get('URL', None)
-        self.isbn = yaml_dict.get('ISBN', None)
-        self.rating = yaml_dict.get('Rating', None)
-        self.tags = frozenset()
-        self.guid = yaml_dict['GUID']
-
-    def has_comments(self):
-        return False
-
-
-class ReadingLogEntrySet(YamlEntrySet):
-
-    entry_class = ReadingLogEntry
-
-
-class Comment(object):
-
-    def __init__(self, comments_dir, id):
-        path = os.path.join(comments_dir, id)
-        if not os.path.exists(path):
-            raise CommentNotFoundError()
-        if not os.access(path, os.R_OK):
-            raise CommentForbiddenError()
-
-        self.id = id
-        msg = email.message_from_file(open(path, 'r'))
-        self.metadata = cleanup_metadata(msg.items())
-        self.body = msg.get_payload().decode('utf8') # XXX encoding
-        
-        self.author = self.metadata.get('from', None)
-        self.author_url = self.metadata.get('author-url', None)
-        self.date = datetime.fromtimestamp(os.path.getmtime(path))
-
-    def author_name(self):
-        return self.author or u'Anonymous'
-
-
-class CommentSet(DirectoryEntrySet):
-
-    entry_class = Comment
diff --git a/config.defaults b/config.defaults
@@ -30,6 +30,8 @@ entries_in_feed = 20
 # utf8!
 encoding = utf8
 
+item_sets = [BlogEntrySet('./entries'), ReadingLogEntrySet('./reading_log.yaml')]
+
 [links]
 
 # The name of the YAML file containing links to be placed at the bottom of 
@@ -38,9 +40,6 @@ filename =
 
 [blog]
 
-# The directory containing blog entries.
-dir = ./entries
-
 # Require reCAPTCHA verification for comment submission?
 require_captcha = False
 
@@ -52,9 +51,6 @@ recaptcha_privkey =
 
 [readinglog]
 
-# The name of the file containing a YAML stream of readinglog entries.
-filename = 
-
 # Should LibraryThing covers be shown for readinglog entries?
 # See also librarything_devkey below.
 show_covers = False
diff --git a/itemtypes.py b/itemtypes.py
@@ -0,0 +1,229 @@
+
+"""
+This module defines the various item types which are supported by Constance.
+
+An "item" is anything which can be referenced individually by a URL and 
+rendered, and/or appears in a stream of items. As a minimum each type of item 
+must have the following data attached to it:
+
+    * datetime of original publication (as the `publication_date` attribute)
+    * datetime at which the content of the item was last modified (as the 
+      `modified_date` attribute)
+    * unique URI path within the collection (as the `uri_path` attribute)
+
+Each item type defines a collection type whose name ends in Set (e.g. 
+BlogEntrySet). The collection can be iterated across using iter(), which yields 
+a sequence of item instances (if a collection does not support iteration its 
+iterator yields nothing). Individual items can also be fetched from the 
+collection by calling its `get` attribute with a URI path; either an item 
+instance is returned, or KeyError is raised if the URI path is not known.
+"""
+
+import os, re, uuid, email
+from datetime import datetime
+import genshi.template
+import yaml
+
+template_loader = genshi.template.TemplateLoader(
+        os.path.join(os.path.dirname(__file__), 'templates'), 
+        variable_lookup='strict', 
+        auto_reload=True)
+
+__all__ = ['NoAccessError', 'NotExistError', 'UnsupportedFormatError', 
+           'BlogEntrySet', 'ReadingLogEntrySet']
+
+def count(iterable):
+    count = 0
+    for _ in iterable:
+        count += 1
+    return count
+
+def cleanup_metadata(header_items):
+    cleaned = {}
+    for k, v in header_items:
+        k = k.lower()
+        if k.endswith('date'):
+            v = datetime.strptime(v, '%Y-%m-%d %H:%M:%S')
+        else:
+            v = v.decode('utf8') # XXX encoding
+        cleaned[k] = v
+    return cleaned
+
+IDIFY_WHITESPACE_PATT = re.compile(r'(?u)\s+')
+IDIFY_ACCEPT_PATT = re.compile(r'(?u)\w|[-_]')
+def idify(s):
+    # http://www.w3.org/TR/REC-xml/#NT-Name
+    s = s.lower()
+    s = IDIFY_WHITESPACE_PATT.sub(u'-', s)
+    return u''.join(c for c in s if IDIFY_ACCEPT_PATT.match(c))
+
+
+class NoAccessError(StandardError): pass
+
+class NotExistError(StandardError): pass
+
+class UnsupportedFormatError(StandardError): pass
+
+
+class BlogEntry(object):
+
+    def __init__(self, entries_dir, id, uri_path):
+        assert isinstance(id, str), id
+        self.id = id.decode('utf8') # XXX shouldn't hardcode the encoding
+        self.uri_path = uri_path
+        self.dir = os.path.join(entries_dir, id)
+        self.comments_dir = os.path.join(self.dir, 'comments')
+
+        if not os.path.exists(self.dir):
+            raise EntryNotFoundError()
+        if not os.access(self.dir, os.R_OK):
+            raise EntryForbiddenError()
+
+        # not really a MIME document, but parse it like one
+        msg = email.message_from_file(open(os.path.join(self.dir, 'content.txt'), 'r'))
+        self.metadata = cleanup_metadata(msg.items())
+        self.body = msg.get_payload().decode('utf8') # XXX encoding
+        self.title = self.metadata['title']
+
+        raw_tags = self.metadata.get('tags', '').strip()
+        if raw_tags:
+            self.tags = frozenset(tag.strip() for tag in raw_tags.split(','))
+        else:
+            self.tags = frozenset()
+
+        self.modified_date = datetime.fromtimestamp(os.path.getmtime(os.path.join(self.dir, 'content.txt')))
+        self.publication_date = self.metadata.get('publication-date', None) or self.modified_date
+        self.guid = self.metadata['guid']
+
+    def render(self, format):
+        if format == 'text/html':
+            template = template_loader.load(self.__class__.__name__ + '.xml')
+            return template.generate(item=self)
+        else:
+            raise UnsupportedFormatError(format)
+
+    def comments(self):
+        return CommentSet(self.comments_dir)
+
+    def has_comments(self):
+        """
+        Returns True if this Entry could *possibly* have comments, although it 
+        may still have no comments (yet).
+        """
+        return os.path.isdir(self.comments_dir) and \
+                os.access(self.comments_dir, os.R_OK)
+
+    def add_comment(self, metadata, content):
+        if not os.access(self.comments_dir, os.W_OK):
+            raise CommentingForbiddenError()
+        # XXX write to temp file
+        guid = uuid.uuid4().get_hex()
+        f = open(os.path.join(self.comments_dir, guid), 'w')
+        for k, v in metadata.iteritems():
+            f.write('%s: %s\n' % (k, v.encode('utf8'))) # XXX encoding
+        f.write('\n')
+        f.write(content.encode('utf8')) # XXX encoding
+
+
+class BlogEntrySet(object):
+
+    def __init__(self, base_dir, prefix='/blog'):
+        self.base_dir = base_dir
+        assert os.path.isdir(self.base_dir), self.base_dir
+        self.prefix = prefix
+        self.entry_patt = re.compile(re.escape(prefix) + r'/([^/]+)/?$')
+
+    def get(self, path_info):
+        m = self.entry_patt.match(path_info)
+        if m is None:
+            raise NotExistError(path_info)
+        id = m.group(1).encode('utf8') # XXX don't hardcode
+        if not os.path.isdir(os.path.join(self.base_dir, id)):
+            raise NotExistError(path_info)
+        return BlogEntry(self.base_dir, id, path_info)
+
+    def __iter__(self):
+        assert isinstance(self.base_dir, str)
+        return (BlogEntry(self.base_dir, filename, self.prefix + '/' + filename.encode('utf8'))
+                for filename in os.listdir(self.base_dir)
+                if not filename.startswith('.'))
+
+
+class ReadingLogEntry(object):
+
+    def __init__(self, yaml_dict, uri_path):
+        self.uri_path = uri_path
+        self.title = yaml_dict['Title']
+        self.id = idify(self.title)
+        self.author = yaml_dict['Author']
+        self.publication_date = self.modified_date = self.date = yaml_dict['Date']
+        self.url = yaml_dict.get('URL', None)
+        self.isbn = yaml_dict.get('ISBN', None)
+        self.rating = yaml_dict.get('Rating', None)
+        self.tags = frozenset()
+        self.guid = yaml_dict['GUID']
+
+    def render(self, format):
+        if format == 'text/html':
+            template = template_loader.load(self.__class__.__name__ + '.xml')
+            return template.generate(item=self)
+        else:
+            raise UnsupportedFormatError(format)
+
+    def has_comments(self):
+        return False
+
+
+class ReadingLogEntrySet(object):
+
+    def __init__(self, filename, prefix='/reading'):
+        self.filename = filename
+        assert os.path.isfile(self.filename), self.filename
+        self.prefix = prefix
+
+    def get(self, path_info):
+        raise NotExistError(path_info)
+
+    def __iter__(self):
+        return (ReadingLogEntry(d, self.prefix + '/#ReadingLogEntry-' + idify(d['Title']))
+                for d in yaml.load_all(open(self.filename, 'r')))
+
+
+class Comment(object):
+
+    def __init__(self, comments_dir, id):
+        path = os.path.join(comments_dir, id)
+        if not os.path.exists(path):
+            raise CommentNotFoundError()
+        if not os.access(path, os.R_OK):
+            raise CommentForbiddenError()
+
+        self.id = id
+        msg = email.message_from_file(open(path, 'r'))
+        self.metadata = cleanup_metadata(msg.items())
+        self.body = msg.get_payload().decode('utf8') # XXX encoding
+        
+        self.author = self.metadata.get('from', None)
+        self.author_url = self.metadata.get('author-url', None)
+        self.date = datetime.fromtimestamp(os.path.getmtime(path))
+
+    def author_name(self):
+        return self.author or u'Anonymous'
+
+
+class CommentSet(object):
+
+    def __init__(self, base_dir):
+        self.base_dir = base_dir
+        assert os.path.isdir(self.base_dir), self.base_dir
+
+    def __len__(self):
+        return count(filename 
+                for filename in os.listdir(self.base_dir) 
+                if not filename.startswith('.'))
+
+    def __iter__(self):
+        assert isinstance(self.base_dir, str)
+        return (Comment(self.base_dir, filename)
+                for filename in os.listdir(self.base_dir)
+                if not filename.startswith('.'))
diff --git a/templates/BlogEntry.xml b/templates/BlogEntry.xml
@@ -0,0 +1,38 @@
+<div xmlns="http://www.w3.org/1999/xhtml"
+     xmlns:py="http://genshi.edgewall.org/"
+     xmlns:xi="http://www.w3.org/2001/XInclude"
+     class="item BlogEntry">
+
+<xi:include href="_fragments.xml" />
+
+<?python
+from viewutils import markdown, mini_markdown, tag_list
+from recaptcha.client import captcha
+?>
+
+<h3 id="BlogEntry-${item.id}">${mini_markdown(item.title)}</h3>
+
+<div class="date">
+    ${item.publication_date.strftime(str('%-1d %b %Y'))}
+    <!-- XXX script_name --><a href="${item.uri_path}" rel="bookmark" class="permalink" title="permalink">#</a>
+</div>
+
+<div py:if="item.tags" class="tags">
+    tagged: ${tag_list('', item.tags)} <!-- XXX script_name -->
+</div>
+
+<div class="body">
+    ${markdown(item.body)}
+</div>
+
+<!-- XXX comments -->
+
+<div class="commentslink" py:if="item.has_comments()">
+    <!-- XXX script_name --><a href="${item.uri_path}#comments" py:choose="len(item.comments())">
+        <py:when test="0">no comments »</py:when>
+        <py:when test="1">1 comment »</py:when>
+        <py:otherwise>${len(item.comments())} comments »</py:otherwise>
+    </a>
+</div>
+
+</div>
diff --git a/templates/ReadingLogEntry.xml b/templates/ReadingLogEntry.xml
@@ -0,0 +1,35 @@
+<div xmlns="http://www.w3.org/1999/xhtml"
+     xmlns:py="http://genshi.edgewall.org/"
+     xmlns:xi="http://www.w3.org/2001/XInclude"
+     class="item ReadingLogEntry">
+
+<xi:include href="_fragments.xml" />
+
+<?python
+from viewutils import markdown, mini_markdown, tag_list
+from recaptcha.client import captcha
+?>
+
+<span py:def="stars(rating)" py:strip="True">
+<img src="${uri('static', 'images', 'star.png')}" alt="[star]" py:for="_ in range(int(rating))" /><img src="${uri('static', 'images', 'star-half.png')}" alt="[half-star]" py:if="rating > int(rating)" /><img src="${uri('static', 'images', 'star-off.png')}" alt="" py:for="_ in range(int(5 - rating))" />
+</span>
+
+<!-- XXX img py:if="config.getboolean('readinglog', 'show_covers') and entry.isbn" class="cover"
+     src="http://covers.librarything.com/devkey/${config.get('readinglog', 'librarything_devkey')}/small/isbn/${entry.isbn}" 
+     alt="Cover image for ${entry.title}" /-->
+
+<h3 class="title" id="ReadingLogEntry-${item.id}">
+    <a py:strip="not entry.url" href="${item.url}">${mini_markdown(item.title)}</a>
+    <span py:if="item.author" class="author">by ${item.author}</span>
+</h3>
+
+<div class="date">
+    ${item.publication_date.strftime(str('%-1d %b %Y'))}
+    <a href="${item.uri_path}" rel="bookmark" class="permalink" title="permalink">#</a>
+</div>
+
+<div py:if="item.rating" class="rating">
+    ${stars(item.rating)}
+</div>
+
+</div>
diff --git a/templates/_entry.xml b/templates/_entry.xml
@@ -1,113 +0,0 @@
-<div xmlns="http://www.w3.org/1999/xhtml"
-     xmlns:py="http://genshi.edgewall.org/"
-     xmlns:xi="http://www.w3.org/2001/XInclude"
-	 py:strip="True"
-	 py:def="show_entry(entry, show_comments=True)">
-
-<xi:include href="_fragments.xml" />
-
-<?python
-import blog
-from viewutils import markdown, mini_markdown, tag_list
-from recaptcha.client import captcha
-?>
-
-<div class="entry" py:if="isinstance(entry, blog.BlogEntry)">
-
-	<h3 class="entrytitle" id="entry-${entry.id}">${mini_markdown(entry.title)}</h3>
-
-	<div class="entrydate">
-		${entry.publication_date.strftime(str('%-1d %b %Y'))}
-	    <a href="${uri('blog', entry.id)}" rel="bookmark" class="permalink" title="permalink">#</a>
-	</div>
-
-    <div py:if="entry.tags" class="entrytags">
-        tagged: ${tag_list(environ.get('SCRIPT_NAME', ''), entry.tags)}
-    </div>
-  
-	<div class="entrybody">
-		${markdown(entry.body)}
-    </div>
-
-    <div class="entrycommentslink" py:if="not show_comments and entry.has_comments()">
-        <a href="${uri('blog', entry.id)}#comments" py:choose="len(entry.comments())">
-            <py:when test="0">no comments »</py:when>
-            <py:when test="1">1 comment »</py:when>
-            <py:otherwise>${len(entry.comments())} comments »</py:otherwise>
-        </a>
-    </div>
-
-    <div class="commentblock" py:if="show_comments and entry.has_comments()"
-         py:with="comments = sorted(entry.comments(), key=lambda c: c.date)">
-        <h4 class="comments" py:choose="len(comments)">
-            <py:when test="0">No comments</py:when>
-            <py:when test="1">1 comment</py:when>
-            <py:otherwise>${len(entry.comments())} comments</py:otherwise>
-        </h4>
-        <div py:for="n, comment in enumerate(comments)" id="comment-${comment.id}">
-            ${markdown(comment.body, safe_mode='escape')}
-            <p class="commentmeta">
-                ― <a py:strip="not comment.author_url" href="${comment.author_url}">${comment.author_name()}</a>
-                <span class="commentdatetime">${comment.date.strftime(str('%-1d %b %Y %H:%M'))}</span>
-                <a href="#comment-${comment.id}" class="permalink" title="permalink">#</a>
-            </p>
-            ${block_sep()}
-        </div>
-        <div class="commentform"><form method="post" action="${uri('blog', entry.id, 'comments', '+new')}">
-            <p>
-                <label for="commentform-from">Name</label>
-                <input type="text" id="commentform-from" name="from" />
-            </p>
-            <p>
-                <label for="commentform-author-email">E-mail address (not published)</label>
-                <input type="text" id="commentform-author-email" name="author-email" />
-            </p>
-            <p>
-                <label for="commentform-author-url">URL</label>
-                <input type="text" id="commentform-author-url" name="author-url" />
-            </p>
-            <p>
-                <label for="commentform-comment">Comment (use <a href="http://daringfireball.net/projects/markdown/syntax">Markdown</a>, no HTML)</label>
-                <textarea id="commentform-comment" name="comment" rows="7" cols="30"></textarea>
-            </p>
-            <py:if test="config.getboolean('blog', 'require_captcha')">
-                <script type="text/javascript">
-                    RecaptchaOptions = {theme: 'white'};
-                </script>
-                ${Markup(captcha.displayhtml(config.get('blog', 'recaptcha_pubkey')))}
-            </py:if>
-            <p><button type="submit">Submit</button></p>
-        </form></div>
-    </div>
-
-</div>
-
-<span py:def="stars(rating)" py:strip="True">
-<img src="${uri('static', 'images', 'star.png')}" alt="[star]" py:for="_ in range(int(rating))" /><img src="${uri('static', 'images', 'star-half.png')}" alt="[half-star]" py:if="rating > int(rating)" /><img src="${uri('static', 'images', 'star-off.png')}" alt="" py:for="_ in range(int(5 - rating))" />
-</span>
-
-<div class="entry readinglog" py:if="isinstance(entry, blog.ReadingLogEntry)">
-
-    <img py:if="config.getboolean('readinglog', 'show_covers') and entry.isbn" class="cover"
-         src="http://covers.librarything.com/devkey/${config.get('readinglog', 'librarything_devkey')}/small/isbn/${entry.isbn}" 
-         alt="Cover image for ${entry.title}" />
-
-    <h3 class="entrytitle" id="entry-${entry.id}">
-        <a py:strip="not entry.url" href="${entry.url}">${mini_markdown(entry.title)}</a>
-        <py:if test="entry.author">
-            <span class="author">by ${entry.author}</span>
-        </py:if>
-    </h3>
-
-    <div class="entrydate">
-        ${entry.publication_date.strftime(str('%-1d %b %Y'))}
-	    <a href="${uri('reading', '')}#entry-${entry.id}" rel="bookmark" class="permalink" title="permalink">#</a>
-    </div>
-
-    <div py:if="entry.rating" class="rating">
-        ${stars(entry.rating)}
-    </div>
-
-</div>
-
-</div>
diff --git a/templates/_fragments.xml b/templates/_fragments.xml
@@ -10,7 +10,4 @@ import urllib
 <span py:def="inline_sep()" class="inlinesep">·</span>
 <div py:def="block_sep()" class="blocksep">~</div>
 
-<span py:def="uri(*components)" py:strip="True">${'/'.join([environ.get('SCRIPT_NAME', '')] + [urllib.quote(component.encode(config.get('global', 'encoding')), '+') for component in components])}</span>
-<span py:def="abs_uri(*components)" py:strip="True">${environ['APP_URI']}/${'/'.join(urllib.quote(component.encode(config.get('global', 'encoding')), '+') for component in components)}</span>
-
 </div>