[workshop] rantelope: day 002
Michal Wallace
workshop@cornerhost.com
Tue, 24 Sep 2002 22:56:09 -0400 (Eastern Daylight Time)
rantelope: day 002
------------------
Today I added an XSLT template system to Rantelope.
It wasn't terribly complicated. Most of the work
was just brushing up on my XSLT to get a default
template working.
The new code and live demo are up on:
http://www.rantelope.com/
==[ today's objective ]==
Since we already had an RSS feed, I thought it would
make sense to generate HTML by transforming the RSS
with XSLT.
I could have used my own template language (Zebra),
but I want Rantelope to be built on standards, not
my personal whims. Zebra has hardly any documentation,
whereas there are plenty of great resources for
learning XSLT.
Using XSLT also means we can potentially reuse the
templates for RSS feeds generated by *any* blogging
tool, which is a nice plus.
==[ implementation ]==
Running an XSLT transformation in python is easy,
thanks to the 4Suite library:
http://4suite.org/index.xhtml
I went ahead and installed 4suite on all cornerhost
machines in case anyone wants to play around with it.
I wasn't sure how to use it, but a quick google
search saved the day. Uche Ogbuji, the principal
author of 4Suite, has a tutorial for working with
XSLT here:
http://uche.ogbuji.net:8080/uche.ogbuji.net/tech/akara/pyxml/python-xslt/
Once I read that, invoking a tranformation was easy.
If you're dealing with xml stored in python strings,
it boils down to something like this:
def transform(xml, xsl):
"""
A simple wrapper for 4XSLT.
"""
from Ft.Xml.Xslt.Processor import Processor
from Ft.Xml.InputSource import DefaultFactory
proc = Processor()
## @TODO: these fromString() calls should
## really include a URI...
xslObj = DefaultFactory.fromString(xsl)
proc.appendStylesheet(xslObj)
xmlObj = DefaultFactory.fromString(xml)
return proc.run(xmlObj)
Well, an RSS feed looks something like this:
<?xml version="1.0"?>
<rss version="2.0" xmlns="http://backend.userland.com/rss2">
<channel>
<title>my channel</title>
<link>http://rantelope.com/</link>
<description>my first channel.</description>
<item>
<title>first post!</title>
<link>http://rantelope.com/</link>
<description>rantelope rules!</description>
</item>
</channel>
</rss>
I was missing the <channel> tag yesterday, but once I fixed
that, it just took a little reading to put together a default
XSLT template. It looked something like this:
plainXSLT =\
'''\
<xsl:stylesheet version="1.0"
xmlns:rss="http://backend.userland.com/rss2"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="rss:channel">
<html>
<head>
<title><xsl:value-of select="rss:title"/></title>
</head>
<body>
<h1><xsl:value-of select="rss:title"/></h1>
<p><i><xsl:value-of select="rss:description"/></i></p>
<xsl:for-each select="rss:item">
<div class="post">
<h2><xsl:value-of select="rss:title"/></h2>
<xsl:value-of select="rss:description"/>
</div>
</xsl:for-each>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
'''
Making that the default involved several steps.
First, I had to add a .template field to the Channel
class. It uses the "default" option of the
strongbox.attr() type:
class Channel(Strongbox):
# ...
template = attr(str, default=plainXSLT)
Second, I had to add a "template" field to my table
in MySQL.
Finally, I updated all the existing records to use the
new template:
for c in CLERK.match(Channel):
c.template = plainXSLT
c.writeFiles()
CLERK.store(c)
The writeFiles() function is also new. Now that I want
to write HTML as well as RSS, it made sense to refactor
things a bit. So we now have:
class Channel(Strongbox):
#...
def toRSS(self):
return zebra.fetch("rss", BoxView(self))
def toHTML(self, input=None):
rss = input or self.toRSS()
return transform(rss, self.template)
def writeFiles(self):
rss = self.toRSS()
print >> open(self.path + self.rssfile, "w"), rss
if self.template:
print >> open(self.path + self.htmlfile, "w"), self.toHTML(rss)
I did make one other change: I wanted to keep people from
changing the output filename to something like "rantelope.app",
which could possibly overwrite the actual rantelope application -
a HUGE security flaw! :)
So I hard-coded the output directory to "./out/" and
added a validation rule to the .rssfile and .htmlfile
attributes:
class Channel(Strongbox):
#...
rssfile = attr(str, okay=lambda x: "/" not in x and x.endswith(".rss"))
htmlfile = attr(str, okay=lambda x: "/" not in x and x.endswith(".html"))
The "okay" option also accepts regular expressions, so I might
use those in the future instead of these long, ugly lambdas.
(Lambdas are a way of writing really short python functions
on the fly.)
Now if you try and input an invalid filename, you'll get a nasty
error screen. Eventually, it'll be a nicely formatted error
message, of course.
That's pretty much it for today. Tomorrow, I'm going to expand
rantelope from just a blogging tool to a true content management
system capable of managing an entire website.
-----
PS: I tried to make this one a little more "skimmable", and
not force people to go hunt down the code... Is this better?
Sincerely,
Michal J Wallace
Sabren Enterprises, Inc.
-------------------------------------
contact: michal@sabren.com
hosting: http://www.cornerhost.com/
my site: http://www.sabren.net/
--------------------------------------