Thu, 07 Dec 2006

What is PyBlosxom?

  • weblogging system written in Python
  • minimal core engine of about 3500 lines of code
  • blog posts are stored as individual files in a category oriented hierarchy of directories in a filesystem
  • no database is required - anything that can produce text files can be your source of blog entries
  • much of the functionality of pyblosxom is delivered via a plug-in architecture

[23:59] | [/1-OVERVIEW] | #-permalink-#

About PyBlosxom

Originally started as a Python clone of Blosxom but has since evolved into a beast of its own. Pyblosxom focuses on three things:

  • Simplicity - uses the file system for all of its data storage
  • Extensibility - most of the functionality is derived from its plugin framework
  • Community - several hundred PyBlosxom users who have different needs, and work in different computing environments
  • Hackable - easy to write plug-ins and tweak functionality

PyBlosxom got its name and inspiration from Rael Dornfest's Blosxom, which is written in Perl. In fact, there are a number of other Blosxom like packages written in various languages. All of the members of this family of blogging packages share the filesystem model and a plugin architecture of some sort.

[23:58] | [/1-OVERVIEW] | #-permalink-#

URI Representation

http://www.sauria.com/blog/2005/03/09/a-post

You can also use the hierarchical categories to provide the path to a posting:

http://www.sauria.com/blog/computers/internet/weblogs/pyblosxom/a-post

On my blog, both of these URIs refer to the same weblog posting. You can also look at groups of entries by removing information at the end of the URI:

http://www.sauria.com/blog/                                         
    shows the root of my blog
http://www.sauria.com/blog/2005/03/09/                              
    shows all posts dated on the 9th of March in 2005
http://www.sauria.com/blog/2005/03/
    shows all posts in March in 2005
http://www.sauria.com/blog/2005/
    shows all posts in 2005
http://www.sauria.com/blog/computers/internet/weblogs/pyblosxom/    
    shows all posts in the computers/internet/weblogs/pyblosxom category
http://www.sauria.com/blog/computers/internet/                      
    shows all posts in the computers/internet category

[23:57] | [/1-OVERVIEW] | #-permalink-#

Flavours

A flavour can be thought of as a theme or a type of output.

Another characteristic of Blosxom derived systems is the use of flavours. By appending a ?flav=<label> to the end of any PyBlosxom URI, you can specify what output format (aka flavour) you would like. The weblog world is full of potential output formats: HTML, XHTML, RSS (several different varieties), Atom, etc. The flavour system allows a PyBlosxom user to define a new flavour in order to produce output in one or more of these formats. Additionally, flavours can be theme-oriented: sunshine, rainy-day, purple-pie-man, etc.

A flavour consists of a series of templates which are assembled to form the completed output. Using this template system allows PyBlosxom users to use flavour templates built by users of other *osxom systems with only a few modifications.

[23:56] | [/1-OVERVIEW] | #-permalink-#

Plugins

The plugin system allows users to add functionality beyond what comes with PyBlosxom by default. PyBlosxom plugins have access to the information that the core engine has. They can read and introduce variables that appear in PyBlosxom templates, they can inject new entries into the rendering pipeline, they can render output, and they can even work with the HTTP headers that accompanied the request for a weblog page.

Here some examples of functionality that is provided via plugins:

  • archives - create an access to archives, one link per month
  • calendar nav - use a calendar as a way to navigate through archives
  • xmlrpc - metaweblog - implement the Metaweblog XML-RPC API to allow fancy posting/editing UI
  • http 304 handling - implement conditional HTTP handling to save bandwidth
  • autoping - ping all referenced urls that have trackback or pingback URI's
  • logging - log HTTP information to a file
  • lucene - use Lucene to provide full text search
  • handle text formatting of blog posts formatted using text+html, textile, or ReST

[23:55] | [/1-OVERVIEW] | #-permalink-#

Limitations

PyBlosxom is a small project. As such, we have no customer support hotline, our documentation will be eternally half-done, and it's likely you will run into problems eventually unless you want a bare-bones blog.

PyBlosxom is a CGI program at its heart. While there have been changes to make it work with WSGI and mod_python and other web-servers, it's still architected as a CGI program. Using it in other ways may cause headaches.

If this sort of thing infuriates you, we don't think you should use PyBlosxom.

PyBlosxom is not all things to all people. We have a framework that allows for plugins to be built, but if you're not into building plugins, then it's possible that PyBlosxom may not fit your needs.

[23:54] | [/1-OVERVIEW] | #-permalink-#

Pre-requisites for installing PyBlosxom

You will need:

  • a web-server that runs CGI
  • Python version 2.2 or later
  • a text editor (notepad, vi, emacs, nano, ...)

[23:49] | [/2-INSTALLATION-and-CONFIG] | #-permalink-#

Installation

These were the steps to setup the blog for this presentation on my Mac OSX 10.4 Notebook.

1. install pyblosxom python package after extracting the archive

$ sudo /usr/local/bin/python2.4 setup.py install --prefix=/usr/local 

2. install pyblosxom contrib (contributed plugins) after 
   extracting the archive

$ sudo cp -R contrib.1.3.0 /usr/local/lib/python2.4/site-packages/

3. create the necessary directories for the web site 

$ cd /Library/WebServer/
$ sudo  mkdir michipug-pyblosxom-Dec-2006
$ chown xxx   michipug-pyblosxom-Dec-2006
$ mkdir cgi-bin
$ mkdir blog
$ mkdir blog/entries
$ mkdir blog/flavours
$ mkdir blog/flavours/html.flav
$ mkdir blog/logs
$ mkdir static
$ mkdir css
$ mkdir graphics

4. copy the cgi and config file

$ cd path/to/pyblosxom_build_dirs/web
$ cp pyblosxom.cgi /Library/WebServer/michipug-pyblosxom-Dec-2006/cgi-bin/
$ cp config.py     /Library/WebServer/michipug-pyblosxom-Dec-2006/cgi-bin/
$ vi pyblosxom.cgi to change to #!/usr/local/bin/python2.4 

[23:48] | [/2-INSTALLATION-and-CONFIG] | #-permalink-#

Configuration

Configure the following in your apache configuration file(s) or in a .htaccess file.

1. configured apache with Listen,VirtualHost,Directory,Files 
   directives

# *****************************************************************
# *****************************************************************
# * for MichiPUG pyblosxom presentation Dec 2006
# *****************************************************************

Listen 9000
    
