Example App

We want to write a small throwaway todolist, that really does the following simple things:

  1. Allow us to enter a new Task (create and delete)
  2. Allow us to enter subtasks under the Task (create and delete)
  3. Allow us to export the entire todo list.
  4. Allow us to import a todolist in the expected format

Creating the project

If you have not done so yet, go ahead and create the project, following the steps on the Get Started page.

STOP CHECK!!!

For this example, we'll be using Cheetah as a templating system, SqlObject as an ORM and an SQLite database backend. Before continuing, make sure they're properly installed on your machine.

Essential Configuration - The plugins

With simpleweb, configuration is done in the config.py file. Simpleweb uses it, and if you want to use it, you can always 'import config' anywhere that you need it.

First, we'll ensure our templating plugin is what we need. There is currently only *integrated* support for Cheetah, but you don't even need to have another system integrated to use it. Simpleweb is that simple.

Lets go ahead and fire up config.py in our favourite editor

template_plugin = template.Cheetah()

Next, we'll have to enable a Database plugin config. In this case, we have two default plugins, SqlObject and SqlAlchemy. We'll select SqlObject and move along.

The db_plugin URI strings have a format. You can learn more about that from the sqlobject documentation. To use the sqlite interface, you need to specify an Absolute Path for the sqlite database. This should look like:

import os

db_path = os.path.join(os.getcwd(), 'todolist.db')
db_plugin = dblayer.SqlObjectDB('sqlite:%s' % (db_path))

For windows users, Sqlite needs a special way of specifying the file path see the sqlobject docs for details of this. Mervin Beng was kind enough to send in a work around for windows systems.

import os
import re

db_path = os.path.join(os.getcwd(), 'todolist.db')

if(db_path[1] == ':'):              #looks like a Windows path
   s = re.sub('\\\\', '/', db_path) #change '\' to '/'
   s = re.sub(':', '|', s, 1)       #special for sqlite
   db_path = '/' + s

db_plugin = dblayer.SqlObjectDB('sqlite:%s' % (db_path))

We can delete all the other commented-out lines now, to make our config.py look clean. We're set to move on.

Building The Models

Next, we need to build our data models which will be contained in our 'models.py'. This is a particularly simple application, so the models will also be simple. In fact we'll have two tables - Task and SubTask. The Models we have in our models.py look like this:

from sqlobject import *

class Task(SQLObject):
	description = StringCol(unique=True)
	subtasks = MultipleJoin('SubTask')


class SubTask(SQLObject):
	description = StringCol()
	task = ForeignKey('Task')

For more information on using SqlObject... see the sqlobject tutorial.

Create The Tables

Now that we have our models, we will proceed to create the tables. Once again, simpleweb steps in to help out a bit. The command is:

simpleweb-admin create-tables

Tables are created and we're ready to go to the next step.

Planning the Application API

We're going to create some mappings in url.py. First, we want our app to look like:

GET 	/task 		---> List All Available Tasks, 
						 Provide form to add new task
POST 	/task 		---> Add new task.
DELETE 	/task/id	---> Delete task with taskid = id
GET 	/task/id	---> List All SubTask for task with 
						 taskid = id, Provide form to add 
						 new subtask
POST 	/task/id	---> Add new Subtask for task with taskid = id
DELETE 	/subtask/id	---> Delete task with subtaskid = id 

GET 	/import 	---> Display upload form
POST 	/import 	---> Upload form with tasks and subtasks to database.
GET 	/export		---> Display exported data in a text/plain file we can save.  

Hooking this up in urls.py will look like this:

from simpleweb import urladd

urladd('/', 'controllers.index')
urladd('/task[/[{id:number}[/]]]', 'controllers.task')
urladd('/subtask/{id:number}[/]', 'controllers.subtask')
urladd('/import[/]', 'controllers.import')
urladd('/export[/]', 'controllers.export') 

