qmsk_www_pages: pages TemplatePage handling for mako .tmpl pages, with DEBUG-dependent error handling
from django.conf import settings
import codecs
import datetime
import logging; log = logging.getLogger('qmsk_www_pages.pages')
import os, os.path
import markdown
import mako.template
class NotFound (Exception):
pass
class RenderError (Exception):
pass
class Site (object):
@classmethod
def lookup (cls):
return cls(
root = settings.QMSK_WWW_PAGES_DIR,
name = settings.QMSK_WWW_PAGES_SITE,
)
def __init__ (self, root, name):
self.root = root
self.name = name
class Tree (object):
INDEX = 'index'
@classmethod
def lookup (cls, site, parts):
"""
Returns Tree
Raises NotFound
"""
parents = ( )
tree = cls(site.root, None, parents, site,
title = site.name,
)
for name in parts:
if name.startswith('.'):
# evil
raise NotFound()
if not name:
continue
path = os.path.join(tree.path, name)
if not os.path.exists(path):
raise NotFound()
if not os.path.isdir(path):
raise NotFound()
# title
title = tree.item_title(name)
parents += (tree, )
tree = cls(path, name, parents, site,
title = title,
)
return tree
def __init__ (self, path, name, parents, site,
title = None,
):
"""
path: filesystem path
name: subtree name, or None for root
parents: (Tree)
site: Site
"""
self.path = path
self.name = name
self.parents = parents
self.site = site
self.title = title or name
def hierarchy (self):
"""
Yield Tree.
"""
for tree in self.parents:
yield tree
yield self
def url (self, tree=None, page=None):
path = '/'.join(tree.name for tree in self.hierarchy() if tree.name is not None)
if path:
path += '/'
if tree:
path = tree + '/'
if page:
path += page
return path
def scan (self):
"""
Scan for files in tree.
"""
for filename in os.listdir(self.path):
if filename.startswith('.'):
continue
if '.' in filename:
file_name, file_type = filename.rsplit('.', 1)
else:
file_name = filename
file_type = None
if not file_name:
continue
path = os.path.join(self.path, filename)
yield path, file_name, file_type
def item_title (self, name):
"""
Lookup item title if exists.
"""
title_path = os.path.join(self.path, name + '.title')
log.info("%s: %s title_path=%s", self, name, title_path)
if os.path.exists(title_path):
return open(title_path).read().strip()
else:
return None
def list (self):
"""
Lists all Trees and Pages for this Tree.
Yields (name, url, page_type or None, title)
"""
for path, name, file_type in self.scan():
title = self.item_title(name) or name
# trees
if os.path.isdir(path):
yield name, self.url(tree=name), None, title
if name == self.INDEX:
continue
# pages
if not file_type:
continue
if file_type not in TYPES:
continue
yield name, self.url(page=name), file_type, title
def list_sorted (self):
return sorted(list(self.list()))
def page (self, name):
"""
Scans through tree looking for a matching page.
Returns Page or None.
"""
if not name:
name = self.INDEX
title_default = self.title
else:
title_default = None
parents = self.parents + (self, )
for path, file_name, file_type in self.scan():
# match on name
if file_name == name:
pass
elif file_type and (file_name + '.' + file_type == name):
pass
else:
continue
# redirects?
if os.path.islink(path):
target = os.readlink(path)
# XXX: this should be some kind of common code
if '.' in target:
target, target_type = target.rsplit('.', 1)
log.info("%s: %s -> %s", self, name, target)
return RedirectPage(path, name, self, parents,
target = target,
)
# match on type
if not file_type:
continue
page_type = TYPES.get(file_type)
if not page_type:
continue
# out
title = self.item_title(file_name) or title_default
return page_type(path, name, self, parents,
title = title,
)
class Page (object):
ENCODING = 'utf-8'
@classmethod
def lookup (cls, site, page):
"""
Returns Page.
Raises NotFound
"""
log.info("page=%r", page)
if page:
parts = page.split('/')
else:
parts = [ ]
if parts:
page_name = parts.pop(-1)
tree_parts = parts
else:
page_name = ''
tree_parts = []
# scan dir
tree = Tree.lookup(site, tree_parts)
# scan page
page = tree.page(page_name)
if not page:
raise NotFound()
return page
def __init__ (self, path, name, tree, parents=(), encoding=ENCODING, title=None):
self.path = path
self.name = name
self.tree = tree
self.parents = parents
self.encoding = encoding
self.title = title or name
def hierarchy (self):
"""
Yield (Tree, name) pairs
"""
parent = None
for tree in self.parents:
if parent:
yield parent, tree.name
parent = tree
yield parent, self.name
def url (self):
return self.tree.url(page=self.name)
def open (self):
return codecs.open(self.path, encoding=self.encoding)
def stat (self):
return os.stat(self.path)
def breadcrumb (self):
for tree in self.tree.hierarchy():
yield tree.url(), tree.title
if self.name != self.tree.INDEX:
yield self.url(), self.title
def modified (self):
return datetime.datetime.utcfromtimestamp(self.stat().st_mtime)
def redirect_page (self, request):
return None
def render_html (self, request):
raise NotImplementedError()
# TODO: tree redirects
class RedirectPage (Page):
def __init__ (self, path, name, tree, parents,
target,
**opts
) :
super(RedirectPage, self).__init__(path, name, tree, parents, **opts)
self.target = target
def redirect_page (self, request):
return os.path.normpath(self.tree.url() + '/' + self.target)
class HTML_Page (Page):
def render_html (self, request):
return self.open().read()
class MarkdownPage (Page):
FORMAT = 'html5'
def __init__ (self, path, name, tree, parents,
format=FORMAT,
**opts
) :
super(MarkdownPage, self).__init__(path, name, tree, parents, **opts)
self.format = format
def render_html (self, request):
return markdown.markdown(self.open().read(),
output_format = self.format,
)
class TemplatePage (Page):
def render_html (self, request):
"""
Raises RenderError if !DEBUG, arbitrary error with stack trace otherwise.
"""
try:
return mako.template.Template(filename=self.path).render()
except Exception as error:
if settings.DEBUG:
raise
else:
raise RenderError(error)
SITE = Site.lookup()
TYPES = {
'html': HTML_Page,
'md': MarkdownPage,
'markdown': MarkdownPage,
'tmpl': TemplatePage,
}
def page (page):
return Page.lookup(SITE, page)