March 9, 2010

Google AppEngine + Google Docs + Some Python = Simple CMS

I was looking for a CMS that could run on the Google App Engine lately. As the free quotas are pretty high, a GAE powered CMS would make sense for small personal web pages that do not get too much traffic and have to run on a tiny budget. There are some CMSes (e.g. the App Engine Site Creator) but I didn't like them a lot. They are either too restricted or don't seem to have a large user base which makes their future uncertain. Also, as the GAE datastore is not a standard database, you have additional issues: How to backup? Possibility to migrate to another host? How to import or export of data?
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
The GAE app would only need to pull the data from the spreadsheet, apply some post-processing and output it. Not really much for the app to do, so I decided to write a little prove-of-concept application. I was using the following ingredients
  • 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
The app expects 2 tables to be present in the spreadsheet: pages and templates. Both need a content column, the templates table also needs a name column. That's it. It could look like this:

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.

February 21, 2010

appify: Make Web Apps Look Like iPhone Apps

As you probably know, simple web pages can appear as 'normal' apps on your iPhone (or iPod touch) when you add them to your home screen using Safari's '+' menu. With some changes to the web site you can enhance the 'app feeling' even further. You can:
  • Give it a nice icon
  • Add a startup screen which appears when clicking the icon on your home screen
  • Hide the Safari address bar and the menu
All the details on what you have to do in your code can be found e.g. here.

And voilĂ , your web page looks like a normal iPhone app.


Left: My web page in Mobile Safari
Right: Same page through appify


But what if you cannot change the code (as it might not be your web site)? I wrote a small web app (I called it 'appify') which can do the trick for you. appify adds a new starting page to the web app of your choice, and adds all of the nice features I just listed. And after displaying that starting page, the browser is redirected to the actual web app.

You can appify a web app yourself. Or you simply try it on your iPhone with the appified mobile version of GMail.




Note: All of this is nice but has one drawback: When you click on a link on your appified web page, the iPhone will close the app and open the new page in Safari. This doesn't happen with AJAX web apps where you do not have 'normal' links. So appify might not be too useful for many web pages :-(. But it works great for e.g. GMail.
This does not only affect appify, but all web apps on the iPhone. If you know a way to avoid this, please let me know!

A small interesting details from the implementation: I have to give the user a chance to save the web site on the iPhone home screen BEFORE I redirect to the actual web app. I found an easy way to do this: In JavaScript you can detect if the app is in standalone mode (i.e. it is fullscreen without the address bar and menu):
if (window.navigator.standalone) {
// fullscreen mode
redirect();
}

So when we are not in fullscreen mode I do not redirect and the user can save the page to the home screen. When coming through the new home screen icon, Safari runs in standalone mode and the script redirects to the actual app.