The urladd function is used to create a new URL-PATTERN to CONTROLLER-OBJECT mapping. The url patterns are based on Selector, yet another wsgi utility that simpleweb relies on.

To learn more on the URL patterns, see the Selector documentation.

Now that we have the urls mapped up to our controllers, we now have to create the controllers.

Creating Controllers and Templates

In the 'controllers' package, there will already be an index.py which looks like:

import config
import models

def GET(request):
	return config.template_plugin.render('index.html') 

You can see that we import config so that we have access to your template_plugin. Other config.py variables are also available here. We use the config.template_plugin.render() method to render any templates (e.g 'index.html') which will be found in the templates/ subfolder.

With that in mind, we can go ahead and build the controller for task (GET and POST).

Our First controller handler (GET '/task/[{id:number}[/]])

Open up controllers/task.py and type in the following.

import config
import models

def GET(request, id=None):

	if id is None:
		task_list = models.Task.select()
		return config.template_plugin.render('tasks.html', **locals())
	else:
		task = models.Task.get(id)
		return config.template_plugin.render('task.html', **locals()) 

Firstly, I have imported config and models which I need. Next, is the definition of the GET handler. All simpleweb handlers are actually Yaro callables with the url dispatcher variables passed in as arguments.

Since in urls.py I specified the 'id' parameter in the task url map as optional, I have to supply a default value for id in the GET definition. I then have to check for that, so I will know when I receive a /task or a /task/5. Depending on the value of id, we server us either 'tasks.html' or 'task.html'

Next we build the Template 'tasks.html', which will give us a form to add new Tasks and show us all available tasks.

Now a template

By default config.template_plugin is a Cheetah template (as you can see in config.py), it's trivial to add another, and you don't even need to add another template system to config to use it, you can use it just by importing it.

Lets go ahead to open up templates/tasks.html and type in the following.

#include "master.html"

#def body
<h3>Available Tasks</h3>
<ol>
	#for $t in $task_list
	<li><a href="/task/$t.id">$t.description</a></li>
	#end for
</ol>

<br/>
<br/>

<h3>Add Task</h3>
<form action="/task/" method="POST">
	<table align="center" border="0" cellspacing="0" cellpadding="0">
		<tr>
			<td valign="top">Description</td>
			<td><textarea name="description"></textarea></td>
		</tr>
		<tr>
			<td> </td>
			<td>
				<input type="submit" value="Add Task"/>
				<input type="reset" value="Cancel"/>
			</td>
		</tr>
	</table>
</form>

#end def

Let's see what we've done so far by pointing to http://localhost:8080/task. We do not need to restart the internal server, as it automagically reloads newer versions of our project by default. Also, you will see warnings by the server about failing to import some controllers. Just ignore them, as they're just that - warnings.

Introducing the 'request' object

Let's now add the POST controller so we can add tasks and see what happens. Open up controllers/task.py and add the following:

def POST(request, id=None):
	if id is None:
		description = request.form['description']
		models.Task(description=description)
		request.redirect('/task/')

A couple of things are happening in the POST handler. First, id also has a default value so we can differentiate a POST to /task (which adds a new task) and a POST to /task/5 for instance (which adds a new subtask to the task with the given id).

We see here how to grab variables from our form. They're in our second most usefull object... the REQUEST object. request.form is a dictionary with our form variables in it. We add a new Task using the sqlobject model we already have (models.Task). Then we use our request object once more to redirect the request to '/task/', causing the page to refresh, so we see our updates.

Pretty straightforward huh? I think so :). Go ahead, use the form on http://localhost/task, to add a new task and see how it works.

Viewing Task Details, Adding SubTasks

When you run the app right now, and add a couple of tasks, you'll notice that they link to a url that looks like: /task/1, etc. Currently, our GET handler in controllers/task.py tries to render the 'task.html' template which doesn't exist yet, so we'll go ahead and add that template.

This template will give us a page which lists all the subtasks for that task, and allow us to add more subtasks to the task.

