lib/page.py
author Tero Marttila <terom@fixme.fi>
Sat, 07 Feb 2009 01:33:30 +0200
changeset 11 fa216534ae45
parent 10 d83b10c210e3
child 12 2abc5ace0b15
permissions -rw-r--r--
funky PageTree stuff

"""
    Handling page requests
"""

# for filesystem ops
import os, os.path

# for ResponseError
import http

# for TemplatePage
import template

# path to directory containing the page heirarcy
PAGE_DIR = "pages"

# path to directory containing the list of visible pages
PAGE_LIST_FILE = os.path.join(PAGE_DIR, "list")

class PageError (http.ResponseError) :
    """
        Error looking up/handling a page
    """

    pass

class PageInfo (object) :
    """
        Contains metainformation about a page
    """

    def __init__ (self, parent, name, title, children=None) :
        """
            Initialize, children defaults to empty list
        """

        # store
        self.parent = parent
        self.name = name
        self.title = title
        self.children = children if children else []

        # no url get
        self._url = None
    
    def set_parent (self, parent) :
        """
            Set a parent where non was set before
        """

        assert self.parent is None

        self.parent = parent

    def add_child (self, child) :
        """
            Add a PageInfo child
        """

        self.children.append(child)
    
    def get_child (self, name) :
        """
            Look up a child by name, returning None if not found
        """

        return dict((c.name, c) for c in self.children).get(name)

    @property
    def url (self) :
        """
            Build this page's URL
        """

        # cached?
        if self._url :
            return self._url
        
        # collect URL segments in reverse order
        segments = []

        # add empty segment if dir
        if self.children :
            segments.append('')
        
        # iterate over ancestry
        item = self
        
        # add all parent names, but not root's
        while item and item.parent :
            segments.append(item.name)

            item = item.parent

        # reverse segments
        segments.reverse()

        # join
        url = '/'.join(segments)
        
        # cache
        self._url = url
        
        # done
        return url

class PageTree (object) :
    """
        The list of pages
    """

    def __init__ (self) :
        """
            Empty PageList, must call load_page_list to initialize, once
        """

    def _load (self, path=PAGE_LIST_FILE) :
        """
            Processes the lines in the given file
        """
        
        # collect the page list
        pages = []
        
        # stack of (indent, PageInfo) items
        stack = []

        # the previous item processed, None for first one
        prev = None

        for line in open(path, 'rb') :
            indent = 0

            # count indent
            for char in line :
                # tabs break things
                assert char != '\t'
                
                # increment up to first non-space char
                if char == ' ' :
                    indent += 1

                elif char == ':' :
                    indent = 0
                    break

                else :
                    break
            
            # strip whitespace
            line = line.strip()

            # ignore empty lines
            if not line :
                continue

            # parse line
            url, title = line.split(':')

            # remove whitespace
            url = url.strip()
            title = title.strip()
            
            # create PageInfo item without parent
            item = PageInfo(None, url, title)

            # are we the first item?
            if not prev :
                assert url == '', "Page list must begin with root item"
                
                # root node does not have a parent
                parent = None
                
                # set root
                self.root = item

                # tee hee hee
                self.root.add_child(self.root)

                # initialize stack
                stack.append((0, self.root))
                
            else :
                # peek stack
                stack_indent, stack_parent = stack[-1]

                # new indent level?
                if indent > stack_indent :
                    # set parent to previous item, and push new indent level + parent to stack
                    parent = prev

                    # push new indent level + its parent
                    stack.append((indent, parent))

                # same indent level as previous
                elif indent == stack_indent :
                    # parent is the one of the current stack level, stack doesn't change
                    parent = stack_parent
                
                # unravel stack
                elif indent < stack_indent :
                    while True :
                        # remove current stack level
                        stack.pop(-1)

                        # peek next level
                        stack_indent, stack_parent = stack[-1]
                        
                        # found the level to return to?
                        if stack_indent == indent :
                            # restore prev
                            parent = stack_parent

                            break

                        elif stack_indent < indent :
                            assert False, "Bad un-indent"
            
            # add to parent?
            if parent :
                item.set_parent(parent)
                parent.add_child(item)

            # update prev
            prev = item
        
        # get root
        assert hasattr(self, 'root'), "No root item found!"
        
    def get_page (self, url) :
        """
            Lookup the given page URL, and return the matching PageInfo object, or None, if not found
        """
        
        # start from root
        node = self.root
        
        # traverse the object tree
        for segment in url.split('/') :
            if segment :
                node = node.get_child(segment)

            if not node :
                return None
        
        # return
        return node
    
    def get_siblings (self, url) :
        """
            Get the list of siblings for the given url, including the given page itself
        """
        
        # look up the page itself
        page = self.get_page(url)
        
        # specialcase root/unknown node
        if page and page.parent :
            return page.parent.children

        else :
            return self.root.children
    
    def dump (self) :
        """
            Returns a string representation of the tree
        """
        
        def _print_node (indent, node) :
            return '\n'.join('%s%s' % (' '*indent, line) for line in [
                "%-15s : %s" % (node.name, node.title)
            ] + [
                _print_node(indent + 4, child) for child in node.children
            ])

        return _print_node(0, self.root)

