more fiddling around with the irclogs layout/css, add query args to URL sites
authorTero Marttila <terom@fixme.fi>
Sun, 08 Feb 2009 02:29:23 +0200
branchsites
changeset 42 5a72c00c4ae4
parent 41 9585441a4bfb
child 43 fc11c4e86a82
more fiddling around with the irclogs layout/css, add query args to URL
lib/helpers.py
lib/http.py
lib/template.py
sites/irclogs.qmsk.net/channels.py
sites/irclogs.qmsk.net/handlers.py
sites/irclogs.qmsk.net/log_channel.py
sites/irclogs.qmsk.net/logs/openttd
sites/irclogs.qmsk.net/templates/channel.tmpl
sites/irclogs.qmsk.net/templates/index.tmpl
sites/irclogs.qmsk.net/templates/layout.tmpl
sites/irclogs.qmsk.net/urls.py
sites/irclogs.qmsk.net/urltree.py
sites/www.qmsk.net/page.py
sites/www.qmsk.net/templates/layout.tmpl
static/irclogs.css
--- a/lib/helpers.py	Sun Feb 08 00:29:36 2009 +0200
+++ b/lib/helpers.py	Sun Feb 08 02:29:23 2009 +0200
@@ -20,6 +20,15 @@
 
     return time.strftime("%Y")
 
+def validation_notice (site_host) :
+    """
+        Returns a short "Validated XHTML & CSS" link text for the given site hostname
+    """
+
+    return 'Validated <a href="http://validator.w3.org/check?uri=%(host)s">XHTML 1.0 Strict</a> &amp; <a href="http://jigsaw.w3.org/css-validator/validator?uri=%(host)s">CSS 2.1</a>' % dict(
+        host = site_host
+    )
+
 def breadcrumb (trail, links=True) :
     """
         Returns a nicely formatted breadcrumb tail, optinally with links
--- a/lib/http.py	Sun Feb 08 00:29:36 2009 +0200
+++ b/lib/http.py	Sun Feb 08 02:29:23 2009 +0200
@@ -29,8 +29,17 @@
 
         # parse query args
         self.arg_dict = cgi.parse_qs(self.arg_str, True)
-    
-    def get_script_dir (self) :
+ 
+    @property
+    def site_host (self) :
+        """
+            Returns the site's hostname (DNS name)
+        """
+        
+        return self.env['HTTP_HOST']
+  
+    @property
+    def site_root (self) :
         """
             Returns the URL path to the requested script's directory with no trailing slash, i.e.
 
@@ -41,7 +50,8 @@
 
         return os.path.dirname(self.env['SCRIPT_NAME']).rstrip('/')
     
-    def get_page_prefix (self) :
+    @property
+    def page_prefix (self) :
         """
             Returns the URL path root for page URLs, based on REQUEST_URI with PATH_INFO removed
 
@@ -68,7 +78,7 @@
         
         # trim
         return request_path[:-len(page_name)].rstrip('/')
-
+    
     def get_page_name (self) :
         """
             Returns the requested page path with no leading slash, i.e.
@@ -88,6 +98,13 @@
 
         else :
             return ''
+    
+    def get_args (self) :
+        """
+            Iterate over all available (key, value) pairs from the query string
+        """
+
+        return cgi.parse_qsl(self.arg_str)
 
 class Response (object) :
     """
--- a/lib/template.py	Sun Feb 08 00:29:36 2009 +0200
+++ b/lib/template.py	Sun Feb 08 02:29:23 2009 +0200
@@ -28,45 +28,51 @@
 
     pass
 
-def render (tpl, **params) :
-    """
-        Render the given template, returning the output as a unicode string, or raising a TemplateError
-    """
-
-    try :
-        return tpl.render_unicode(
-            # global helper stuff
-            h           = helpers,
-
-            # render-specific params
-            **params
-        )
-    
-    # a template may render other templates
-    except TemplateError :
-        raise
-
-    except :
-        details = exceptions.text_error_template().render()
-
-        raise TemplateError("Template render failed", status='500 Internal Server Error', details=details)
-
 class TemplateLoader (mako.lookup.TemplateLookup) :
     """
         Our own specialization of mako's TemplateLookup
     """
 
-    def __init__ (self, path, fileext=TEMPLATE_EXT) :
+    def __init__ (self, path, fileext=TEMPLATE_EXT, **env) :
         """