Fire up templates/task.html in your favourite editor and add the following

#include "master.html"

#def body
<h3>Description</h3>
<p>$task.description</p>

<br/>

<h3>SubTasks</h3>
<ol>
	#for $st in $task.subtasks
	<li><a href="/subtask/$st.id">$st.description</a></li>
	#end for
</ol>

<br/>
<br/>

<h3>Add SubTask</h3>
<form action="/task/$task.id/" method="POST">
	<table align="center" border="0" cellspacing="0" cellpadding="0">
		<tr>
			<td valign="top">Description</td>
			<td><textarea name="description"></textarea></td>
		</tr>
		<tr>
			<td> </td>
			<td>
				<input type="submit" value="Add SubTask"/>
				<input type="reset" value="Cancel"/>
			</td>
		</tr>
	</table>
</form>

#end def

Go ahead and see what happens when you now click on one of the tasks that you've added.

POST for subtasks

Now we'll have to enable the addition of subtasks. To do this, we'll modify our existing POST handler in the task.py controller to also handle '/task/id'

Lets edit controllers/task.py and modify the POST method to look like the following

def POST(request, id=None):
	if id is None:
		description = request.form['description']
		models.Task(description=description)
		request.redirect('/task/')
	else:
		description = request.form['description']
		task = models.Task.get(id)
		models.SubTask(description=description, task=task)
		request.redirect('/task/%s' % (id)) 

Now go ahead and add subtasks to the different available tasks. Nice huh? :)

Here you'll note that we first get a 'task' instance based on the id we get, before we go ahead to create a new SubTask instance, this isn't simpleweb semantics, but sqlobject semantics

Pragmatic Delete - But browsers don't send PUT and DELETE

Next we'll have to implement the DELETE for tasks and subtasks. To do this, we'll implement the DELETE handler in the task controller. The only problem is that, while this will work via webservice clients, normal browsers can't submit forms with methods other than GET and POST. If you try to do a method="DELETE", it will just submit a GET instead. The question is what do we do about this?

A solution I normally use is to do a normal post, but make sure the delete button has a unique name, then I will check for that name in the POST handler, and work with it. Also, since we want to make our app still have a compatible webservice interface, we'll still implement the DELETE handler, and call that from the POST to do the actual dirty delete work.

First, lets edit controllers/task.py and add the DELETE handler. The DELETE handler does its work by first deleting all available subtasks before we delete the main task.

def DELETE(request, id):
	task = models.Task.get(id)

	for subtask in task.subtasks:
		models.SubTask.delete(subtask.id)

	models.Task.delete(id) 

Secondly we again modify the POST handler to call the delete to do the actual work. Once more we update controllers/task.py

def POST(request, id=None):
	if id is None:
		description = request.form['description']
		models.Task(description=description)
		request.redirect('/task/')
	else:
		if request.form.has_key('delete'):
			DELETE(request, id)
			request.redirect('/task/')
		else:
			description = request.form['description']
			task = models.Task.get(id)
			models.SubTask(description=description, task=task)
			request.redirect('/task/%s' % (id))

Then of course we modify the task template to add the delete form just under the description. Lets edit templates/task.html to look as follows

#include "master.html"

#def body
<h3>Description</h3>
<p>$task.description</p>
<form action="/task/$task.id/" method="POST">
	<input type="submit" name="delete" value="Delete"/>
</form>

<br/>

<h3>SubTasks</h3>
<ol>
	#for $st in $task.subtasks
	<li><a href="/subtask/$st.id">$st.description</a></li>
	#end for
</ol>

<br/>
<br/>

<h3>Add SubTask</h3>
<form action="/task/$task.id/" method="POST">
	<table align="center" border="0" cellspacing="0" cellpadding="0">
		<tr>
			<td valign="top">Description</td>
			<td><textarea name="description"></textarea></td>
		</tr>
		<tr>
			<td> </td>
			<td>
				<input type="submit" value="Add SubTask"/>
				<input type="reset" value="Cancel"/>
			</td>
		</tr>
	</table>
