and it works, a lot better than before
authorTero Marttila <terom@fixme.fi>
Fri, 06 Feb 2009 22:48:00 +0200
changeset 8 0ce1f471e9d7
parent 7 d6a8258bd90e
child 9 2a47b00f60b0
and it works, a lot better than before
.hgignore
lib/handler.py
lib/http.py
lib/page.py
lib/template.py
pages/foo.html
pages/index.py
pages/index.tmpl
templates/layout.tmpl
--- a/.hgignore	Fri Feb 06 21:31:02 2009 +0200
+++ b/.hgignore	Fri Feb 06 22:48:00 2009 +0200
@@ -1,4 +1,4 @@
 syntax: regexp
 \.[^/]+.sw[op]$
-^site/templates/[^/]+.py$
+^cache/templates/.
 
--- a/lib/handler.py	Fri Feb 06 21:31:02 2009 +0200
+++ b/lib/handler.py	Fri Feb 06 22:48:00 2009 +0200
@@ -2,12 +2,26 @@
     The actual application behaviour, i.e. generating a Response from a Request :)
 """
 
-import http, page
+import http, page, template
 
 def handle_request (request) :
     """
         Take the Request, and return a Response
     """
 
-    return http.Response("Hello World")
+    # determine the page name
+    page_name = request.get_page_name()
 
+    # get the page handler
+    p = page.lookup(page_name)
+
+    # render the template
+    response_data = template.render("layout",
+        site_root_url   = request.get_script_dir(),
+        page_title      = p.get_title(),
+        page_content    = p.get_content(),
+    )
+    
+    # return the response
+    return http.Response(response_data)
+
--- a/lib/http.py	Fri Feb 06 21:31:02 2009 +0200
+++ b/lib/http.py	Fri Feb 06 22:48:00 2009 +0200
@@ -8,6 +8,9 @@
 # for header handling
 import wsgiref.headers
 
+# for path handling
+import os.path
+
 class Request (object) :
     """
         HTTP Request with associated metadata
@@ -26,6 +29,29 @@
 
         # parse query args
         self.arg_dict = cgi.parse_qs(self.arg_str, True)
+    
+    def get_script_dir (self) :
+        """
+            Returns the URL path to the requested script's directory with no trailing slash, i.e.
+
+            /               -> 
+            /foo.cgi        -> 
+            /foo/bar.cgi    -> /foo
+        """
+
+        return os.path.dirname(self.env['SCRIPT_NAME'])
+    
+    def get_page_name (self) :
+        """
+            Returns the requested page path with no leading slash, i.e.
+
+            /foo.cgi        -> 
+            /foo.cgi/       -> 
+            /foo.cgi/bar    -> bar
+            /foo.cgi/quux/  -> quux/
+        """
+        
+        return os.path.normpath(self.env['PATH_INFO']).lstrip('/')
 
 class Response (object) :
     """
@@ -91,19 +117,25 @@
         A response with an error code / message
     """
 
-    def __init__ (self, status, message) :
+    def __init__ (self, status, message, details=None) :
         """
             Build a plain error message response with the given status/message
+
+            @param status HTTP status code
+            @param message short message to describe errors
+            @param details optional details, plaintext
         """
 
         data = """\
 <html><head><title>%(title)s</title></head><body>
 <h1>%(title)s</h1>
 <p>%(message)s</p>
+%(details)s
 </body></html>
 """ % dict(
             title       = status, 
-            message     = message
+            message     = message,
+            details     = '<pre>%s</pre>' % details if details else ''
         )
             
         super(ErrorResponse, self).__init__(data, status=status)
@@ -113,14 +145,15 @@
         An exception that results in a specfic 4xx ErrorResponse message to the client
     """
 
-    def __init__ (self, message, status='400 Bad Request') :
+    def __init__ (self, message, status='400 Bad Request', details=None) :
         self.status = status
         self.message = message
+        self.details = details
 
         super(ResponseError, self).__init__(message)
 
     def get_response (self) :
-        return ErrorResponse(self.status, self.message)
+        return ErrorResponse(self.status, self.message, self.details)
 
 class Redirect (Response) :
     """
--- a/lib/page.py	Fri Feb 06 21:31:02 2009 +0200
+++ b/lib/page.py	Fri Feb 06 22:48:00 2009 +0200
@@ -3,19 +3,169 @@
     Handling page requests
 """
 
-def handle_html (env) :
-    return "A HTML page"
+# 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"
+
+class PageError (http.ResponseError) :
+    """
+        Error looking up/handling a page
+    """
+
+    pass
+
+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
+
+        # sub-init
+        self._init()
+
+    def _init (self) :
+        """
+            Do initial data loading, etc
+        """
+        
+        pass
+
+    def get_title (self) :
+        """
+            Return the page's title
+
+            Defaults to the Titlecase'd file basename
+        """
+
+        return self.basename.title()
+
+    def get_content (self) :
+        """
+            Return the page content as a string
+        """
+
+        abstract
+
+class HTMLPage (Page) :
+    """
+        A simple .html page that's just passed through directly
+    """
+
+    def get_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
+    """
+
+    def get_content (self) :
+        """
+            Loads the .tmpl file, and renders it
+        """
+
+        return template.render_file(self.path)
 
 # list of page handlers, by type