-            Initialize to load templates located at path, with the given file extension
+            Initialize to load templates located at path, with the given file extension.
+
+            The given **env list is supplied to every template render
         """
 
         # store
         self.path = path
         self.fileext = fileext
+        self.env = env
+            
+        # build the TemplateLookup
+        super(TemplateLoader, self).__init__(directories=[path], module_directory=CACHE_DIR)
+    
+    @staticmethod
+    def _render (tpl, env, params) :
+        """
+            Render the given template with given env/params, returning the output as a unicode string, or raising a TemplateError
+        """
+
+        # build the context from our superglobals, env, and params
+        ctx = dict(
+            h       = helpers,
+        )
+        ctx.update(env)
+        ctx.update(params)
+
+        try :
+            return tpl.render_unicode(**ctx)
         
-        # XXX: separate cache?
-        super(TemplateLoader, self).__init__(directories=[path], module_directory=CACHE_DIR)
+        # a template may render other templates
+        except TemplateError :
+            raise
+
+        except :
+            details = exceptions.text_error_template().render()
+
+            raise TemplateError("Template render failed", status='500 Internal Server Error', details=details)
+
 
     def lookup (self, name) :
         """
@@ -84,7 +90,7 @@
             Render a template, using lookup() on the given name
         """
 
-        return render(self.lookup(name), **params)
+        return self._render(self.lookup(name), self.env, params)
 
     def render_to_response (self, name, **params) :
         """
@@ -108,9 +114,15 @@
     @classmethod
     def render_file (cls, path, **params) :
         """
-            Render a template, using load() on the given path
+            Render a template, using load() on the given path. No global environment vars are defined for the render.
         """
 
-        return render(cls.load(path), **params)
+        return cls._render(cls.load(path), dict(), params)
 
-
+    @classmethod
+    def render_template (cls, template, **params) :
+        """
+            Render the given template object. No global environment vars are defined for the render.
+        """
+        
+        return cls._render(template, dict(), params)
--- a/sites/irclogs.qmsk.net/channels.py	Sun Feb 08 00:29:36 2009 +0200
+++ b/sites/irclogs.qmsk.net/channels.py	Sun Feb 08 02:29:23 2009 +0200
@@ -22,6 +22,9 @@
         'tycoon':   LogChannel('tycoon', "OFTC", "#tycoon", 
             LogDirectory(relpath('logs/tycoon'), pytz.timezone('Europe/Helsinki'))
         ),
+        'openttd':   LogChannel('openttd', "OFTC", "#openttd", 
+            LogDirectory(relpath('logs/openttd'), pytz.timezone('Europe/Helsinki'))
+        ),
     }
 
     def __init__ (self, channels) :
--- a/sites/irclogs.qmsk.net/handlers.py	Sun Feb 08 00:29:36 2009 +0200
+++ b/sites/irclogs.qmsk.net/handlers.py	Sun Feb 08 02:29:23 2009 +0200
@@ -4,15 +4,29 @@
 
 from lib import http, template
 
+import urls, channels
+
 # load templates from here
-templates = template.TemplateLoader("sites/irclogs.qmsk.net/templates")
+templates = template.TemplateLoader("sites/irclogs.qmsk.net/templates",
+    urls            = urls,
+    channel_list    = channels.channel_list,
+)
 
 def index (request) :
     """
         The topmost index page, display a list of available channels, perhaps some general stats
     """
     
-    return templates.render_to_response("index")
+    return templates.render_to_response("index",
+        req             = request,
+    )
+
+def channel_select (request, channel) :
+    """
+        Redirect to the appropriate channel_view
+    """
+   
+    return http.Redirect(urls.channel_view.build(request, channel=channel.id))
 
 def channel_view (request, channel) :
     """
@@ -20,6 +34,7 @@
     """
 
     return templates.render_to_response("channel",
+        req             = request,
         channel         = channel,
     )
 
--- a/sites/irclogs.qmsk.net/log_channel.py	Sun Feb 08 00:29:36 2009 +0200
+++ b/sites/irclogs.qmsk.net/log_channel.py	Sun Feb 08 02:29:23 2009 +0200
@@ -16,4 +16,12 @@
         self.network = network
         self.name = name
         self.source = source