</form>

#end def

Go ahead and try adding and deleting a couple of tasks, to see how it all works so far.

Now for those pesky SubTasks

Now we also have to delete subtasks, which means implementing a GET handler for subtasks, that will allow us to view the subtask details and the delete button, then adding the DELETE handler and the POST handler for calling the DELETE handler

Note that we're just doing this POST/DELETE acrobatics because we're using web forms. If this were a web service, we wouldn't be doing these.

Let's fireup templates/subtask.html in our favourite editor and type in the following

#include "master.html"


#def body
<h3>Description</h3>
<p>$subtask.description</p>
<form action="/subtask/$subtask.id/" method="POST">
	<input type="submit" name="delete" value="Delete"/>
</form>
#end def

We'll also need to add the subtask controller. Lets fire up controllers/subtask.py in an editor and add the following

import config
import models

def GET(request, id):
	subtask = models.SubTask.get(id)
	return config.template_plugin.render('subtask.html', **locals())

def POST(request, id):
	if request.form.has_key('delete'):
		task_id = DELETE(request, id)
		request.redirect('/task/%s' % (task_id))
	else:
		return "Unknown request"

def DELETE(request, id):
	subtask = models.SubTask.get(id)
	task_id = subtask.task.id
	models.SubTask.delete(id)
	return task_id

The template is really straightforward at this stage. Also, the controller is pretty self-explanatory.

The only trick I've used in the DELETE handler is to make it return the parent task id for the subtask we've just deleted, that way, we can redirect back to the task in the POST handler

Also, note that we don't do the redirections in the DELETE handler since they will only ever be called via a webservice (which doesn't care for the redirect to pages, or from the POST which is the proper place to redirect in this case)

Go ahead and run it all, to see what we have so far. Our requirements are almost complete. We just need to do the /import and /export controllers and we're good to go. Also, we don't have any navigation on this site, which just blows terribly.

More About Templates

Before we add import and exporting capabilities, lets go ahead and add a basic navigation menu. Since this is a very simple application, we'll just throw it into the master template which sits at templates/master.html. We'll now modify it, and add a div just below the banner, and add three links:

Let's edit our templates/master.html to look like this:

#def title
todolist
#end def

#def page
todolist
#end def

#def body
todolist
#end def

<html>
<head>
    <title>$title</title>
    <link type="text/css" media="screen" rel="stylesheet" href="/static/css/main.css"/>
</head>

<body>
	<div id="banner"><h1>$page</h1></div>
	<div id="menu">
		<ul>
			<li><a href="/">Dashboard</a></li>
			<li><a href="/task/">Tasks</a></li>
			<li><a href="javascript:history.back(1)">Back</a></li>
		</ul>
	</div>
	<div id="container">
			$body
	</div>
	<div id="footer"> 
		<p>Powered by 'simpleweb'</p>
		<p>Copyright © 2006 Essien Ita Essien</p>
	</div>
</body>

</html>

We'll also have to change that homepage to be a real 'Dashboard' :). A central point for the application more like. Let's edit templates/index.html to look like this:

#include "master.html"

#def title
	todolist - Dashboard
#end def

#def page
	todolist Dashboard
#end def

#def body
	<p>
	Todolist is a sample simpleweb application built to teach quickly, how to work with simpleweb.
	</p>

	<h3>Menu</h3>
	<ul>
		<li><a href="/task/">Manage Tasks</a></li>
		<li><a href="/import/">Import Tasks List</a></li>
		<li><a href="/export/">Export Tasks List</a></li>
	</ul>
	
#end def

Go ahead, play around and see what the application looks like thus far. You may have to refresh your current page to see the menu/navigation bar.

