commit 7d1d495ac8a06e12d999fc767ee20928346550d6
parent 1d9ba128652924ca1219507ca403665a5f93f304
Author: Dan Callaghan <djc@djc.id.au>
Date: Sun, 15 Aug 2010 13:17:27 +1000
new incarnation: constance is now a bunch of helper scripts for generating a static site, rather than a WSGI app
--HG--
rename : itemtypes.py => blog.py
rename : app.py => constance.py
rename : itemtypes.py => reading.py
rename : templates/atom/BlogEntry.xml => templates/blog/entry.atom
rename : templates/html/BlogEntry.xml => templates/blog/entry.html
rename : templates/atom/multiple.xml => templates/blog/index.atom
rename : templates/atom/ReadingLogEntry.xml => templates/reading/entry.atom
rename : templates/html/tag_cloud.xml => templates/tags/index.html
Diffstat:
37 files changed, 536 insertions(+), 1084 deletions(-)
diff --git a/app.py b/app.py
@@ -1,220 +0,0 @@
-
-# vim:encoding=utf-8
-
-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, urllib
-from itertools import chain
-import genshi.template
-from webob import Request, Response
-from webob import exc
-from recaptcha.client import captcha
-
-import config
-from itemtypes import *
-
-template_loader = genshi.template.TemplateLoader(
- os.path.join(os.path.dirname(__file__), 'templates'),
- variable_lookup='strict',
- auto_reload=True)
-
-class Constance(object):
-
- def __init__(self, environ, start_response):
- self.environ = environ
- self.start = start_response
-
- self.config = config.ConstanceConfigParser(self.environ['constance.config_filename'])
- self.encoding = self.config.get('global', 'encoding')
-
- self.req = Request(environ)
- self.req.charset = self.encoding
-
- self.item_sets = eval(self.config.get('global', 'item_sets'))
-
- def __iter__(self):
- try:
- resp = self.dispatch()
- except exc.HTTPException, e:
- resp = e
- return iter(resp(self.environ, self.start))
-
- urls = [(r'/(?:index)?$', 'index'),
- (r'/tags/$', 'tag_cloud'),
- (r'/tags/(.+)$', 'tag'),
- (r'/sitemap.xml$', 'sitemap')]
- urls = [(re.compile(patt), method) for patt, method in urls]
- def dispatch(self):
- if self.req.path_info.endswith('.atom'):
- self.req.format = 'application/atom+xml'
- self.req.path_info = self.req.path_info[:-5]
- else:
- self.req.format = self.req.accept.best_match(['text/html', 'application/atom+xml']) # XXX don't hardcode
-
- path_info = urllib.unquote(self.req.path_info).decode(self.encoding)
-
- # first, try the predefined urls above
- for patt, method in self.urls:
- m = patt.match(path_info)
- if m is not None:
- return getattr(self, method)(*m.groups())
-
- # next, try the item sets
- for item_set in self.item_sets:
- try:
- result = item_set.get(path_info)
- except NotExistError, e:
- pass
- else:
- if hasattr(result, '__iter__'):
- return self.render_multiple(result)
- else:
- return self.render_single(result)
-
- # no matching URI found, so give a 404
- raise exc.HTTPNotFound().exception
-
- def render_single(self, item):
- if self.req.format == 'text/html':
- template = template_loader.load('html/single.xml')
- rendered = template.generate(
- config=self.config,
- item=item
- ).render('xhtml')
- else:
- raise exc.HTTPBadRequest('Unacceptable format for render_single %r' % self.req.format).exception
- return Response(rendered, content_type=self.req.format)
-
- def render_multiple(self, items):
- try:
- offset = int(self.req.GET.get('offset', 0))
- except ValueError:
- raise exc.HTTPBadRequest('Invalid offset %r' % self.GET['offset']).exception
- if self.req.format == 'text/html':
- template = template_loader.load('html/multiple.xml')
- rendered = template.generate(
- config=self.config,
- items=items,
- title=None,
- offset=offset
- ).render('xhtml')
- elif self.req.format == 'application/atom+xml':
- template = template_loader.load('atom/multiple.xml')
- rendered = template.generate(
- config=self.config,
- items=items,
- title=None,
- self_url=self.req.path_url
- ).render('xml')
- else:
- raise exc.HTTPBadRequest('Unacceptable format for render_multiple %r' % self.req.format).exception
- return Response(rendered, content_type=self.req.format)
-
- def index(self):
- items = chain(*self.item_sets)
- return self.render_multiple(items)
-
- def tag_cloud(self):
- tag_freqs = {}
- for entry in chain(*self.item_sets):
- for tag in entry.tags:
- tag_freqs[tag] = tag_freqs.get(tag, 0) + 1
- if self.req.format == 'text/html':
- rendered = template_loader.load('html/tag_cloud.xml').generate(
- config=self.config,
- environ=self.environ,
- tag_freqs=tag_freqs
- ).render('xhtml', encoding=self.encoding)
- else:
- raise exc.HTTPBadRequest('Unacceptable format for render_multiple %r' % self.req.format).exception
- return Response(rendered, content_type=self.req.format)
-
- def add_post_comment(self, id):
- entry = self.blog_entries[id]
-
- if self.config.getboolean('blog', 'require_captcha'):
- # first verify the captcha
- if ('recaptcha_challenge_field' not in self.req.POST or
- 'recaptcha_response_field' not in self.req.POST):
- raise exc.HTTPForbidden('CAPTCHA form values missing. Are you a bot?').exception
- captcha_response = captcha.submit(
- self.req.POST['recaptcha_challenge_field'],
- self.req.POST['recaptcha_response_field'],
- self.config.get('blog', 'recaptcha_privkey'),
- self.req.remote_addr)
- if not captcha_response.is_valid:
- raise exc.HTTPForbidden('You failed the CAPTCHA. Please try submitting again. '
- '(reCAPTCHA error code: %s)' % captcha_response.error_code).exception
-
- try:
- metadata = {}
- metadata['From'] = self.req.POST['from'] or u'Anonymous'
- if self.req.POST['author-url']:
- metadata['Author-URL'] = self.req.POST['author-url']
- if self.req.POST['author-email']:
- metadata['Author-Email'] = self.req.POST['author-email']
- if self.req.headers['User-Agent']:
- metadata['User-Agent'] = self.req.headers['User-Agent']
- if self.req.remote_addr:
- metadata['Received'] = u'from %s' % self.req.remote_addr
- entry.add_comment(metadata, self.req.POST['comment'])
- raise exc.HTTPFound('%s/%s/' % (self.req.application_url,
- id.encode(self.encoding))).exception
- except blog.CommentingForbiddenError:
- raise exc.HTTPForbidden('Commenting is disabled for this entry.').exception
-
- def tag(self, tag):
- with_tag = [e for e in chain(*self.item_sets) if tag in e.tags]
- if not with_tag:
- raise exc.HTTPNotFound().exception
- return self.render_multiple(with_tag)
-
- def sitemap(self):
- tags = {}
- for entry in self.blog_entries:
- for tag in entry.tags:
- tags[tag] = max(entry.modified_date, tags.get(tag, datetime.datetime.min))
- sorted_blog_entries = sorted(self.blog_entries,
- key=lambda e: e.publication_date, reverse=True)
- sorted_entries = sorted(chain(self.blog_entries, self.readinglog_entries),
- key=lambda e: e.publication_date, reverse=True)
- readinglog_entries = list(self.readinglog_entries)
- if len(readinglog_entries) != 0:
- rl_updated = max(e.date for e in readinglog_entries)
- else:
- rl_updated = None
- rendered = template_loader.load('sitemap.xml').generate(
- config=self.config,
- environ=self.environ,
- blog_entries=self.blog_entries,
- tags=tags,
- readinglog_updated=rl_updated,
- blog_index_updated=max(e.modified_date for e in sorted_blog_entries[:self.config.getint('global', 'entries_per_page')]),
- index_updated=max(e.modified_date for e in sorted_entries[:self.config.getint('global', 'entries_per_page')]),
- ).render('xml', encoding='utf8') # sitemaps must be UTF-8
- return Response(rendered, content_type='text/xml')
-
-application = Constance
-
-if __name__ == '__main__':
- import optparse
- parser = optparse.OptionParser(usage='%prog [OPTIONS...] CONFIG_FILENAME')
- parser.set_defaults(port=8082)
- parser.add_option('-p', '--port', type='int',
- help='Port to server on (default: %default)')
- options, args = parser.parse_args()
- if not args:
- parser.error('You must supply a CONFIG_FILENAME')
-
- from paste.urlparser import StaticURLParser
- from paste.urlmap import URLMap
- application = URLMap()
- application['/static'] = StaticURLParser(os.path.join(os.path.dirname(__file__), 'static'))
- application['/'] = Constance
-
- import wsgiref.simple_server
- server = wsgiref.simple_server.make_server('0.0.0.0', options.port, application)
- server.base_environ['constance.config_filename'] = args[0]
- server.serve_forever()
diff --git a/blog.py b/blog.py
@@ -0,0 +1,83 @@
+
+# vim:encoding=utf-8
+
+import os
+import re
+import email
+from datetime import datetime
+import genshi.template
+import lxml.etree
+
+import viewutils
+
+template_loader = genshi.template.TemplateLoader(
+ os.path.join(os.path.dirname(__file__), 'templates', 'blog'),
+ variable_lookup='strict')
+
+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
+
+class BlogEntry(object):
+
+ def __init__(self, dir, name):
+ content_filename = os.path.join(dir, name + '.txt')
+ self.id = name.decode('utf8')
+
+ # not really a MIME document, but parse it like one
+ msg = email.message_from_file(open(content_filename, 'r'))
+ self.metadata = cleanup_metadata(msg.items())
+ self.body = viewutils.markdown(msg.get_payload().decode('utf8'))
+ self.title = viewutils.mini_markdown(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(content_filename))
+ self.publication_date = self.metadata.get('publication-date', None) or self.modified_date
+ self.guid = self.metadata['guid']
+
+ def generate_atom(self):
+ return template_loader.load('entry.atom').generate(item=self)
+
+class BlogEntrySet(object):
+
+ def __init__(self, dir):
+ self.dir = dir
+ self.entries = []
+ for filename in os.listdir(dir):
+ m = re.match(r'([^.].*)\.txt$', filename)
+ if m:
+ self.entries.append(BlogEntry(dir, m.group(1)))
+
+ def __iter__(self):
+ return iter(self.entries)
+
+def generate(dir, xslt):
+ entries = BlogEntrySet(dir)
+
+ for entry in entries:
+ rendered = template_loader.load('entry.html').generate(item=entry).render('xhtml')
+ transformed = str(xslt(lxml.etree.fromstring(rendered)))
+ open(os.path.join(dir, entry.id.encode('utf8') + '.html'), 'w').write(transformed)
+
+ # index
+ rendered = template_loader.load('index.html').generate(items=entries).render('xhtml')
+ transformed = str(xslt(lxml.etree.fromstring(rendered)))
+ open(os.path.join(dir, 'index.html'), 'w').write(transformed)
+
+ # feed
+ rendered = template_loader.load('index.atom').generate(items=entries).render('xml')
+ open(os.path.join(dir, 'index.atom'), 'w').write(rendered)
+
+ return entries
diff --git a/config.defaults b/config.defaults
@@ -1,61 +0,0 @@
-[global]
-
-# The name of the blog. Appears at the end of page <title>s.
-name = Untitled
-
-# The name of the author of this blog.
-author = Anonymous
-
-# The author's e-mail address. Leave this blank if you don't want to publish an
-# e-mail address.
-email =
-
-# The maximum number of items (of any kind) to be shown on each page.
-items_per_page = 20
-
-# Whether to include explicit previous/next <A> links for paged browsing (when needed). <LINK> elements are always included.
-show_prev_next = False
-
-# The maximum number of items to be included in feeds.
-items_in_feed = 20
-
-# Character encoding to be used everywhere. That is, for:
-# * all data read from disk (including this config)
-# * URL components and query string arguments
-# * POST data
-# * rendered templates
-# and anywhere else I have forgotten. Really whenever we are converting between
-# Unicode data and bytestrings, this is the encoding that is used.
-# It is *highly* recommended that you not change this value from its default of
-# 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
-# every page. Leaving this blank disables the link section.
-filename =
-
-[blog]
-
-# Require reCAPTCHA verification for comment submission?
-require_captcha = False
-
-# If require_captcha is True, you must supply a valid public/private reCAPTCHA
-# key pair here. Otherwise they can be left blank.
-# See https://admin.recaptcha.net/recaptcha/createsite/
-recaptcha_pubkey =
-recaptcha_privkey =
-
-[readinglog]
-
-# Should LibraryThing covers be shown for readinglog entries?
-# See also librarything_devkey below.
-show_covers = False
-
-# If show_covers is True, you must supply a valid LibraryThing developer key to
-# be included in cover URLs. You can leave this blank otherwise.
-# See http://www.librarything.com/services/keys.php
-librarything_devkey =
diff --git a/config.py b/config.py
@@ -1,15 +0,0 @@
-
-# vim:encoding=utf-8
-
-import os
-from ConfigParser import SafeConfigParser
-
-class ConstanceConfigParser(SafeConfigParser):
-
- def __init__(self, filename):
- SafeConfigParser.__init__(self)
- self.readfp(open(os.path.join(os.path.dirname(__file__), 'config.defaults'), 'r'))
- self.readfp(open(filename, 'r'))
-
- def getunicode(self, section, option):
- return self.get(section, option).decode(self.get('global', 'encoding'))
diff --git a/constance.py b/constance.py
@@ -0,0 +1,42 @@
+
+# vim:encoding=utf-8
+
+import os, sys
+sys.path.insert(0, os.path.dirname(__file__))
+sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'lib'))
+
+import re
+import datetime
+import time
+import urllib
+import optparse
+import genshi.template
+import lxml.etree
+
+import blog
+import reading
+import tags
+import homepage
+
+def main():
+ parser = optparse.OptionParser()
+ parser.add_option('--blog-dir', metavar='DIR')
+ parser.add_option('--reading-log', metavar='FILENAME')
+ parser.add_option('--tags-dir', metavar='DIR')
+ parser.add_option('--root-dir', metavar='DIR')
+ parser.add_option('--xslt', metavar='FILENAME')
+ parser.set_defaults(blog_dir='./blog/',
+ reading_log='./reading_log.yaml',
+ tags_dir='./tags/',
+ root_dir='./',
+ xslt='./style.xsl')
+ options, args = parser.parse_args()
+
+ xslt = lxml.etree.XSLT(lxml.etree.parse(options.xslt))
+ blog_entries = blog.generate(options.blog_dir, xslt)
+ reading_entries = reading.generate(options.reading_log, xslt)
+ tags.generate(options.tags_dir, xslt, blog_entries)
+ homepage.generate(options.root_dir, xslt, blog_entries, reading_entries)
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/deploy.txt b/doc/deploy.txt
@@ -1,15 +0,0 @@
-Deployment Instructions
-=======================
-
-Make a local copy of config.defaults (for example, config.local) and edit it
-as appropriate.
-
-mod_wsgi
---------
-
- SetEnv constance.config_filename /home/dan/constance/config.local
- <Directory /home/dan/constance>
- Allow from all
- </Directory>
- WSGIScriptAlias /blog /home/dan/constance/app.py
- Alias /blog/static /home/dan/constance/static/
diff --git a/homepage.py b/homepage.py
@@ -0,0 +1,24 @@
+
+# vim:encoding=utf-8
+
+import os
+from itertools import chain
+import genshi.template
+import lxml.etree
+
+import viewutils
+
+template_loader = genshi.template.TemplateLoader(
+ os.path.join(os.path.dirname(__file__), 'templates', 'homepage'),
+ variable_lookup='strict')
+
+def generate(dir, xslt, blog_entries, reading_entries):
+ # index
+ template = template_loader.load('index.html')
+ rendered = template.generate(blog_entries=blog_entries).render('xhtml')
+ transformed = str(xslt(lxml.etree.fromstring(rendered)))
+ open(os.path.join(dir, 'index.html'), 'w').write(transformed)
+
+ # firehose
+ rendered = template_loader.load('firehose.atom').generate(items=chain(blog_entries, reading_entries)).render('xml')
+ open(os.path.join(dir, 'firehose.atom'), 'w').write(rendered)
diff --git a/itemtypes.py b/itemtypes.py
@@ -1,231 +0,0 @@
-
-"""
-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
-
-
-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('html/' + self.__class__.__name__ + '.xml')
- return template.generate(item=self)
- elif format == 'application/atom+xml':
- template = template_loader.load('atom/' + 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.index_patt = re.compile(re.escape(prefix) + r'/?(index)?$')
- self.entry_patt = re.compile(re.escape(prefix) + r'/([^/]+)/?$')
-
- def get(self, path_info):
- if self.index_patt.match(path_info):
- return iter(self)
- 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.decode('utf8'))
- for filename in os.listdir(self.base_dir)
- if not filename.startswith('.'))
-
-
-class ReadingLogEntry(object):
-
- def __init__(self, yaml_dict):
- self.title = yaml_dict['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('html/' + self.__class__.__name__ + '.xml')
- return template.generate(item=self)
- elif format == 'application/atom+xml':
- template = template_loader.load('atom/' + 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
- self.index_patt = re.compile(re.escape(prefix) + r'/?(index)?$')
-
- def get(self, path_info):
- if self.index_patt.match(path_info):
- return iter(self)
- raise NotExistError(path_info)
-
- def __iter__(self):
- return (ReadingLogEntry(d)
- 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/reading.py b/reading.py
@@ -0,0 +1,52 @@
+
+# vim:encoding=utf-8
+
+import os
+import genshi.template
+import yaml
+import lxml.etree
+
+import viewutils
+
+template_loader = genshi.template.TemplateLoader(
+ os.path.join(os.path.dirname(__file__), 'templates', 'reading'),
+ variable_lookup='strict')
+
+class ReadingLogEntry(object):
+
+ def __init__(self, yaml_dict):
+ self.title = viewutils.mini_markdown(yaml_dict['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 generate_atom(self):
+ return template_loader.load('entry.atom').generate(item=self)
+
+class ReadingLogEntrySet(object):
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.entries = []
+ for d in yaml.load_all(open(self.filename, 'r')):
+ self.entries.append(ReadingLogEntry(d))
+
+ def __iter__(self):
+ return iter(self.entries)
+
+def generate(filename, xslt):
+ entries = ReadingLogEntrySet(filename)
+
+ rendered = template_loader.load('reading.html').generate(items=entries).render('xhtml')
+ transformed = str(xslt(lxml.etree.fromstring(rendered)))
+ open(os.path.join(os.path.dirname(filename), 'reading.html'), 'w').write(transformed)
+
+ # feed
+ rendered = template_loader.load('reading.atom').generate(items=entries).render('xml')
+ open(os.path.join(os.path.dirname(filename), 'reading.atom'), 'w').write(rendered)
+
+ return entries
diff --git a/static/css/common.css b/static/css/common.css
@@ -1,185 +0,0 @@
-
-body {
- font-family: "Cambria", "Liberation Serif", serif;
- line-height: 2.75ex;
- margin: 0;
- padding: 0;
- background-color: #ddd;
-}
-#contentwrapper {
- border-bottom: 1px dashed #aaa;
- background-color: white;
- margin: 0;
- padding: 0.01em 1em 1em 2em;
-}
-#content {
- margin: 1em auto;
- max-width: 45em;
-}
-
-a {
- color: #0066b3;
-}
-a:visited {
- color: #990099;
-}
-h1, h2, h3, h4, h5, h6 {
- font-family: "Corbel", "Liberation Sans", sans-serif;
-}
-pre, code {
- font-family: "Consolas", monospace;
-}
-img {
- border: none;
-}
-blockquote {
- border-left: 3px solid #ddd;
- padding-left: 1em;
- margin-left: 2em;
-}
-pre {
- margin-left: 2em;
-}
-hr {
- border: none;
- border-top: 1px dashed #aaa;
- height: 0;
-}
-
-span.inlinesep {
- font-weight: bold;
- padding: 0 0.25em;
-}
-div.blocksep {
- font-size: 1.6em;
- text-align: center;
-}
-
-/* Styles which are common to all item types */
-.item {
- margin-top: 2em;
- padding-top: 2em;
- clear: both;
- border-top: 1px solid #ddd;
-}
-.item h3 {
- display: inline;
- margin-right: 0.5em;
-}
-.item .date {
- font-family: "Corbel", "Liberation Sans", sans-serif;
- display: inline;
- font-size: 1.2em;
- letter-spacing: 0.1ex;
- word-spacing: 0.1em;
- color: #888;
- white-space: nowrap;
- margin-right: 0.25em;
-}
-.item .date a {
- color: #888;
-}
-.commentslink {
- text-align: right;
- font-style: italic;
-}
-.commentslink a {
- color: #888;
- white-space: nowrap;
-}
-
-/* Blog entries */
-.BlogEntry h3 {
- font-size: 1.5em;
- text-transform: uppercase;
- letter-spacing: 0.1ex;
- word-spacing: 0.2em;
-}
-.BlogEntry .tags {
- display: inline;
- font-style: italic;
- color: #888;
-}
-.BlogEntry .tags a {
- color: black;
- white-space: nowrap;
-}
-
-/* Reading log entries */
-.ReadingLogEntry h3 {
- font-size: 1.2em;
- word-spacing: 0.1em;
-}
-.ReadingLogEntry h3 a {
- font-style: italic;
- margin-right: 0.2em;
-}
-.ReadingLogEntry .author {
- white-space: nowrap;
-}
-.ReadingLogEntry img.cover {
- float: right;
- margin: 0 0 1em 1em;
-}
-.ReadingLogEntry .rating {
- display: inline;
- white-space: nowrap;
-}
-
-/* Comments */
-.commentblock {
- margin: 2em 4em 0 4em;
- font-size: 0.9em;
-}
-.commentblock h4 {
- font-size: 1.2em;
-}
-.commentblock .commentmeta {
- text-align: right;
-}
-.commentblock .commentmeta .commentdatetime {
- color: #888;
- margin: 0 0.5em;
-}
-.commentblock .commentmeta .permalink {
- color: #888;
-}
-
-/* Comment form */
-.commentform label {
- display: block;
-}
-.commentform input {
- display: block;
- width: 30em;
-}
-.commentform textarea {
- width: 100%;
- font-size: 1em;
-}
-
-/* Previous/next links, if enabled */
-#prevnextlinks {
- padding: 2em 0;
- clear: both;
- border-top: 1px solid #ddd;
- text-align: center;
-}
-
-/* Links at the bottom */
-#links {
- padding: 0.5em 2em 2em 2em;
- margin-top: 1em;
-}
-.linkrow {
- text-align: center;
- margin: 1em 0;
-}
-.linkrow h2 {
- display: inline;
- font-size: 1.1em;
- margin-right: 1em;
-}
-.linkrow p {
- display: inline;
-}
diff --git a/static/css/tag_cloud.css b/static/css/tag_cloud.css
@@ -1,19 +0,0 @@
-ol#tagcloud {
- margin: 0;
- padding: 0;
- line-height: 4ex;
-}
-ol#tagcloud li {
- display: inline;
- list-style: none;
- margin-right: 0.75em;
-}
-ol#tagcloud a {
- white-space: nowrap;
-}
-ol#tagcloud .frequency {
- /* http://css-discuss.incutio.com/?page=OffLeft */
- position: absolute;
- left: -1000px;
- width: 100px;
-}
diff --git a/static/images/feed-icon-16x16.png b/static/images/feed-icon-16x16.png
Binary files differ.
diff --git a/static/images/star-half.png b/static/images/star-half.png
Binary files differ.
diff --git a/static/images/star-off.png b/static/images/star-off.png
Binary files differ.
diff --git a/static/images/star.png b/static/images/star.png
Binary files differ.
diff --git a/tags.py b/tags.py
@@ -0,0 +1,28 @@
+
+# vim:encoding=utf-8
+
+import os
+import genshi.template
+import lxml.etree
+
+import viewutils
+
+template_loader = genshi.template.TemplateLoader(
+ os.path.join(os.path.dirname(__file__), 'templates', 'tags'),
+ variable_lookup='strict')
+
+def generate(dir, xslt, blog_entries):
+ tag_freqs = {}
+ for entry in blog_entries:
+ for tag in entry.tags:
+ tag_freqs[tag] = tag_freqs.get(tag, 0) + 1
+
+ for tag in tag_freqs.keys():
+ tagged_entries = [e for e in blog_entries if tag in e.tags]
+ rendered = template_loader.load('tag.html').generate(tag=tag, items=tagged_entries).render('xhtml')
+ transformed = str(xslt(lxml.etree.fromstring(rendered)))
+ open(os.path.join(dir, tag.encode('utf8') + '.html'), 'w').write(transformed)
+
+ rendered = template_loader.load('index.html').generate(tag_freqs=tag_freqs).render('xhtml')
+ transformed = str(xslt(lxml.etree.fromstring(rendered)))
+ open(os.path.join(dir, 'index.html'), 'w').write(transformed)
diff --git a/templates/atom/BlogEntry.xml b/templates/atom/BlogEntry.xml
@@ -1,27 +0,0 @@
-<entry xmlns="http://www.w3.org/2005/Atom"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
-
-<?python
-import blog
-from viewutils import markdown, tag_list, ATOM_TIME_FORMAT
-?>
-
-<id>${item.guid}</id>
-<published>${item.publication_date.strftime(ATOM_TIME_FORMAT)}</published>
-<updated>${item.modified_date.strftime(ATOM_TIME_FORMAT)}</updated>
-<!--author py:with="email = config.getunicode('global', 'email')">
- <name>${config.getunicode('global', 'author')}</name>
- <email py:if="email">${email}</email>
-</author-->
-<category py:for="tag in item.tags" scheme="/tags/" term="${tag}" /><!-- XXX app_uri -->
-<link rel="alternate" href="${item.uri_path}" /><!-- XXX app_uri -->
-<title type="text">${item.title}</title>
-<content type="xhtml" xml:base="${item.uri_path}"><!-- XXX app_uri -->
- <div xmlns="http://www.w3.org/1999/xhtml">
- <p py:if="item.tags">Tagged: ${tag_list('', item.tags)}</p><!-- XXX script_name -->
- ${markdown(item.body)}
- </div>
-</content>
-
-</entry>
diff --git a/templates/atom/ReadingLogEntry.xml b/templates/atom/ReadingLogEntry.xml
@@ -1,26 +0,0 @@
-<entry xmlns="http://www.w3.org/2005/Atom"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
-
-<?python
-from viewutils import ATOM_TIME_FORMAT
-?>
-
-<id>${item.guid}</id>
-<published>${item.publication_date.strftime(ATOM_TIME_FORMAT)}</published>
-<updated>${item.modified_date.strftime(ATOM_TIME_FORMAT)}</updated>
-<!--author py:with="email = config.getunicode('global', 'email')">
- <name>${config.getunicode('global', 'author')}</name>
- <email py:if="email">${email}</email>
-</author-->
-<category py:for="tag in item.tags" scheme="/tags/" term="${tag}" /><!-- XXX app_uri -->
-<title type="text">${item.title} by ${item.author}</title>
-<summary py:if="item.rating" type="text">${item.rating} stars</summary>
-<content type="xhtml">
- <div xmlns="http://www.w3.org/1999/xhtml">
- <p><a href="${item.url}">${item.title}</a> by ${item.author}</p>
- <p py:if="item.rating">${item.rating} stars</p>
- </div>
-</content>
-
-</entry>
diff --git a/templates/atom/multiple.xml b/templates/atom/multiple.xml
@@ -1,21 +0,0 @@
-<feed xmlns="http://www.w3.org/2005/Atom"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
-
-<?python
-from viewutils import ATOM_TIME_FORMAT
-sorted_items = sorted(items, key=lambda item: item.publication_date, reverse=True)[:config.getint('global', 'items_in_feed')]
-?>
-
-<id>${self_url}</id>
-<title type="text">${config.getunicode('global', 'name')}<py:if test="title"> (${title})</py:if></title>
-<link rel="self" type="application/atom+xml" href="${self_url}" />
-<link rel="alternate" href="${self_url}" />
-<generator>constance</generator>
-<updated py:if="sorted_items">${max(item.modified_date for item in sorted_items).strftime(ATOM_TIME_FORMAT)}</updated>
-
-<py:for each="item in sorted_items">
- ${item.render('application/atom+xml')}
-</py:for>
-
-</feed>
diff --git a/templates/blog/entry.atom b/templates/blog/entry.atom
@@ -0,0 +1,25 @@
+<entry xmlns="http://www.w3.org/2005/Atom"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<?python
+from viewutils import ATOM_TIME_FORMAT
+?>
+
+<id>${item.guid}</id>
+<published>${item.publication_date.strftime(ATOM_TIME_FORMAT)}</published>
+<updated>${item.modified_date.strftime(ATOM_TIME_FORMAT)}</updated>
+<author>
+ <name>Dan C</name>
+ <email>djc@djc.id.au</email>
+</author>
+<category py:for="tag in item.tags" scheme="http://www.djc.id.au/tags/" term="${tag}" />
+<link rel="alternate" href="http://www.djc.id.au/blog/${item.id}" />
+<title type="text">${item.title.striptags()}</title>
+<content type="xhtml" xml:base="http://www.djc.id.au/blog/${item.id}">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ ${item.body}
+ </div>
+</content>
+
+</entry>
diff --git a/templates/blog/entry.html b/templates/blog/entry.html
@@ -0,0 +1,30 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ lang="en-AU">
+
+<?python
+from viewutils import tag_list
+?>
+
+<head>
+ <title>${item.title.striptags()}</title>
+ <meta name="DC.date" content="${item.publication_date.strftime(str('%Y-%m-%d'))}" />
+</head>
+<body>
+ <div class="item blog-entry">
+
+ <h1 class="entry-title"><a href="http://www.djc.id.au/blog/${item.id}" rel="bookmark">${item.title}</a></h1>
+
+ <div class="date published">${item.publication_date.strftime(str('%-1d %b %Y'))}</div>
+
+ <div py:if="item.tags" class="tags">
+ tagged: ${tag_list('', item.tags)} <!-- XXX script_name -->
+ </div>
+
+ <div class="entry-content">
+ ${item.body}
+ </div>
+
+ </div>
+</body>
+</html>
diff --git a/templates/blog/index.atom b/templates/blog/index.atom
@@ -0,0 +1,21 @@
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<?python
+from viewutils import ATOM_TIME_FORMAT
+sorted_items = sorted(items, key=lambda item: item.publication_date, reverse=True)
+?>
+
+<id>http://www.djc.id.au/blog/index.atom</id>
+<title type="text">djc blog</title>
+<link rel="self" type="application/atom+xml" href="http://www.djc.id.au/blog/index.atom" />
+<link rel="alternate" href="http://www.djc.id.au/blog/" />
+<generator>constance</generator>
+<updated py:if="sorted_items">${max(item.modified_date for item in sorted_items).strftime(ATOM_TIME_FORMAT)}</updated>
+
+<py:for each="item in sorted_items">
+ ${item.generate_atom()}
+</py:for>
+
+</feed>
diff --git a/templates/blog/index.html b/templates/blog/index.html
@@ -0,0 +1,28 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ lang="en-AU">
+
+<?python
+from itertools import groupby
+from viewutils import markdown, mini_markdown, tag_list
+?>
+
+<head>
+ <title>Blog archive</title>
+ <link rel="alternate" type="application/atom+xml" title="Atom feed" href="index.atom" />
+</head>
+
+<body>
+
+ <h1>Blog archive</h1>
+
+ <py:for each="year, items in groupby(sorted(items, key=lambda e: e.publication_date, reverse=True), key=lambda e: e.publication_date.year)">
+ <h2>${year}</h2>
+ <div class="item blog-entry-stub" py:for="item in items">
+ <h3 class="entry-title"><a href="${item.id}">${item.title}</a></h3>
+ <div class="date published">${item.publication_date.strftime(str('%-1d %b %Y'))}</div>
+ </div>
+ </py:for>
+
+</body>
+</html>
diff --git a/templates/homepage/firehose.atom b/templates/homepage/firehose.atom
@@ -0,0 +1,21 @@
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<?python
+from viewutils import ATOM_TIME_FORMAT
+sorted_items = sorted(items, key=lambda item: item.publication_date, reverse=True)
+?>
+
+<id>http://www.djc.id.au/firehose.atom</id>
+<title type="text">djc firehose</title>
+<link rel="self" type="application/atom+xml" href="http://www.djc.id.au/firehose.atom" />
+<link rel="alternate" href="http://www.djc.id.au/" />
+<generator>constance</generator>
+<updated py:if="sorted_items">${max(item.modified_date for item in sorted_items).strftime(ATOM_TIME_FORMAT)}</updated>
+
+<py:for each="item in sorted_items">
+ ${item.generate_atom()}
+</py:for>
+
+</feed>
diff --git a/templates/homepage/index.html b/templates/homepage/index.html
@@ -0,0 +1,43 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ lang="en-AU">
+
+<?python
+from itertools import groupby
+from viewutils import markdown, mini_markdown, tag_list
+?>
+
+<head>
+ <link rel="alternate" type="application/atom+xml" title="Atom feed" href="index.atom" />
+</head>
+
+<body py:with="sorted_blog_entries=sorted(blog_entries, key=lambda e: e.publication_date, reverse=True)">
+
+ <div class="item blog-entry" py:with="item = sorted_blog_entries[0]">
+
+ <h1 class="entry-title"><a href="http://www.djc.id.au/blog/${item.id}" rel="bookmark">${item.title}</a></h1>
+
+ <div class="date published">${item.publication_date.strftime(str('%-1d %b %Y'))}</div>
+
+ <div py:if="item.tags" class="tags">
+ tagged: ${tag_list('', item.tags)} <!-- XXX script_name -->
+ </div>
+
+ <div class="entry-content">
+ ${item.body}
+ </div>
+
+ </div>
+
+ <div>
+ <h2>Previous blog entries</h2>
+ <div class="item blog-entry-stub" py:for="item in sorted_blog_entries[1:4]">
+ <h3 class="entry-title"><a href="blog/${item.id}">${item.title}</a></h3>
+ <div class="date published">${item.publication_date.strftime(str('%-1d %b %Y'))}</div>
+ </div>
+ <a href="blog/">more...</a>
+ </div>
+
+</body>
+</html>
+
diff --git a/templates/html/BlogEntry.xml b/templates/html/BlogEntry.xml
@@ -1,28 +0,0 @@
-<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>
-
-</div>
diff --git a/templates/html/ReadingLogEntry.xml b/templates/html/ReadingLogEntry.xml
@@ -1,35 +0,0 @@
-<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, idify
-from recaptcha.client import captcha
-?>
-
-<span py:def="stars(rating)" py:strip="True">
-<!-- XXX script_name -->
-<img src="/static/images/star.png" alt="[star]" py:for="_ in range(int(rating))" /><img src="/static/images/star-half.png" alt="[half-star]" py:if="rating > int(rating)" /><img src="/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 id="ReadingLogEntry-${idify(item.title)}">
- <a py:strip="not item.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'))}
-</div>
-
-<div py:if="item.rating" class="rating">
- ${stars(item.rating)}
-</div>
-
-</div>
diff --git a/templates/html/_commonwrapper.xml b/templates/html/_commonwrapper.xml
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<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">
-
-<xi:include href="_fragments.xml" />
-
-<?python
-import yaml
-?>
-
-<py:match path="head">
- <head profile="http://gmpg.org/xfn/11" py:attrs="select('@*')" py:with="title = unicode(select('title[1]/text()'))">
- ${select('./*[local-name() != "title"]')}
- <title py:if="title">${title} - ${config.getunicode('global', 'name')}</title>
- <title py:if="not title">${config.getunicode('global', 'name')}</title>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <meta name="generator" content="constance" />
- <link rel="stylesheet" type="text/css" href="/static/css/common.css" /><!-- XXX script_name -->
- </head>
-</py:match>
-
-<py:match path="body">
- <body py:attrs="select('@*')">
- <div id="contentwrapper">
- <div id="content">
- ${select('./*')}
- </div>
- </div>
- <div id="links"
- py:if="config.getunicode('links', 'filename')"
- py:with="link_sections = yaml.load(open(config.getunicode('links', 'filename'), 'r'))">
- <div class="linkrow" py:for="section, links in link_sections.iteritems()">
- <h2>${section}</h2>
- <p>
- <py:for each="n, link in enumerate(links)">
- <py:if test="n > 0">${inline_sep()}</py:if>
- <a py:attrs="dict((a, v) for a, v in link.iteritems() if a != 'anchor')">${link['anchor']}</a>
- </py:for>
- </p>
- </div>
- </div>
- </body>
-</py:match>
-
-</div>
-
diff --git a/templates/html/_fragments.xml b/templates/html/_fragments.xml
@@ -1,13 +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">
-
-<?python
-import urllib
-?>
-
-<span py:def="inline_sep()" class="inlinesep">·</span>
-<div py:def="block_sep()" class="blocksep">~</div>
-
-</div>
diff --git a/templates/html/multiple.xml b/templates/html/multiple.xml
@@ -1,46 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude"
- lang="en-AU">
-<xi:include href="_commonwrapper.xml" />
-
-<?python
-sorted_items = sorted(items, key=lambda item: item.publication_date, reverse=True)
-?>
-
-<head>
- <title py:if="title">${title}</title>
- <link rel="alternate" type="application/atom+xml" title="Atom feed" href="?format=atom" />
- <py:if test="defined('offset')">
- <link py:if="bool(offset)" rel="prev" href="?offset=${max(0, offset - 20)}" />
- <link py:if="len(sorted_items) > offset + config.getint('global', 'items_per_page')" rel="next" href="?offset=${offset + 20}" />
- </py:if>
-</head>
-<body>
-
-<h2 py:if="title">Archive of ${title}</h2>
-
-<py:for each="item in (defined('offset') and sorted_items[offset:offset + config.getint('global', 'items_per_page')] or sorted_items)">
- ${item.render('text/html')}
-
- <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>
-</py:for>
-
-<p id="prevnextlinks"
- py:if="defined('offset') and config.getboolean('global', 'show_prev_next')"
- py:with="show_prev = bool(offset);
- show_next = len(sorted_items) > offset + config.getint('global', 'items_per_page')">
- <a py:if="show_prev" rel="prev" href="?offset=${max(0, offset - 20)}">Newer items</a>
- <py:if test="show_prev and show_next">—</py:if>
- <a py:if="show_next" rel="next" href="?offset=${offset + 20}">Older items</a>
-</p>
-
-</body>
-</html>
diff --git a/templates/html/single.xml b/templates/html/single.xml
@@ -1,70 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude"
- lang="en-AU">
-<xi:include href="_commonwrapper.xml" />
-
-<?python
-from viewutils import markdown
-from recaptcha.client import captcha
-?>
-
-<head>
- <title>${item.title}</title>
-</head>
-<body>
-
-${item.render('text/html')}
-
-<div id="comments" class="commentblock" py:if="item.has_comments()"
- py:with="comments = sorted(item.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="${item.uri_path}/comments/+new"><!-- XXX script_name -->
-
- <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>
-
-</body>
-</html>
diff --git a/templates/html/tag_cloud.xml b/templates/html/tag_cloud.xml
@@ -1,24 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude"
- lang="en-AU">
-<xi:include href="_commonwrapper.xml" />
-
-<head>
- <title>Tag cloud</title>
- <link rel="stylesheet" type="text/css" href="/static/css/tag_cloud.css" /><!-- XXX script_name -->
-</head>
-<body>
-
-<h2>Tag cloud</h2>
-
-<ol id="tagcloud">
- <li py:for="tag, freq in sorted(tag_freqs.iteritems(), key=lambda (t, f): t.lower())">
- <!-- XXX script_name --><a rel="tag" href="/tags/${tag}" style="font-size: ${0.8 + (freq / 10.)}em;">${tag}</a>
- <span class="frequency">(used ${freq} times)</span>
- </li>
-</ol>
-
-</body>
-</html>
diff --git a/templates/reading/entry.atom b/templates/reading/entry.atom
@@ -0,0 +1,26 @@
+<entry xmlns="http://www.w3.org/2005/Atom"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<?python
+from viewutils import ATOM_TIME_FORMAT
+?>
+
+<id>${item.guid}</id>
+<published>${item.publication_date.strftime(ATOM_TIME_FORMAT)}</published>
+<updated>${item.modified_date.strftime(ATOM_TIME_FORMAT)}</updated>
+<author>
+ <name>Dan C</name>
+ <email>djc@djc.id.au</email>
+</author>
+<category py:for="tag in item.tags" scheme="http://www.djc.id.au/tags/" term="${tag}" />
+<title type="text">${item.title.striptags()} by ${item.author}</title>
+<summary py:if="item.rating" type="text">${item.rating} stars</summary>
+<content type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <p><a href="${item.url}">${item.title}</a> by ${item.author}</p>
+ <p py:if="item.rating">${item.rating} stars</p>
+ </div>
+</content>
+
+</entry>
diff --git a/templates/reading/reading.atom b/templates/reading/reading.atom
@@ -0,0 +1,21 @@
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<?python
+from viewutils import ATOM_TIME_FORMAT
+sorted_items = sorted(items, key=lambda item: item.publication_date, reverse=True)
+?>
+
+<id>http://www.djc.id.au/reading.atom</id>
+<title type="text">djc reading log</title>
+<link rel="self" type="application/atom+xml" href="http://www.djc.id.au/reading.atom" />
+<link rel="alternate" href="http://www.djc.id.au/reading" />
+<generator>constance</generator>
+<updated py:if="sorted_items">${max(item.modified_date for item in sorted_items).strftime(ATOM_TIME_FORMAT)}</updated>
+
+<py:for each="item in sorted_items">
+ ${item.generate_atom()}
+</py:for>
+
+</feed>
diff --git a/templates/reading/reading.html b/templates/reading/reading.html
@@ -0,0 +1,47 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ lang="en-AU">
+
+<?python
+from viewutils import markdown, mini_markdown, tag_list, idify
+?>
+
+<span py:def="stars(rating)" py:strip="True">
+ <img src="/images/star.png" alt="[star]" py:for="_ in range(int(rating))" /><img src="/images/star-half.png" alt="[half-star]" py:if="rating > int(rating)" /><img src="/images/star-off.png" alt="" py:for="_ in range(int(5 - rating))" />
+</span>
+
+<head>
+ <title>Reading log</title>
+ <link rel="alternate" type="application/atom+xml" title="Atom feed" href="reading.atom" />
+</head>
+
+<body>
+
+ <h1>Reading log</h1>
+
+ <py:for each="item in sorted(items, key=lambda e: e.publication_date, reverse=True)">
+ <div class="item reading-log-entry">
+
+ <img py:if="item.isbn" class="cover"
+ src="http://covers.librarything.com/devkey/f6da4b15120267233430bb13cdaf1be9/small/isbn/${item.isbn}"
+ alt="Cover image for ${item.title.striptags()}" />
+
+ <h3 id="${idify(item.title.striptags())}">
+ <a py:strip="not item.url" href="${item.url}">${item.title}</a>
+ <span py:if="item.author" class="author">by ${item.author}</span>
+ </h3>
+
+ <div class="date published">
+ ${item.publication_date.strftime(str('%-1d %b %Y'))}
+ </div>
+
+ <div py:if="item.rating" class="rating">
+ ${stars(item.rating)}
+ </div>
+
+ </div>
+ </py:for>
+
+</body>
+</html>
+
diff --git a/templates/tags/index.html b/templates/tags/index.html
@@ -0,0 +1,21 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ lang="en-AU">
+
+<head>
+ <title>Tag cloud</title>
+ <link rel="stylesheet" type="text/css" href="../style/tag_cloud.css" />
+</head>
+<body>
+
+<h1>Tag cloud</h1>
+
+<ol id="tagcloud">
+ <li py:for="tag, freq in sorted(tag_freqs.iteritems(), key=lambda (t, f): t.lower())">
+ <!-- XXX script_name --><a rel="tag" href="/tags/${tag}" style="font-size: ${0.8 + (freq / 10.)}em;">${tag}</a>
+ <span class="frequency">(used ${freq} times)</span>
+ </li>
+</ol>
+
+</body>
+</html>
diff --git a/templates/tags/tag.html b/templates/tags/tag.html
@@ -0,0 +1,24 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ lang="en-AU">
+
+<?python
+from itertools import groupby
+from viewutils import markdown, mini_markdown, tag_list
+?>
+
+<head>
+ <title>“${tag}” tag</title>
+</head>
+
+<body>
+
+ <h1>“${tag}” tag</h1>
+
+ <div class="item blog-entry-stub" py:for="item in sorted(items, key=lambda e: e.publication_date, reverse=True)">
+ <h3 class="entry-title"><a href="${item.id}">${item.title}</a></h3>
+ <div class="date published">${item.publication_date.strftime(str('%-1d %b %Y'))}</div>
+ </div>
+
+</body>
+</html>