+    
+    @property
+    def title (self) :
+        """
+            Title is 'Network - #channel'
+        """
 
+        return "%s - %s" % (self.network, self.name)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sites/irclogs.qmsk.net/logs/openttd	Sun Feb 08 02:29:23 2009 +0200
@@ -0,0 +1,1 @@
+/home/terom/backups/zapotek-irclogs/#openttd
\ No newline at end of file
--- a/sites/irclogs.qmsk.net/templates/channel.tmpl	Sun Feb 08 00:29:36 2009 +0200
+++ b/sites/irclogs.qmsk.net/templates/channel.tmpl	Sun Feb 08 02:29:23 2009 +0200
@@ -1,6 +1,45 @@
-<h1>Channel ${channel.name}</h1>
+<%inherit file="layout.tmpl" />
 
-<h2>Last 10 lines:</h2>
+<%def name="menu()">
+<ul>
+    <li><a href="${urls.index.build(req)}">Home</a></li>
+
+    <li>
+        <form action="${urls.channel_select.build(req)}" method="GET">
+            <label for="channel">Channel:</label>
+
+            <select name="channel">
+            % for ch in channel_list :
+                <option value="${ch.id}"${' selected="selected"' if ch == channel else ''}>${ch.title}</option>
+            % endfor
+            </select><input type="submit" value="Go &raquo;" />
+        </form>
+    </li>
+
+    <li>
+        <form action="#" method="GET">
+            View last
+
+            <select name="count">
+            % for count in (10, 20, 50, 100, 'all') :
+                <option>${count}</option>
+            % endfor
+            </select>
+
+            lines: <input type="submit" value="Go &raquo;" />
+        </form>
+    </li>
+
+    <li><a href="#">Browse by Date</a></li>
+
+    <li><a href="#">Search</a></li>
+
+    <li><a href="#">[RSS]</a></li>
+</ul>
+</%def>
+
+<h1>${channel.title} &raquo; Last 10 lines</h1>
+
 <pre>
 % for line in channel.source.get_latest(10) :
 ${line}
--- a/sites/irclogs.qmsk.net/templates/index.tmpl	Sun Feb 08 00:29:36 2009 +0200
+++ b/sites/irclogs.qmsk.net/templates/index.tmpl	Sun Feb 08 02:29:23 2009 +0200
@@ -1,2 +1,9 @@
-Index page template
+<%inherit file="layout.tmpl" />
 
+<h1>Available Channels</h1>
+<ul>
+% for channel in channel_list :
+    <li><a href="${urls.channel_view.build(req, channel=channel.id)}">${channel.title}</a></li>
+% endfor
+</ul>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sites/irclogs.qmsk.net/templates/layout.tmpl	Sun Feb 08 02:29:23 2009 +0200
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+
+<%def name="menu()">
+
+</%def>
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+    <head>
+        <title>irclogs.qmsk.net ${('::' + channel.title) if channel else ''}</title>
+        <link rel="Stylesheet" type="text/css" href="${req.site_root}/static/irclogs.css" />
+    </head>
+    <body>
+        <div id="menu">
+            ${self.menu()}
+        </div>
+
+        <div id="content">
+            ${next.body()}
+        </div>
+
+        <div id="footer">
+            <div id="footer-left">
+            </div>
+            
+            <div id="footer-center">
+                ${h.validation_notice(req.site_host)}
+            </div>
+        </div>
+    </body>
+</html>
+
--- a/sites/irclogs.qmsk.net/urls.py	Sun Feb 08 00:29:36 2009 +0200
+++ b/sites/irclogs.qmsk.net/urls.py	Sun Feb 08 02:29:23 2009 +0200
@@ -26,12 +26,13 @@
 
 # urls
 index           = url('/',                                                              handlers.index                  )
+channel_select  = url('/channel_select/?channel:cid',                                   handlers.channel_select         )
 channel_view    = url('/channels/{channel:cid}',                                        handlers.channel_view           )
 channel_last    = url('/channels/{channel:cid}/last/{count:int=100}/{format=html}',     handlers.channel_last           )
 channel_search  = url('/channels/{channel:cid}/search',                                 handlers.channel_search         )
 
 # mapper
 mapper = URLTree(
-    [index, channel_view, channel_last, channel_search]
+    [index, channel_select, channel_view, channel_last, channel_search]
 )
 
