My search ended without having found something I really liked...
But while searching, I had an idea: Why not just use a spreadsheet hosted on Google Docs as the base for a CMS? It has a couple of advantages:
- Simple, everybody knows how to use a spreadsheet application
- you get an online editor for free
- Google Docs does versioning, so it's easy to roll back changes
- Backup/restore and import/export is simple
- Google App Engine SDK for Python
- gdata-python-client, the Python version of the Google Data API to access the spreadsheet
- Mako, a templating system for Python
- and some code to glue these together
templates:
pages:
There are 2 templates defined, main and defs. The template main is the base template for the html output, defs holds some Mako style methods (more on whose later).
The pages table holds 3 pages. The first one is used as the root document, the others can be got via http://<host>/page/<id>. The content is treated as a Mako template, references to other templates are resolved by looking up the name in the templates table. Therefore the first two lines load methods from the defs template and make the page inherit from main:
<%namespace import="*" file="defs" />
<%inherit file="main"/>
The page content invokes 2 methods, image() and page(). The methods make use of global objects that are injected into the global Mako template context:
- pageid, which is the id of the page currently rendered
- page, representing the current page row, a dict with the column names as keys
- db, the Database object. Row objects can be retrieved like this:
db.<table>[<row_no>]['<col_name>']
Using the db object any table of the spreadsheet can be accessed. This way the system can be extended in a really easy way. In out little example this is used for the image() method. References to images are stored in a table called images and are referenced by an id:
<%def name="image(id)">
<img src="${db.images[id]['url']}" alt=""/>
</%def>
When rendering the root url, this now results in:
<html>
<head><title>Welcome!</title></head>
<body>
<h1>Welcome!</h1>
<p>
<p>Some text</p>
<img src="http://www.andreaskahler.com/me.png" alt=""/>
<br/>
<a href="page/2">link to page 2</a>
</p>
this is the footer
</body>
</html>
Not really a useful page, but you get the idea.
The python code for the whole thing looks like this:
#!/usr/bin/env python
import sys
import logging
import cgi
from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
import gdata.spreadsheet.text_db
from mako.template import Template
from mako.lookup import TemplateLookup
from mako.runtime import Context
from StringIO import StringIO
email = 'your.google.account@gmail.com'
password = 'yourpasswd'
spreadsheet = 'spreadcms' #name of spreadsheet
table_pages = 'pages'
table_templates = 'templates'
col_name = 'name'
col_content = 'content'
class Db:
""" wrapper for the spreadsheet. places the tables into its __dict__ """
def __init__(self):
self.client = gdata.spreadsheet.text_db.DatabaseClient(username=email, password=password)
self.db = self.client.GetDatabases(name=spreadsheet)[0]
for table in self.db.GetTables():
name = table.entry.title.text
self.__dict__[name] = Table(self, name)
def getTableRecord(self, table, row_no):
return self.db.GetTables(name=table)[0].GetRecord(row_number=row_no).content
class Table:
""" wraps a spreadsheet table. access rows with their index (starting with 1) """
def __init__(self, db, table):
self.db = db
self.table = table
def __getitem__(self, index):
return self.db.getTableRecord(self.table, index)
class MainHandler(webapp.RequestHandler):
""" the main web request handler """
def connect(self):
try:
self.db = Db()
except gdata.spreadsheet.text_db.Error, err:
self.response.out.write(err)
return False
return True
def get(self):
if not self.connect():
return
# get page id, default to 1
pageid = 1
parts = self.request.path.strip('/').split('/')
if len(parts)==2 and parts[0]=="page":
try: pageid = int(parts[1])
except ValueError: pass
page = self.db.__dict__[table_pages][pageid]
mylookup = TemplateLookup(filesystem_checks=False)
#load all templates
i = 1
while True:
try:
template = self.db.__dict__[table_templates][i]
mylookup.put_template(template[col_name], Template(template[col_content], lookup=mylookup))
except:
break
i+=1
content = page[col_content] or ""
# invoke Mako
try:
t = Template(content, lookup=mylookup)
mylookup.put_template("_page"+str(pageid), t)
s = t.render(page=page, pageid=pageid, db=self.db)
self.response.out.write(s)
except Exception,e:
s = "<h1>Error</h1>"
s += "<pre>" + cgi.escape(str(e)) + "</pre>"
s += "<h1>Page</h1>"
s += "<pre>" + cgi.escape(page[col_content]) + "</pre>"
self.response.out.write(s)
def main():
application = webapp.WSGIApplication([('/', MainHandler),
('/page/[0-9]+', MainHandler)],
debug=True)
logging.getLogger().setLevel(logging.DEBUG)
#logging.getLogger().setLevel(logging.WARNING)
util.run_wsgi_app(application)
if __name__ == '__main__':
main()
Of course this is not production-ready code. A lot of error handling is missing and one needs a proper way to handle the gdata login captcha requests (I guess storing a valid token would work). There is also no caching implemented.
This of course is not intended to replace a full-blown CMS. But I guess if you would spend some time fixing these things, this could be suitable for small web sites, where all content is more or less static.