<VirtualHost *:9000>
    ServerAdmin sample@example.invalid
    DocumentRoot /Library/WebServer/michipug-pyblosxom-Dec-2006/
    ServerName 127.0.0.1   
    ErrorLog "/private/var/log/httpd/port-9000-error_log"
    AddHandler cgi-script .cgi
    ScriptAlias /cgi-bin/ /Library/WebServer/michipug-pyblosxom-Dec-2006/cgi-bin/
    DirectoryIndex cgi-bin/pyblosxom.cgi
    RewriteEngine On
    RewriteRule blog$              /cgi-bin/pyblosxom.cgi/                [last]
    RewriteRule blog(/.*)?$        /cgi-bin/pyblosxom.cgi$1               [last]
</VirtualHost>
    
<Directory "/Library/WebServer/michipug-pyblosxom-Dec-2006/">
    Options Indexes
    AllowOverride None
    Order allow,deny
    Allow from all 
</Directory>

<Directory "/Library/WebServer/michipug-pyblosxom-Dec-2006/cgi-bin/">
    Options ExecCGI
    AllowOverride None
    Order allow,deny
    Allow from all
</Directory>

<Files config.py> 
deny from all 
</Files>

# *****************************************************************
# * for MichiPUG pyblosxom presentation Dec 2006
# *****************************************************************
# *****************************************************************

3. configured pyblosxom by modifying the config.py file

$ diff config.py.orig config.py
12c12
< py['blog_title'] = "Another pyblosxom blog"
---
> py['blog_title'] = "MichiPUG PyBlosxom Presentation blog"
18c18
< py['blog_author'] = "name"
---
> py['blog_author'] = "Jim McDonald"
21c21
< py["blog_email"] = "email@blah.com"
---
> py["blog_email"] = "jimc_mcdonald@pobox.com"
33c33
< py["blog_rights"] = "Copyright 2005 Joe Bobb"
---
> py["blog_rights"] = "Copyright 2006 Jim McDonald"
36c36
< py['datadir'] = "/path/to/blog"
---
> py['datadir'] = "/Library/WebServer/michipug-pyblosxom-Dec-2006/blog/entries"
71a72
> py['flavourdir'] = "/Library/WebServer/michipug-pyblosxom-Dec-2006/blog/flavours"
79c80
< # py['base_url'] = "http://www.some.host/weblog"
---
> py['base_url'] = "http://127.0.0.1:9000/blog"
99a101
> py['plugin_dirs'] = ['/usr/local/lib/python2.4/site-packages/contrib.1.3.0/plugins/']
110a113
> py['load_plugins'] = ["pyfilenamemtime","pycalendar", "pycategories", "pyarchives"]
118a122
> py["static_dir"] = "/Library/WebServer/michipug-pyblosxom-Dec-2006/static"

3. copied the custom html flavour to ./blog/ and modified it to include the correct
   setting for the stylesheet 

4. copied the CSS stylesheet to ./css/ 

 

The PyBlosxom Configuration File: config.py

PyBlosxom's configuration file is called config.py, and basically contains a dict named py, whose entries are the configuration variables for PyBlosxom. This dict ends up getting passed all the way through the core processing engine. Some of the important values that you need to set are

py['datadir'] the directory of the root of the PyBlosxom data, which will contain all the blog entries.

py['plugin_dirs'] a list of directories containing the plugins that you wish to use.

py['load_plugins'] a list containing the names of the plugins that you wish to use.

In order to use plugins you need to correctly set py['plugin_dirs'] and place the corresponding entry in py['load_plugins']

You will need to be careful about operating system permissions between the web server user and the user PyBlosxom is running as. This will affect comment files and logs especially.

After these steps are complete the blog is functional.

Note: Normally you'd place your datadir blog entries outside of the root document directory of your web site!

[23:47] | [/2-INSTALLATION-and-CONFIG] | #-permalink-#

Test the cgi script

Run the pyblosxom.cgi cgi script from the commandline to verify the configuration.