# global singleton PageList instance
page_tree = PageTree()

def load_page_tree () :
    """
        Load the global singleton PageInfo instance
    """
    
    page_tree._load()

# XXX: should inherit from PageInfo
class Page (object) :
    """
        This object represents the information about our attempt to render some specific page
    """

    def __init__ (self, url, path, basename, url_tail) :
        """
            Initialize the page at the given location

            @param url the URL leading to this page
            @param path the filesystem path to this page's file
            @param basename the filesystem name of this page's file, without the file extension
            @param url_trail trailing URL for this page
        """
        
        # store
        self.url = url
        self.path = path
        self.basename = basename
        self.url_tail = url_tail

        # unbound
        self.request = None

        # sub-init
        self._init()

    def _init (self) :
        """
            Do initial data loading, etc
        """
        
        pass

    def bind_request (self, request) :
        """
            Bind this page-render to the given request
        """

        self.request = request

    @property
    def title (self) :
        """
            Return the page's title

            Defaults to the retreiving the page title from page_list, or basename in Titlecase.
        """
        
        # lookup in page_list
        page_info = page_tree.get_page(self.url)
        
        # fallback to titlecase
        if page_info :
            title = page_info.title

        else :
            title = self.basename.title()

        return title
    
    @property
    def content (self) :
        """
            Return the page content as a string
        """

        abstract

class HTMLPage (Page) :
    """
        A simple .html page that's just passed through directly
    """

    @property
    def content (self) :
        """
            Opens the .html file, reads and returns contents
        """

        return open(self.path, 'rb').read()

class TemplatePage (Page) :
    """
        A template that's rendered using our template library
    """
    
    @property
    def content (self) :
        """
            Loads the .tmpl file, and renders it
        """

        return template.render_file(self.path,
            request     = self.request,
            page_tree   = page_tree
        )

# list of page handlers, by type
TYPE_HANDLERS = [
    ('html',                    HTMLPage        ),
    (template.TEMPLATE_EXT,     TemplatePage    ),
]

def _lookup_handler (url, path, filename, basename, extension, tail) :
    """
        We found the file that we looked for, now get its handler
    """

    # find appropriate handler
    for handler_ext, handler in TYPE_HANDLERS :
        # match against file extension?
        if handler_ext == extension :
            # found handler, return instance
            return handler(url, path, basename, tail)

    # no handler found
    raise PageError("No handler found for page %r of type %r" % (url, extension))

def lookup (name) :
    """
        Look up and return a Page object for the given page, or raise an error
    """

    # inital path
    path = PAGE_DIR
    url_segments = []

    # name segments
    segments = name.split('/')

    # iterate through the parts of the page segments
    while True :
        segment = None

        # pop segment
        if segments :
            segment = segments.pop(0)

            url_segments.append(segment)

        # translate empty -> index
        if not segment :
            segment = 'index'

        # look for it in the dir
        for filename in os.listdir(path) :
            # build full file path
            file_path = os.path.join(path, filename)

            # stat, recurse into subdirectory?
            if os.path.isdir(file_path) and filename == segment :
                # use new dir
                path = file_path

                # break for-loop to look at next segment
                break
 
            # split into basename + extension
            basename, extension = os.path.splitext(filename)

            # ...remove that dot
            extension = extension.lstrip('.')
            
            # match against requested page name?
            if basename == segment :
                # found the file we wanted
                return _lookup_handler('/'.join(url_segments), file_path, filename, basename, extension, '/'.join(segments))
            
            else :
                # inspect next file in dir
                continue

        else :
            # did not find any dir or file, break out of while loop
            break

    # did not find the filename we were looking for in os.listdir
    raise PageError("Page not found: %s" % name, status='404 Not Found')