At this point, we have a basic application (there are no updates, but based on how we added delete, that shouldn't be hard to add. Just keep in mind that in HTTP speak, updates are done with HTTP PUT)

At this point, we'll go on to the import and export functionality

Exporting Tasks

We'll first have to figure out a simple format to use, so that it's trivial. The format we'll be using will be strict, so that things are easy to parse without building a computer science project :). The format is as follows:

  1. There will be groups
  2. Each group contains a Task description (ON ONE LINE) and its subtasks each on their own single lines.
  3. Once we meet more than one newline, it ends the current group.

An example:

House Cleaning
Clean kitchen
Clean that pesky bedroom!
Take out the garbage!!!


World Domination
Build lousy kernel, just make sure it can print to screen.
Find a way of convincing other smart guys to improve it.
Hope google doesn't cache this blueprint so we're not found out.

The /export controller :- Meet 'request.res'

Now that we have our format, lets do the export controller, we'll keep it simple by making it export the format on the returned page, so you can just do a 'Save As' from the browser.

We'll first create the template, which we'll put in templates/export.txt.

#for $task in $task_list
$task.description
#for $subtask in $task.subtasks
$subtask.description
#end for

#end for

Note the blank line before the last 'end for'. That ensures that different groups are seperated with more than one newline.

Next lets create the controller in controllers/export.py

import config
import models

def GET(request):
	task_list = models.Task.select()

	request.res.headers['Content-Type'] = 'text/plain'
	return config.template_plugin.render('export.txt', **locals())

This is simple enough. Infact, the only strange thing there is the 'request.res.headers' line.

Like previously mentioned, the request object contains a wealth of information, and one of them is the response object aptly named 'request.res'. The response object has a headers dictionary which you can tweak when you want to. In this case, we want to return plain text, not html, so we set the 'Content-Type' header to 'text/plain'. The rest is normal

Let's test what the export page does right now. Neat? I think so

File Uploads: The final frontier

To neatly round up our todolist, we'll now implement the import functionality of our app, and test it out. To do this, you should first run your todolist, and delete all the items on the list.

First comes the template, which is just a form with a file upload control. Note that for the HTML file-upload control to work well, we need to add an extra attribute to the form... the enctype='multipart/form-data'. This is what enables HTML file uploads to work.

Type the following into templates/import.html

#include "master.html"

#def body
<h3>Select File To Upload</h3>
<form action="/import/" enctype="multipart/form-data" method="POST">
	<input type="file" name="import_file"/>
	<input type="submit" value="Upload"/>
</form>
#end def 

Next type the following into controllers/import.py, to hook up the import controller, with its GET and POST handlers.

import config
import models


def GET(request):
	return config.template_plugin.render('import.html')

def POST(request):
	import_file = request.form['import_file']

	if import_file.filename.strip():
		content = import_file.file.read().strip()
		groups = (i for i in content.split('\n\n') if i.strip())
		for group in groups:
			detail = group.strip().split('\n')
			task = models.Task(description=detail[0].strip())
			for s in detail[1:]:
				models.SubTask(description=s.strip(), task=task)

		request.redirect('/task/')
	else:
		return "No file to upload"

As usual the 'import_file' form variable shows up in request.form['import_file']. The cool thing is that thanks to Yaro's design, import_file will be a cgi.FieldStorage object!!! This means if you know how to use python's CGI FieldStorage object... you're good to go

We first check that the import_file.filename field is not empty. If it were empty, that would mean that you submitted the form, without any filename to upload.

The rest of the POST handler is just our simpleparser that grabs from the file and dumps in the database

Of course the GET handler just renders the import page for us, nothing more

Hurray! We're done

Go ahead and check out the whole application, and play with it some.

At this point, we've acheived all we set out to achieve for this simple throwaway todolist application

In building this application, we've come across the basic building blocks of a simpleweb application, and we can proceed to build more complex applications.

Are we there yet?!!

No we're not... but seriously, there's more that can still be done. 'simpleweb-admin' for instance, has more options, that you can investigate by doing:

   simpleweb-admin help
   

For specify reference to more features and pizzaz, check the HowTo's section of the site.