$ ./pyblosxom.cgi 
Welcome to PyBlosxom's installation verification system.
------
]] printing diagnostics [[
pyblosxom:   1.3.2 2/13/2006
sys.version: 2.4.4 (#1, Oct 20 2006, 14:37:05)  
             [GCC 4.0.1 (Apple Computer, Inc. build 5363)]
os.name:     posix
codebase:    --default--
------
]] checking config file [[
config has 19 properties set.

   missing optional property: 'renderer'
   missing optional property: 'cacheDriver'
   missing optional property: 'cacheConfig'

Refer to the documentation for what properties are available
and what they do.
PASS: config file is fine.
------
]] checking datadir [[
PASS: datadir is there.  Note: this does not check whether
      your webserver has permissions to view files therein!
------
Now we're going to verify your plugin configuration.
=== plugin: 'pycalendar'
    file: /usr/local/lib/python2.4/site-packages/contrib.1.3.0/plugins/pycalendar.py
    version: $Id: pycalendar.py 883 2006-03-24 03:43:42Z willhelm $
    PASS
=== plugin: 'pycategories'
    file: /usr/local/lib/python2.4/site-packages/contrib.1.3.0/plugins/pycategories.py
    version: $Id: pycategories.py 883 2006-03-24 03:43:42Z willhelm $
missing optional config property 'category_template' which allows 
you to specify how the category hierarchy is rendered.  see
the documentation at the top of the pycategories plugin code 
file for more details.
    PASS
=== plugin: 'pyarchives'
    file: /usr/local/lib/python2.4/site-packages/contrib.1.3.0/plugins/pyarchives.py
    version: $Id: pyarchives.py 883 2006-03-24 03:43:42Z willhelm $
missing optional config property 'archive_template' which 
allows you to specify how the archive links are created.  
refer to pyarchive plugin documentation for more details.
    PASS

The following plugins do not support installation verification:
   'pyfilenamemtime' 
(/usr/local/lib/python2.4/site-packages/contrib.1.3.0/plugins/pyfilenamemtime.py)

[23:46] | [/2-INSTALLATION-and-CONFIG] | #-permalink-#

Flavours

PyBlosxom takes the data provided in the entries and by the plugins and transforms it into output using renderers. Output can be in html, xhtml, xml, or anything else--anything that you could get back from a CGI script or web application.

The default renderer can be set in your config file like this:

py["renderer"] = "blosxom"

PyBlosxom comes with two renderers: blosxom and debug.

The debug renderer displays all the data in the various parts of the PyBlosxom Request object. This is really helpful to see what variables are at your disposal and also to debug problems you might be having with plugins you've installed.

The blosxom renderer renders entries just like Blosxom does.

If you want your blog rendered using a different template system--say Cheetah or htmltmpl--implement a renderer that renders the output. This can be done as a PyBlosxom plugin. See the chapter on writing plugins for more information.

Flavours and Templates

The blosxom renderer uses the same template style that Blosxom uses. As such, you can use most Blosxom flavour templates and only have to make some minor modifications.

A flavour can be thought of as a theme or an output format. For example, you could have an "html" flavour that renders the blog data in html format. You could have an "xhtml" flavour that renders the blog in a strict xhtml format. You could have a "happy-sunshine" flavour that renders the blog in html format using a happy sunshiney look and feel. You can have an "rss" flavour that renders the output in RSS 2.0 format with enclosures. So on and so forth.

A flavour consists of a series of templates each of which is a part of the page that finally gets rendered. The minimum set of templates are these:

  • content_type - holds the content type of the flavour

  • head - holds everything before all the entries

  • story - holds a single entry

  • foot - holds everything after all the entries

  • date_head - shows at the start of a date

  • date_foot - shows at the end of a date

You can have other templates as well. Many plugins require additional templates in order to work.

The template files for a given flavour all have the same file extension which is the flavour's name. For example, if you were using an "html" flavour, the flavour itself would be composed of the following files:

  • content_type.html

  • head.html

  • story.html

  • foot.html

  • date_head.html

  • date_foot.html

If you want to create a "joy" flavour, you would have the following files:

  • content_type.joy

  • head.joy

  • story.joy

  • foot.joy

  • date_head.joy

  • date_foot.joy

You can have as many flavours as you want in your blog.

PyBlosxom comes with a series of flavours: html, rss ("RSS 0.9.1), rss20 (RSS 2.0), and atom (Atom 1.0). These flavours come as part of PyBlosxom and they will work out of the box with no modifications and no configuration changes. Additionally, you can override all or portions of these flavours. We'll talk about this a little later.

Additionally, there is a flavour registry on the PyBlosxom web-site at http://pyblosxom.sourceforge.net/ . This is where you can submit flavours that you have created and see flavours other people have created and submitted.

Where To Put Your Flavour Files

If you want to override the existing flavours, add new flavours, or develop your own flavours, you should set the flavourdir property of your config.py file. I have this directory parallel to my datadir. In my flavourdir, I have flavour directories--one for each flavour in my blog:

/home
 |-- willg/
    |-- myblog/
       |-- entries/        <-- my datadir
       |  |-- content/        <-- category
       |  |-- dev/            <-- category
       |  |-- links/          <-- category
       |
       |-- flavours/       <-- my flavourdir
          |-- html.flav/      <-- defines the html flavour
          |-- xml.flav/       <-- defines the xml flavour
          |-- links/          <-- parallels the links category
             |-- html.flav/   <-- defines the html flavour for the links category


In my flavourdir, I have two flavour directories html.flav and xml.flav. The xml.flav is a copy of the atom.flav directory that comes with PyBlosxom. I copied it so that I could use "xml" for the flavour name. This isn't necessarily a wonderful idea, but it helped me upgrade my blog without disturbing planets and writing lots of .htaccess redirects and such.

You'll notice there's an html.flav directory in the links directory. When someone is looking at items in the links directory, then PyBlosxom will use this html flavour.

The order of overiding works like this:

  1. PyBlosxom looks for flavour files that came with PyBlosxom

  2. PyBlosxom starts at the root of the flavourdir and looks for flavour files there. If there are some, then these files override the files PyBlosxom has found so far.

  3. PyBlosxom iterates through category directories in the flavourdir if there are any that are parallel to the datadir and looks for flavour directories there. If there are some, then those files override the files it has so far.

This allows you to easily override specific templates in your blog (like the header or footer) depending on what category the user is looking at.

[23:39] | [/3-USING-PYBLOSXOM] | #-permalink-#

Template Variables

This is the list of variables that are available to your templates. Additionally, plugins that you are using will add additional variables. To get a complete list of what variables are available in your blog, use the debug renderer by changing the renderer property in your config.py file to debug like this:

py["renderer"] = "debug"

That will tell you all kinds of stuff about the data structures involved in the request. Don't forget to change it back when you're done!

URL Encoding and Escaping of Template Variables

PyBlosxom 1.3 allows you to escape and URL encode any variables by adding "_escaped" or "_urlencoded" to the end of the variable name.

For example, title_escaped is an escaped form of the title with ' (single-quote) replaced with ' and " (double-quote) replaced with ".

title_urlencoded is a URL encoded form of the title which uses the Python urllib.

Variables From config.py

These template variables are available to all templates. They come directly from your config.py file.

Template variables from config.py

variable name description example
blog_description the description of the blog blosxom with a touch of python
blog_title the title of the blog RoughingIT - pyblosxom : /weblogs/tools/pyblosxom
blog_language the language of the blog en
blog_encoding the encoding of the blog iso8859-1
blog_author the author of the blog (hopefully you) Joe Dirt
blog_email the email address of the author of the blog (feel free to obfuscate it) joe at joe dot com

Additionally, any other properties you set in config.py are available in your templates. If you wanted to create a blog_images variable holding the base url of the directory with all your images:

Example 4-2. creating your own config.py variables

py["blog_images"] = "http://www.joe.com/~joe/images/"

to your config.py file and it would be available in all your templates.

Calculated Template Variables

These template variables are available to all templates as well. They are calculated based on the request.

Table - Calculated template variables

variable name description example
content_type the content type of this page text/html
root_datadir the root datadir of this page? /home/subtle/blosxom/weblogs/tools/pyblosxom
url the PATH_INFO to this page pyblosxom/weblogs/tools/pyblosxom
flavour the flavour for this page html
latest_date the date of the most recent entry that is going to show Tue, 15 Nov 2005
latest_w3cdate the date of the most recent entry that is going to show in w3cdate format 2005-11-13T17:50:02Z
latest_rfc822date the date of the most recent entry that is going to show in RFC 822 format Sun, 13 Nov 2005 17:50 GMT
pi_yr the four-digit year the user requested 2002
pi_mo the month name the user requested Sep
pi_da the day number the user requested 15
pi_bl the entry the user requested to see weblogs/tools/pyblosxom
pyblosxom_version the version number and release date of the pyblosxom version you're using 1.2 3/25/2005

Template Variables Only Available in the story Template

These template variables are only available in your story template.

Table - Variables in the story template

variable name description example
title title of the entry First Post!
filename template file name and path of the entry /home/subtle/blosxom/weblogs/tools/pyblosxom/index.txt
file_path just the filename and extensions of the entry index.txt
fn just the filename (no extension) index
absolute_path the category/path of the entry weblogs/tools/pyblosxom
body the text of the entry first post!
tb_id the trackback id of the entry _index
path the category/path of the entry weblogs/tools/pyblosxom
yr the four digit year of the mtime of this entry 2004
mo the month abbreviation of the mtime of this entry Jan
mo_num the month number of the mtime of this entry 01
ti the hour and minute of the mtime of this entry 16:40
date the date string of the mtime of this entry Sun, 23 May 2004
w3cdate the date in w3cdate format 2005-11-13T17:50:02Z
rfc822date the date in RFC 822 format Sun, 13 Nov 2005 17:50 GMT
fulltime the date in YYYYMMDDHHMMSS format 20040523164000
timetuple the time tuple (year, month, month-day, hour, minute, second, week-day, year-day, isdst) (2004, 5, 23, 16, 40, 0, 6, 144, 1)
mtime seconds since the epoch 1085348400.0
dw the day of the week Sunday
da the day of the month 23

Template Variables From Plugins

Additionally, many plugins will create additional variables. Refer to the plugin's documentation for what variables it creates, where the variables are available, and what the variables do.

[23:38] | [/3-USING-PYBLOSXOM] | #-permalink-#

Invoking a Flavour

The flavour for a given page is specified in the extension of the file being requested. For example:

Table - Examples of request URIs

url what happens
http://some.blog.org/ brings up the index in the default flavour which is "html"
http://some.blog.org/index.html brings up the index in the "html" flavour
http://some.blog.org/index.rss brings up the index in the "rss" flavour (which by default is RSS 0.9.1)
http://some.blog.org/2004/05/index.joy brings up the index for May of 2004 in the "joy" flavour

Additionally, you can specify the flavour by adding a flav variable in the query-string. Examples:

Table - Specifying flavour using the querystring

uri what happens
http://some.blog.org/ brings up the index in the default flavour which is "html"
http://some.blog.org/?flav=rss brings up the index in the "rss" flavour
http://some.blog.org/2004/05/index?flav=joy brings up the index for May of 2004 in the "joy" flavour

You can change the default flavour from html to some other flavour in your config.py file with the default_flavour property:

Example - default_flavour variable value

py["default_flavour"] = "joy"

Doing this will set the default flavour to use when the URI the user has used doesn't specify which flavour to use. For example, if you do the above, then the following URIs will use the default flavour:

Table - Default flavour used

uri what happens
http://www.joe.com/cgi-bin/pyblosxom.cgi/2005/03 uses the default flavour which is set to "joy"
http://www.joe.com/cgi-bin/pyblosxom.cgi/2005/03/?flav=html uses the html flavour as specified by flav=

Example Flavours

  • Show gray flavour
  • Show green flavour

[23:37] | [/3-USING-PYBLOSXOM] | #-permalink-#

An Example Flavour (from this presentation)

content_type.html

text/html

head.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <meta name="generator" content="pyblosxom" />
        <title>$blog_title</title>

<link rel="stylesheet" 
      href="http://127.0.0.1:9000/css/table-pyblosxom-styles.css" 
      type="text/css" />
<link rel="alternate" type="application/rss+xml" 
      title="MichiPUG PyBlosxom Presentation Blog" href="$base_url/?flav=rss" />
</head>

<body>

<table id="main">
<tr>
<td>

<table id="banner">
<tr>
<td>
<h1><a class="title" href="$url">$blog_title</a></h1>
<h2>-- $blog_description -- </h2>
</td>
</tr>
</table>

<table id="middle-content">
<tr>
<td id="left" class="sidebar" >
<h2>Starting Points</h2>
<a href="$base_url">this blog</a>
<br />
<br />

<h2>Blog Categories</h2>
$categorylinks
<br />
<br />

<h2>Other Feeds</h2>
<ul>
   <li>
    <a href="$base_url/?flav=rss">RSS</a> 
   </li>
</ul>
<p> </p>
<ul>
   <li> 
     <img src="http://www.w3.org/Icons/valid-xhtml10" 
          alt="Valid XHTML 1.0!" height="31" width="88" />
   </li>
   <li>
    <a href="http://pyblosxom.sourceforge.net/">
     <img src="http://127.0.0.1:9000/graphics/pb_pyblosxom.gif" 
          border="0" alt="pyblosxom" />
    </a>
   </li>
<!--   
   <li> 
    <a href="http://validator.w3.org/check/referer">
     <img src="http://www.w3.org/Icons/valid-xhtml10" 
     alt="Valid XHTML 1.0!" height="31" width="88" />
    </a>
   </li>
   <li>
    <a href="http://jigsaw.w3.org/css-validator/">
    <img style="border:0;width:88px;height:31px"
     src="http://jigsaw.w3.org/css-validator/images/vcss" alt="Valid CSS!" />
    </a>
   </li>
-->
</ul>
</td> <!-- end of the left sidebar column -->

<td id="center">
<div class="content">

story.html

<div class="story">
<a name="$fn"></a>
<h3>$title</h3>
$body
<p class="posted">
[<a href="$base_url/$yr/$mo/$da#$fn" title="Date link">$ti</a>] | 
[<a href="$base_url/$path" title="Category">$path</a>] |
 <a href="$base_url/$file_path.html" title="Permalink">#-permalink-#</a>  
</p>
</div>

foot.html

</div>

</td> <!-- end of the center column -->

<td id="right">
<div class="sidebar">

<h2>Archives</h2>
$archivelinks
<br />

<h2>Calendar</h2>
$calendar
</div>
</td> <!-- end of the right sidebar column -->

</tr> <!-- end of the 3 column content row column -->

</table>

<table id="footer">
<tr>
<td>
<div class="footer">
<h2>$blog_title <$url></h2>
</div>
</td>
</tr>
</table>

</td>
</tr>
</table> 

</body>
</html>

[23:36] | [/3-USING-PYBLOSXOM] | #-permalink-#

Writing Entries [Metadata-mood=good]

The Format of an Entry

PyBlosxom entries consist of three parts: the title, the metadata, and then the body of the entry. The first line is title of the entry. Then comes the metadata of the entry (if any). After the metadata comes the body of the entry.

The title consists of a single line of plain text. You can have whatever characters you like in the title of your entry. The title doesn't have to be the same as the entry file name.

The metadata section is between the title line and the body of the entry. It consists of a series of lines that start with the hash mark (#), then a metadata variable name, then the metadata variable value.

The body of the entry is written in HTML and comprises the rest of the entry file.

Here's an example first post entry:

Example - first post

This is my first post!

<p>
  This is the body of the first post to my blog.
</p>

Here's a more complex example:

Example - more complex first post

The rain in Spain....
<p>
  The rain
</p>
<p align="center">

  in Spain
</p>
<p align="right">
  is <font color="ff0000">mainly</font> on the plain.
</p>

Here's an example of a post with metadata:

Example - first post with metadata

The rain in Spain....
#mood bored
#music The Doors - Greatest Hits Vol 1

<p>
  The rain
</p>
<p align="center">
  in Spain
</p>
<p align="right">
  is <font color="ff0000">mainly</font> on the plain.

</p>

The metadata variables here would be available in your story template. So for the above example, the template variable $mood would be filled in with bored and $music would be filled in with The Doors - Greatest Hits Vol 1.

[23:35] | [/3-USING-PYBLOSXOM] | #-permalink-#

Categories (and Sub-Categories)

Writing entries in PyBlosxom is fairly straightforward. Each entry is a single text file located somewhere in the directory tree of your datadir. The directory that the entry is in is the category the entry is "filed under". For example, if my datadir was /home/joe/myblog/entries and I stored an entry named firstpost.txt in /home/joe/myblog/entries/status then the category for my entry would be /status.

Be careful when you create your categories--be sure to use characters that are appropriate in directory names in the file system.

Don't worry about making sure you have all the categories you need up front--you can add them as you need them.

Sub-categories are simply directories under another directory.

[23:34] | [/3-USING-PYBLOSXOM] | #-permalink-#

Rendering & Entry Parsers

PyBlosxom supports only one format for entry files by default. This format is the same format that blosxom uses. The extension for this format is .txt. The first line of the file is in plain text and forms the title of the entry. The second line through the end of the file is in HTML and is the body of the entry.

A sample blog entry could look like this:

First post
<p>
  Here's the body of my first post.
</p>

Some people really detest writing in HTML which is valid. Other people use their entries in other places, so they need a markup format that's less web-oriented. Some folks write a lot of material in a non-HTML markup format and would like to use that same format for blog entries. These are all very valid reasons to want to use other markup formats.

PyBlosxom allows you to install entry parser plugins which are PyBlosxom plugins that implement an entry parser. These entry parser plugins allow you to use other markup formats. Check the Plugin Registry at http://pyblosxom.sourceforge.net/ for which entry parsers are available.

In general, we only have entry parsers written by people who really wanted that markup format. If you don't see your favorite markup format represented, try looking at the code for other entry parsers and implement it yourself. If you need help, talk to us on the pyblosxom-users or pyblosxom-devel mailing lists.

Details on the various entry parsers should be at the top of the entry parser plugin itself in the Python doc-string.

Other entry parsers:

  • PyMarkdown : Allows you to format your blog postings in Markdown format (http://daringfireball.net/projects/mark...
  • Native : This plugin allows you to use a different format for your weblog entries. This format is basically ...
  • htmlentryparser : This file will use a simple HTML file as the entry format. The <h1/> is used as the title of ...
  • genericwiki : Generic wiki markup PreFormatter for pyblosxom.
  • linebreaks : Preformatter for people who are lazy to type <p>s, <br />s, and </p>. Let's face...
  • moinmoin : Preformatter and entryparser for the MoinMoin wiki software.
  • This preformatter/entryparser uses the...
  • py : An entryparser that reads python source code and formats it accordingly.
  • rst : A reStructuredText entry formatter for pyblosxom. reStructuredText is part of the docutils project ...
  • txtl : PyTextile is a Python port of Textile, Dean Allen's Humane Web Text Generator. It supports all the ...

[23:33] | [/3-USING-PYBLOSXOM] | #-permalink-#

Posting Date

The posting date of the entry file is the modification time (also known as mtime) on the file itself as stored by your file system. Every time you go to edit an entry, it changes the modification time. You can see this in the following example of output:

willg ~/blogdata/blosxom/site: vi testpost.txt                            [1]
willg ~/blogdata/blosxom/site: ls -l
total 16
-rw-r--r--  1 willg willg 764 Jul 20  2003 minoradjustments.txt
-rw-r--r--  1 willg willg 524 Jul 24  2003 moreminoradjustments.txt
-rw-r--r--  1 willg willg 284 Aug 15  2004 nomorecalendar.txt
-rw-r--r--  1 willg willg  59 Mar 21 16:30 testpost.txt                   [2]
willg ~/blogdata/blosxom/site: vi testpost.txt                            [3]
willg ~/blogdata/blosxom/site: ls -l
total 16
-rw-r--r--  1 willg willg 764 Jul 20  2003 minoradjustments.txt
-rw-r--r--  1 willg willg 524 Jul 24  2003 moreminoradjustments.txt
-rw-r--r--  1 willg willg 284 Aug 15  2004 nomorecalendar.txt
-rw-r--r--  1 willg willg  59 Mar 21 16:34 testpost.txt                   [4]

[1] I create the blog entry testpost.txt using vi (a text editor). The mtime of the file will be the time I last save the file and exit out of vi.

[2] See the mtime on the file is Mar 21 16:30. That's when I last saved the blog entry and exited out of vi.

[3] I discover that I made a spelling mistake in my entry... So I edit it again in vi and fix the mistake. The mtime of the entry has now changed...

[4] Now the mtime of the file is Mar 21 16:34. This is the time that will show up in my blog as the posting date.

be careful with the mtimes
 

There are some issues with this method for storing the posting date. First, if you ever change the blog entry, the mtime will change as well. That makes updating blog entries very difficult down the line.

There's a utility that comes with the contributed plugins pack called editfile.py. This will note the mtime of the file, open up your favorite editor to edit the file, and when you're done, it'll reset the mtime of the file back to what it was.

pyfilenamemtime plugin

Using the pyfilenamemtime plugin one can force the mtime by using a filename format like xxxxx-YYYY-MM-DD-hr-mi.txt .

If a filename contains a timestamp in the form of YYYY-MM-DD-hh-mm, change the mtime to be the timestamp instead of the one kept by the filesystem. For example, a valid filename would be foo-2002-04-01-00-00.txt for April fools day on the year 2002.

[23:32] | [/3-USING-PYBLOSXOM] | #-permalink-#

Comments

PyBlosxom does not come with comments functionality built-in. Instead, comments are implemented as a plugin which people who are interested in in having comments can install and everyone else can ignore.

Available comments plugins:

  • Magic Word : The Magic Word plugin is a way of preventing comment spam. It is a bit like the nospam plugin, but...
  • nospam : Human verification for the comments plugin. Based on a idea and ref impl of Jesus Roncero Franco <j...
  • comments : Comprehensive comments support for pyblosxom, includes files for trackback, pingback (requires xmlr...
  • wbgcomment_blacklist : This plugin works in conjunction with the comments plugin (i.e. you need the comments plugin for th...
  • akismet comments : This plugin merges the work of two plugins by myself and Blake Winton. The comment uses the API of ...

[23:31] | [/3-USING-PYBLOSXOM] | #-permalink-#

Syndication

Most newsreaders read most of the syndication formats. So you shouldn't feel that you have to implement each one of them in your blog--you can most assuredly get away with implementing RSS 2.0 or ATOM and be just fine.

Feed formats that come with PyBlosxom

PyBlosxom comes with a few default flavours, three of which are feeds.

RSS 0.9.1

PyBlosxom comes with an rss flavour that produces RSS 0.9.1 output. Here's a sample of what it produces:

Example 9-1. example of RSS 0.9.1 feed


<?xml version="1.0" encoding="utf-8"?>

<!-- name="generator" content="pyblosxom/1.2 3/25/2005" -->
<!DOCTYPE rss PUBLIC "-//Netscape Communications//DTD RSS 0.91//EN"
"http://my.netscape.com/publish/formats/rss-0.91.dtd">

<rss version="0.91">
<channel>
<title>My Blog</title>
<link>http://www.joe.com/blog/index.rss</link>

<description>This is my blog about trivial things</description>
<language>en</language>
<item>
    <title>Example of an entry post</title>

    <link>http://www.joe.com/blog/entries/example1.html</link>

    <description>&lt;p&gt;
  Here's an example of an entry in my blog.  This is the body of the entry.
&lt;/p&gt;
</description>
  </item>
</channel>
</rss>

This example only has one entry in it. The number of entries the rss flavour will display is determined by the num_entries property in your config.py file.

probably better not to use RSS 0.9.1
 

RSS 0.9.1 format lacks dates in the data for the items. Unless you include the date for the entry somewhere in the description block, people looking at your RSS 0.9.1 feed won't know what the date the entry was created on was.

Unless you have some reason to use RSS 0.9.1 as your syndication format, you should look at using RSS 2.0 or Atom 1.0 both of which also come with PyBlosxom.

The standard for RSS 0.9.1 is at: http://my.netscape.com/publish/formats/rss-spec-0.91.html

RSS 2.0

PyBlosxom 1.3 comes with an RSS 2.0 flavour called "rss20". If it's missing features that you want (for example, some folks are doing podcasting with their blog), then override the individual templates you need to adjust.

The standard for RSS 2.0 is at: http://blogs.law.harvard.edu/tech/rss

Atom 1.0

PyBlosxom 1.3 comes with an Atom 1.0 flavour called "atom". If it's missing features that you want, then override the individual templates you need to adjust.

The standard for Atom 1.0 is at: http://atomenabled.org/

[23:30] | [/3-USING-PYBLOSXOM] | #-permalink-#

Static Rendering

Static rendering made its first appearance in PyBlosxom 1.0. It fills the functionality gap for people who want to use PyBlosxom, but don't have a web-server with CGI installed, don't have CGI access, or can't run PyBlosxom for one of a myriad of other reasons. Static rendering allows these people to run PyBlosxom on their local machine, write blog entries, render their entire site into HTML, and then use ftp or some other file copy method to move the pages up to their static web-site.

PyBlosxom's static rendering allows for incremental building. It can scan your entries, figure out what's changed, and render only the pages that need re-rendering.

Beyond that, it's not particularly sophisticated.

Configuring Static Rendering

These are the instructions for configuring static rendering in PyBlosxom.

  1. Install PyBlosxom. When you're copying the pyblosxom.cgi and config.py files, you don't have to put them in a CGI directory--you can put them in any directory you have permissions in. For example, I created a directory /home/joe/pyblosxom/ and put both files in there.

  2. Add static_dir to your config.py file. This is the directory we will save all the static output. The value of static_dir should be a string representing the absolute path of the output directory for static rendering.

  3. Add static_flavours to your config.py file. The value of static_flavours should be a list of strings representing all the flavours that should be rendered. This defaults to [ "html" ].

  4. Add static_monthnames to your config.py file. The value (either 1 or 0) will determine if you want month names (such as April) in the static pages.

  5. Add static_monthnumbers to your config.py file. The value (either 1 or 0) will determine if you want month numbers (such as 04 for April) in the static pages.

  6. Set base_url in your config.py file to the base url your blog will have. For example, if your static_dir were set to /home/joe/public_html and the url for that directory were http://www.joe.com/~joe/ , then you probably want your base_url to be http://www.joe.com/~joe/ .

Example 7-1. static rendering configuration


py["static_dir"] = "/home/joe/public_html/static/"
py["static_flavours"] = ["html"]
py["static_monthnames"] = 0     # i do not want month names
py["static_monthnumbers"] = 1   # i do want month numbers
#!/bin/bash
./pyblosxom.cgi --static

or 
./pyblosxom.cgi --static --incremental

[23:29] | [/3-USING-PYBLOSXOM] | #-permalink-#

PyBlosxom as a WSGI Application with Twisted

Dependencies

  • Twisted - The Twisted event-driven networking framework

  • Twisted Web 2: Twisted Web2 has built-in support for WSGI, which is used to connect to PyBlosxom.

  • The wsgi_app.py file from the PyBlosxom source ./pyblosxom-1.3.2/web directory

General Setup

  1. Install the Twisted and Twisted Web 2 packages.
  2. Create a directory for all of your configuration and run-time files.
  3. Copy web/wsgi_app.py from the PyBlosxom distribution in the directory.
  4. Copy the config.py PyBlosxom configuration file into the directory as well, making any customizations that are needed.

Twisted Configuration

Use a .tac file for the Twisted server configuration. A .tac file is simply a set of Python code required to set up the server and its parameters. Here is the .tac configuration in this presentation demo:

#
# michipug.tac for pyblosxom presentation
#
from twisted.application import service, strports
from twisted.web2 import server, channel, wsgi
from twisted.web2.channel import http
from wsgi_app import application as pyblosxom_application

pyBlosxomResource = wsgi.WSGIResource(pyblosxom_application)

site = server.Site(pyBlosxomResource)
application = service.Application('web')
s = strports.service('tcp:9001', channel.HTTPFactory(site))
s.setServiceParent(application)

Note: the TCP port is 9001

Startup Script

Here is the script from this presentation demo:

#!/bin/bash
set -v
twistd -noy michipug.tac

A more production ready approach might be something like
the following:

twistd -y michipug.tac --pidfile=michipug.pid --logfile=michipug.log

Run it.

$ ./start.sh

[23:22] | [/4-WSGI-DEPLOYMENT] | #-permalink-#

Architecture Overview

PyBlosxom is implemented as a pipeline of plugins. The input to the pipeline is an HTTP request and the output is an HTTP response (most commonly containing HTML or RSS/Atom). There are abstractions to represent the HTTP request and response, the configuration information, the entries and other data stored in PyBlosxom.

In PyBlosxom, a plugin is a Python module. You define functions in the module, and PyBlosxom will call functions with a specific name at a well-defined point in the execution of the PyBlosxom pipeline. For example, a callback function named cb_start is called before most PyBlosxom processing begins. If your plugin module provides a function called cb_start, then it will be called.

PyBlosxom allows you to install many plugins, so a natural question is how do all those plugins interact. The py['load_plugins'] list serve two purposes. It tells PyBlosxom which plugins in the various plugin directories will actually get used. It also defines a sequential order in which plugins will be called. If you don't provide a value for py['load_plugins'] (you must still provide a value for py['plugin_dirs'] if you want to use any plugins), then PyBlosxom will load all the modules in all the plugin directories specified in py['plugin_dirs']. The plugins will be sorted by alphanumeric sorting on their names, and that will be the order in which they are called.

At a given callback point, every module function which provides a correctly named function will be called. So if you load five plugin modules and each module provides a cb_start function, then all five functions will be called when PyBlosxom reaches the start callback point. The order in which those five functions are called will be determined by the rules described in the previous paragraph. This allows multiple plugins to get access to data at the proper point during PyBlosxom's execution. It also allows plugins to exchange information if they know their relative ordering (it's best to use py['load_plugins'] in this case.)

For advanced usage, it is possible to change the way that chains of callback functions are processed. So instead of calling every callback function in the chain, it's possible to arrange for a particular callback chain to work differently. The cb_handle callback chain gives each callback function in the chain a chance to try processing data. If the callback is successful then none of the remaining callback functions in the chain will execute.

When a callback function is called it is passed a dictionary that contains the information in the HTTP request, the information from the configuration file, and any data that may exist in PyBlosxom at that point in time (usually a list of weblog entries). I'll be referring to this dictionary as the "args dictionary' in the rest of this document.

Here's a skeleton of a start callback function -- remember that all callbacks are prefixed with cb_.

def cb_start(args):
    request = args['request']           # get the argument dictionary
    config = request.getConfiguration() # config file information
    http = request.getHttp()            # HTTP header information
    cgi_form = http['form']             # including any CGI form
    data = request.getData()            # the rest of the data

[23:19] | [/5-ARCHITECTURE] | #-permalink-#

Basic Pipeline

PyBlosxom as a pipleline of plugins
pyblosxom basic pipeline

Now we're ready to look at the various stages of the PyBlosxom pipeline. We're going to drill down through the various layers.

At the simplest level the execution of PyBlosxom is broken up into an initialization section and a "main" section. We'll skip over the initialization for the moment, since it is really only needed for entryparser usage, which we'll cover later on.

pyblosxom basic pipeline

(In the diagrams I've omitted the cb_ prefix on the names of all the callbacks.)

The main section calls the cb_start callback, which is where you would perform any global initialization in your plugin if you needed that initialization to happen before any real work got done in the pipeline.

Next PyBlosxom will call any cb_handle callbacks. If a cb_handle callback is successful, PyBlosxom will jump to the end of its processing, calling any cb_end callbacks before it returns an HTTP response. (Note that the HTTP request is processed by the cb_handle callback in this case). If the cb_handle callback fails or there are no cb_handle callbacks registered, then processing passes to the blosxom handler, which does its job and then exits via cb_end.

The cb_handle callback provides a way to bypass the Blosxom handler in case you need to do a different kind of processing. Currently this is used to implement XML-RPC handling in PyBlosxom, since XML-RPC handling consumes and produces completely different input and output from regular PyBlosxom processing.

[23:18] | [/5-ARCHITECTURE] | #-permalink-#

Blosxom handling

The blosxom_handler is a combination of a series of setup callbacks followed by a renderer that is actually responsible for producing the output.

blosxom-handler.jpg

The first of the setup callbacks, cb_renderer, is used to select a renderer to be used. At the moment, the core distribution contains a a Blosxom compatible renderer and a debugging renderer. People have expressed interest in building renderers based on other templating engines, such as Cheetah.

Now we come to the first callback function that actually does something with the HTTP request. The cb_pathinfo callback is charged with looking at the information in the request and modifying the data part of the plugin args dictionary with information about the type of entry being requested:

  • 'bl_type' - the type ('dir' or 'file')
  • 'pi_bl' - the HTTP PATH_INFO header
  • 'pi_yr' - the yar
  • 'pi_mo' - the month, either as a 2 digit number or 3 letter string
  • 'pi_da' - the day as a 2 digit number
  • 'root_datadir' - the full path to the entry folder in the filesystem
  • 'flavour - the flavour of output requested

After cb_pathinfo has identified the entry being requested (and its type), the cb_filelist callback is used to generate a list of entries. The entries will all be instances of pyblosxom.entries.base.EntryBase, and will be stored in the data dictionary under the key 'entry_list'. In the default PyBlosxom configuration, these entries are actually instances of pyblosxom.entries.fileentry.FileEntry that represent weblog entries stored in the filesystem according to the Blosxom family rules. In particular, instances of FileEntry have the mtime (modified time) of the file for each entry, which is used to sort the entries in reverse chronological order for a typical weblog presentation. By providing your own cb_filelist callback, you could provide entries using some other storage mechanism, at least in theory. Currently PyBlosxom doesn't support storing entries in other storage mechanisms, though it's something we are looking into fixing in the future.

After the list of entries has been generated, it will be sent to the renderer to be formatted for output. Before this happens, there are two callbacks that can be used to alter that data that will be rendered.

The cb_prepare callback is a general-purpose callback that should be used to modify the data before the rendering phase. This can include injecting or filtering out entries, or modifying the data in specific entries. Another common thing to do in cb_prepare is to create new variables that can be accessed from the renderer, in order to provide some additional information. You can even use cb_prepare to handle HTTP POST requests, which is what the comments plugin does.

Just before rendering, the cb_logrequest callback is called. This is mostly used to log information to a file. To that end, it expects an entry in the argument dictionary whose key is filename. It also expects an argument entry keyed by return_code. This is why there is a separate callback (you ought to be able to do lots of logging type functionality using cb_prepare). The internal pathway through the PyBlosxom core is slightly different from that of cb_prepare, in order to obtain the return_code for cb_logrequest.

[23:17] | [/5-ARCHITECTURE] | #-permalink-#

Rendering

By default, PyBlosxom uses a renderer that is based on the Blosxom template language. The renderer allows a template writer to insert variables into their weblog templates. At rendering time, the renderer replaces the variables with their actual values -- that's the meaning of 'render' in the context of PyBlosxom.

The Blosxoms combine several distinct templates in order to create a page in a weblog:

  • head - the content of this template is inserted before any entries are rendered.
  • date_head - the content of this templete in inserted at the start of a new day (the output is sorted chronologically).
  • story - the content of this template is inserted once for each entry to be rendered.
  • date_foot - the content of this templete in inserted at the end of a day (the output is sorted chronologically).
  • foot - the content of this template is appended after all entries have been rendered.
render.jpg

The Blosxom renderer also has callback functions that you can provide in order to override behavior. The callbacks are named after the templates they are associated. They are all executed right before the template is rendered. There is also a callback that isn't associated with a template, cb_story_end that is called after the store template is rendered. Also, these callbacks are passed different arguments. They are passed a dictionary that contains all the variables that the renderer will use for template rendering.

  • cb_head - before the head template is rendered.
  • cb_date_head - before the date_head template is rendered.
  • cb_story - before the story template is rendered.
  • cb_story_end - the story template is rendered.
  • cb_date_foot - before the date_foot template is rendered.
  • cb_foot - before the foot template is rendered.

[23:16] | [/5-ARCHITECTURE] | #-permalink-#

Entry Parsers

During PyBlosxom initialization there a call to the cb_entryparser callback. The cb_entryparser callback returns a dict whose keys are filename extensions and whose entries are functions uses to parse entry files whose extension is the key. This allows the user to use different formatting languages to write weblog posts. PyBlosxom includes entryparsers that can deal with ReStructuredText and Textile formats, as well as Python source code.

The blosxom_entry_parser (the default) parses entry files with extensions of '.txt'. The first line of a file will be the title of the entry, and the rest of the file will be the body of the entry. The body will be substituted directly in to the blosxom templates, and can contain HTML.

Entry parsers also support their own plugin mechanism. There are two callbacks, cb_preformat and cb_postformat that can be called before and after the entry parser has done whatever parsing it does. cb_preformat and cb_postformat must return a dict that contains entries for the 'title' and story keys. The dict can have more entries that just these two. PyBlosxom includes preformatters for allowing wiki words and replacing linebreaks with <br /> or <p>.

[23:15] | [/5-ARCHITECTURE] | #-permalink-#

XML-RPC Subsystem

The XML-RPC subsystem is the last of the major PyBlosxom subsystems that we need to cover. It is interesting because it is implemented entirely via the plugin system - in fact it's contained entirely in the contributed plugins distribution. What we want is a way for PyBlosxom to handle requests made via XML-RPC. Initially, this is because we want to support some of the popular XML-RPC based blog authoring API's, such as the Blogger and Metaweblog API's. But we'd also like to make it possible for plugins (like the pingback handler for the comments plugin) to offer some of their services via XML-RPC, which means that we cannot hardwire the set of functions that are processed by the XML-RPC system.

xmlrpc.jpg

The XML-RPC system consists of a PyBlosxom plugin that acts as a dispatcher for all XML-RPC services. This plugin, named xmlrpc, is wired in via PyBlosxom's cb_handle callback, since we want it to process any HTTP request that contains an XML-RPC request. Recall that cb_handle bypasses the regular blosxom_handler flow through the PyBlosxom engine. xmlrpc also takes care of authenticating XML-RPC requests. The plugin takes care of dispatching XML-RPC requests by using a dispatch table stored in the plugin's args dictionary. The table is a Python dictionary that is accessed via the 'methods' key.

Every plugin that wants to offer its services via XML-RPC needs to provide a cb_xml_rpc_register function. This function must update the 'methods' dictionary with entries the map the XML-RPC method names to functions in the plugin's module. XML-RPC plugins may use any of the other PyBlosxom callback methods as well.

[23:14] | [/5-ARCHITECTURE] | #-permalink-#

More About Plugins

Example Plugin

import sys, time

def verify_installation(request):
  # No configuration parameters
  return 1

def cb_story(args):
  entry = args['entry']
  time_tuple = entry['timetuple']
  try:
    import datetime
    import US
    dt = datetime.datetime.fromtimestamp(entry['mtime'], US.Pacific)
    entry['w3cdate'] = dt.isoformat()
  except ImportError:
    pass

  try:
    import xml.utils.iso8601
    tzoffset = time.timezone
    # if is_dst flag set, adjust for daylight savings time
    if time_tuple[8] == 1:
      tzoffset = time.altzone
    entry['w3cdate'] = xml.utils.iso8601.tostring(time.mktime(time_tuple),tzoffset)    
  except ImportError:
    pass

Show how the $w3cdate variable is used in the gray story template.

[22:59] | [/6-MORE-ABOUT-PLUGINS] | #-permalink-#

PyBlosxom Resources

Attribution: Most of the material in this presentation is a repackaging of the information found in the documents listed above.

[22:39] | [/7-RESOURCES] | #-permalink-#