qmsk_www_pages/pages.py
author Tero Marttila <terom@paivola.fi>
Sun, 14 Sep 2014 13:54:07 +0300
changeset 226 26ec457d0eb2
parent 224 cea4578f7910
child 99 88fa55fa0194
permissions -rw-r--r--
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)