"""
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)
def get_ancestry (self) :
"""
Returns a list of this page's parents and the page itself, but not root
"""
# collect in reverse order
ancestry = []
# starting from self
item = self
# add all items, but not root
while item and item.parent :
ancestry.append(item)
item = item.parent
# reverse
ancestry.reverse()
# done
return ancestry
@property
def url (self) :
"""
Build this page's URL
"""
# cached?
if self._url :
return self._url
segments = [item.name for item in self.get_ancestry()]
# add empty segment if dir
if self.children :
segments.append('')
# 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 if child != node
])
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')