--- a/sites/irclogs.qmsk.net/urltree.py	Sun Feb 08 00:29:36 2009 +0200
+++ b/sites/irclogs.qmsk.net/urltree.py	Sun Feb 08 02:29:23 2009 +0200
@@ -93,6 +93,13 @@
         """
 
         abstract
+    
+    def build (self, value_dict) :
+        """
+            Return a string representing this label, using the values in the given value_dict if needed
+        """
+
+        abstract
 
 class EmptyLabel (Label) :
     """
@@ -118,6 +125,9 @@
         # only empty segments
         if value == '' :
             return True
+    
+    def build (self, values) :
+        return str(self)
 
     def __str__ (self) :
         return ''
@@ -156,6 +166,9 @@
         if value == self.name :
             return True
 
+    def build (self, values) :
+        return str(self)
+
     def __str__ (self) :
         return self.name
 
@@ -180,6 +193,21 @@
         """
 
         return isinstance(other, ValueLabel) and self.key == other.key
+    
+    def build (self, values) :
+        """
+            Return either the assigned value from values, our default value, or raise an error
+        """
+
+        value = values.get(self.key)
+        
+        if not value and self.default :
+            value = self.default
+
+        elif not value :
+            raise URLError("No value given for label %r" % (self.key, ))
+
+        return value
 
 class SimpleValueLabel (ValueLabel) :
     """
@@ -273,9 +301,43 @@
         self.handler = handler
         self.defaults = defaults
 
-        # build our labels
+        # query string
+        self.query_args = dict()
+        
+        # parse any query string
+        # XXX: conflicts with regexp syntax
+        if '/?' in url_mask :
+            url_mask, query_mask = url_mask.split('/?')
+        
+        else :
+            query_mask = None
+
+        # build our label path
         self.label_path = [Label.parse(mask, defaults, config.type_dict) for mask in url_mask.split('/')]
-        
+
+        # build our query args list
+        if query_mask :
+            # split into items
+            for query_item in query_mask.split('&') :
+                # parse default
+                if '=' in query_item :
+                    query_item, default = query_item.split('=')
+
+                else :
+                    default = None
+                
+                # parse type
+                if ':' in query_item :
+                    query_item, type = query_item.split(':')
+                else :
+                    type = None
+                
+                # parse key
+                key = query_item
+
+                # add to query_args as (type, default) tuple
+                self.query_args[key] = (self.config.type_dict[type], default)
+         
     def get_label_path (self) :
         """
             Returns a list containing the labels in this url
@@ -295,9 +357,46 @@
         # then add all the values
         for label_value in label_values :
             kwargs[label_value.label.key] = label_value.value
+       
+        # then parse all query args
+        # XXX: catch missing arguments
+        for key, value in request.get_args() :
+            # lookup spec
+            type, default = self.query_args[key]
+
+            # normalize empty value to None
+            if not value :
+                value = None
+
+            else :
+                # process value
+                value = type(value)
+
+            # set default?
+            if not value :
+                if default :
+                    value = default
+
+                if default == '' :
+                    # do not pass key at all
+                    continue
+
+                # otherwise, fail
+                raise URLError("No value given for required argument: %r" % (key, ))
             
+            # set key
+            kwargs[key] = value
+
         # execute the handler
         return self.handler(request, **kwargs)
+    
+    def build (self, request, **values) :
+        """
+            Build an absolute URL pointing to this target, with the given values
+        """
+
+        # build URL from request page prefix and our labels
+        return request.page_prefix + '/'.join(label.build(values) for label in self.label_path)
 
     def __str__ (self) :
         return '/'.join(str(label) for label in self.label_path)
--- a/sites/www.qmsk.net/page.py	Sun Feb 08 00:29:36 2009 +0200
+++ b/sites/www.qmsk.net/page.py	Sun Feb 08 02:29:23 2009 +0200
@@ -99,10 +99,8 @@
         """
 
         # render the template