-type_handlers = [
-    ('.html',   handle_html),
+TYPE_HANDLERS = [
+    ('html',                    HTMLPage        ),
+    (template.TEMPLATE_EXT,     TemplatePage    ),
 ]
 
-def lookup_handler (path) :
+def _lookup_handler (url, path, filename, basename, extension, tail) :
     """
-        Look up and return a handler for the given page, or raise an error
+        We found the file that we looked for, now get its handler
     """
 
-    return handle_html
+    # 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 segments :
+        # pop segment
+        segment = segments.pop(0)
+
+        # add to page url
+        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
+
+    # did not find the filename we were looking for in os.listdir
+    raise PageError("Page not found: %s" % name, status='404 Not Found')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/template.py	Fri Feb 06 22:48:00 2009 +0200
@@ -0,0 +1,80 @@
+"""
+    Template handler
+"""
+
+# use Mako
+from mako import exceptions
+from mako.template import Template
+from mako.lookup import TemplateLookup
+
+# for http.ResponseError
+import http
+
+# path to template files
+TEMPLATE_DIR = "templates"
+
+# path to cached templates
+CACHE_DIR = "cache/templates"
+
+# template file extension
+TEMPLATE_EXT = "tmpl"
+
+
+# our Mako template lookup handler
+_lookup = TemplateLookup(directories=[TEMPLATE_DIR], module_directory=CACHE_DIR)
+
+
+class TemplateError (http.ResponseError) :
+    """
+        Raised by the template module functions
+    """
+
+    pass
+
+def lookup (name) :
+    """
+        Looks up a template based on the bare "name", which does not include the path or file extension
+    """
+    
+    try :
+        return _lookup.get_template("%s.%s" % (name, TEMPLATE_EXT))
+
+    except :
+        raise TemplateError("Template broken: %r" % (name, ), status='500 Internal Server Error', details=exceptions.text_error_template().render())
+
+def load (path) :
+    """
+        Loads a template from a specific file
+    """
+
+    try :
+        return Template(filename=path, module_directory=CACHE_DIR)
+
+    except :
+        raise TemplateError("Template broken: %r" % (path, ), status='500 Internal Server Error', details=exceptions.text_error_template().render())
+
+def render_template (tpl, **params) :
+    """
+        Render the given template, returning the output, or raising a TemplateError
+    """
+
+    try :
+        return tpl.render(**params)
+
+    except :
+        raise TemplateError("Template render failed", status='500 Internal Server Error', details=exceptions.text_error_template().render())
+
+def render (name, **params) :
+    """
+        Render a template, using lookup() on the given name
+    """
+
+    return render_template(lookup(name), **params)
+
+def render_file (path, **params) :
+    """
+        Render a template, using load() on the given path
+    """
+
+    return render_template(load(path), **params)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pages/foo.html	Fri Feb 06 22:48:00 2009 +0200
@@ -0,0 +1,3 @@
+
+<strong>HTML</strong> foo!
+
--- a/pages/index.py	Fri Feb 06 21:31:02 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-
-import page
-
-class Page (page.Page) :
-    """
-        Main page with simple stuff
-    """
-    
-    title = "Main Page"
-    name = "Main"
-    path = ""
-    
-    parent = None
-    menu = [
-        "",
-        "main"
-    ]
-
-    def render_template (self) :
-        return self._build_template(_templates.main)
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pages/index.tmpl	Fri Feb 06 22:48:00 2009 +0200
@@ -0,0 +1,8 @@
+<h1>A Mako template!</h1>
+
+<ul>
+% for char in 'Hello World' :
+    <li>${char}</li>
+% endfor    
+</ul>
+
--- a/templates/layout.tmpl	Fri Feb 06 21:31:02 2009 +0200
+++ b/templates/layout.tmpl	Fri Feb 06 22:48:00 2009 +0200
@@ -1,18 +1,9 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 
-#def page_title
-qmsk.net
-#end def
-#def page_content
-<h1>Content Goes Here</h1>
-
-Content goes here.
-#end def
-
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
     <head>
-        <title>qmsk.net :: $page_title</title>
-        <link rel="Stylesheet" type="text/css" href="$site_url/style.css" />
+        <title>qmsk.net :: ${page_title}</title>
+        <link rel="Stylesheet" type="text/css" href="${site_root_url}/static/style.css" />
     </head>
     <body>
             <div id="header">
@@ -20,13 +11,11 @@
             </div>
  
             <ul id="nav">
-                #for $menu_page_path in $page.parent.menu
-                <li><a href="$page_root/$menu_page_path"#if $page.path == $menu_page_path then ' id="selected-page"' else '' #>$menu_page_path</a></li>
-                #end for
+                <li><a href="#foo">Foo</a></li>
             </ul>
 
             <div id="content">
-                $page_content
+                ${page_content}
             </div>
     </body>
 </html>