--- 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> & <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 »" />
+ </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 »" />
+ </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} » 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} ${'»' 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} ${'»' 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> & <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;
+}
+
+