-        response_data = template.render(self.fs.template,
-            request         = request,
-            site_root_url   = request.get_script_dir(),
-            site_page_url   = request.get_page_prefix(),
+        response_data = template.TemplateLoader.render_template(self.fs.template,
+            req             = request,
             page            = self,
             menu            = menu.Menu(self.fs, self),
         )
--- a/sites/www.qmsk.net/templates/layout.tmpl	Sun Feb 08 00:29:36 2009 +0200
+++ b/sites/www.qmsk.net/templates/layout.tmpl	Sun Feb 08 02:29:23 2009 +0200
@@ -4,7 +4,7 @@
 <ul>
 % for pi in items :
     <li>
-        <a href="${site_page_url}/${pi.url}"${' class="selected-page"' if pi == open_page else ''}>${pi.title} ${'&raquo;' if pi.children and pi.parent else ''}</a>
+        <a href="${req.page_prefix}/${pi.url}"${' class="selected-page"' if pi == open_page else ''}>${pi.title} ${'&raquo;' if pi.children and pi.parent else ''}</a>
     % if pi in ancestry and pi.children and pi.parent :
         ${render_menu(page, pi, pi.children, ancestry)}
     % endif
@@ -16,11 +16,11 @@
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
     <head>
         <title>qmsk.net ${' :: ' + h.breadcrumb(menu.ancestry, links=False) if menu.ancestry else ''}</title>
-        <link rel="Stylesheet" type="text/css" href="${site_root_url}/static/style.css" />
+        <link rel="Stylesheet" type="text/css" href="${req.site_root}/static/style.css" />
     </head>
     <body>
             <div id="header">
-                <a href="${site_page_url}/">QMSK.NET</a>
+                <a href="${req.page_prefix}/">QMSK.NET</a>
             </div>
             
             <div id="container">
@@ -48,7 +48,7 @@
                 </div>
                 
                 <div id="footer-center">
-                    Validated <a href="http://validator.w3.org/check?uri=www.qmsk.net">XHTML 1.0 Strict</a> &amp; <a href="http://jigsaw.w3.org/css-validator/validator?uri=www.qmsk.net">CSS 2.1</a>
+                    ${h.validation_notice(req.site_host)}
                 </div>
             </div>
     </body>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/static/irclogs.css	Sun Feb 08 02:29:23 2009 +0200
@@ -0,0 +1,141 @@
+/*
+ * Global styles
+ */
+body {
+    padding: 0px;
+    margin: 0px;
+
+    background-color: #ffffff;
+    color: #000000;
+}
+
+/*
+ * Menu
+ */
+#menu {
+    padding: 0px;
+    margin: 0px;
+}
+
+#menu ul {
+    display: table;
+    list-style-type: none;
+
+    width: 100%;
+
+    padding: 0px;
+    margin: 0px;
+    
+    background-color: #f0f0f0;
+    border-bottom: 1px dashed #a5a5a5;
+
+    text-align: center;
+    white-space: nowrap;
+}
+
+#menu li {
+    display: table-cell;
+
+    height: 1.5em;
+
+    padding: 0px;
+    margin: 0px;
+
+    border-left: 1px solid #b0b0b0;
+
+    font-weight: bold;
+}
+
+#menu li a,
+#menu li form {
+    height: 1.5em;
+}
+
+#menu li:first {
+    border-left: none;
+}
+
+#menu li a {
+    display: block;
+
+    padding: 6px;
+    margin: 0px;
+
+    height: 100%;
+
+    color: #494949;
+    text-decoration: none;
+}
+
+#menu li a:hover {
+    background-color: #d0d0d0;
+    text-decoration: none;
+}
+
+#menu form {
+    display: block;
+
+    padding: 0px;
+    margin: 0px;
+}
+
+#menu form select,
+#menu form input {
+    padding: 9px;
+    margin: 0px;
+
+    border: none;
+    background-color: inherit;
+}
+
+#menu form input {
+    cursor: pointer;
+}
+
+#menu form select:hover,
+#menu form input:hover {
+    background-color: #d0d0d0;
+}
+
+#menu form option {
+    background-color: auto;
+}
+
+/*
+ * Content
+ */
+div#content {
+    padding: 25px;
+}
+
+/*
+ * Footer
+ */
+div#footer {
+    /* force to bottom of page */
+    position: absolute;
+    bottom: 0;
+    
+    width: 100%;
+    padding: 10px 0px 10px;
+    
+    border-top: 1px dashed #a5a5a5;
+
+    font-size: x-small;
+    font-style: italic;
+}
+
+div#footer-left {
+    float: left;
+}
+
+div#footer-right {
+    float: right;
+    text-align: right;
+}
+
+div#footer-center {
+    text-align: center;
+}
+
+