(Another) simple web.py wiki

After spending considerable time learning Django, I stumbled upon another Python framework called web.py. Web.py is much simpler than Django and has far fewer built in features. For example, web.py has no built in object relational mapper, automatic admin site, or authentication classes. These features can be incredibly useful (and time-saving), but are not always suitable for every job. I still really like using Django, but it's nice to know that there is something much more flexible out there. After messing around with the tutorials and getting help from some generous Web.py Google Group members, I built a very simple wiki.
import web
from web import form
import sqlite3

render = web.template.render('templates/')

db = web.database(dbn='sqlite', db='database.sqlite')

urls = (
        '/', 'index',
        '/create/([a-zA-Z0-9_]+)', 'create_page',
        '/([a-zA-Z0-9_]+)', 'view_page',
        '/edit/([a-zA-Z0-9_]+)', 'edit_page'
)

app = web.application(urls, globals())
This section sets up some of the most basic parts of the wiki: gives database parameters, maps urls, tells the app where to look for templates and instantiates the application object. Like Django, the url mapper matches regular expressions to predefined functions--well not exactly. Django maps urls to functions, web.py maps urls to classes. However, the work flow is exactly the same: write a regular expression and then tell it where to go for further instructions. Passing variables in the url is a bit different, though. In Django, you must explicitly define variables in the urlconf. With Web.py, you simply put a regular expression inside parentheses.
edit_form = form.Form(
	form.Textarea("content", description=""),
	form.Hidden("id", description=""),
	form.Button("save", type="submit", description="")
)
This section allows you to define a form using the web.form module. This is very similar to using a Form class in models.py with Django.
def get_page(name):
	myvar = dict(page_title="%s" % name)
	pages = db.select('page', myvar, where="page_title = $page_title")
	v = []
	for page in pages:
		v.append(page)
	if v != []:
		return page
	else:
		return False
This section should give you a good idea how the web.db module works. In place of an ORM, web.py comes with a database wrapper compatible with sqlite, mysql, postgres and a few others. This get_page function is used throughout the script to make querying the database a bit easier. The myvar variable is supposedly a convenient way sanitize sql queries as well as give arguments to the db.select function. I'm still sitting on the fence on this one. This function simply queries the database to get all matches where page_title is the same as the given page title. If an entry matches, the page object is returned, if not, False is returned. Usage is fairly simple as you will see.
class index:
	def GET(self):
		pages = db.select('page')
		return render.index(pages)
This class selects all entries from the table "page" then passes it along to web.py's built in Templetor template engine. If you know Python, you will easily learn Templetor. Inside of a folder called "templates" is a file called index.html:
$def with (pages)


The def with command declares which variables the template will use. Variables begin with a php-esque dollar sign, as do lines of Python code. Look at the for loop. Also notice objectname.attribute (tablename.tablecolumn) notation--very useful. return render.templatename(variables) is a lot more elegant, in my opinion, than Django's render_to_response function. Also, Templetor seems to be a lot quicker to write with as you don't have to type a million brackets (eg, ).
class view_page:
	def GET(self, name):
		if get_page(name) != False:
			page = get_page(name)
			return render.view(page)
		else:
			return render.create(name)

class create_page:
	def GET(self, name):
		if get_page(name) != False:
			raise web.seeother('/%s' % name)
		else:
			db.insert('page', page_title=name, page_content="")
			raise web.seeother('/edit/%s' % name)

class edit_page:
	def GET(self, name):
		if get_page(name) != False:
			page = get_page(name)
			f = edit_form()
			f.get('content').value=page.page_content
			f.get('content').attrs['class'] = 'content'
			f.get('id').value=page.page_id
			return render.edit(f, page)
		else:
			raise web.seeother('/%s' % name)

	def POST(self, name):
		f = edit_form()
		if not f.validates():
			return render.edit(f)
		else:
			i = web.input()
			db.update('page', where="page_id = %s" % i.id, page_content = i.content )
			page = get_page(name)
			raise web.seeother('/%s' % page.page_title)

if __name__ == "__main__": app.run()

Mimicking something forces you to really think about what it is that you're mimicking. A wiki is a set of functions that creates, edits, and views pages of content. view_page uses the get_page function to return a page object. If such an object exists, it passes it to the view.html template:
$def with (page)

$page.page_title

$page.page_content edit
If the page doesn't exist, it passes the page title to the create.html template:
$def with (name)

Page $name does not exist

create?
Next we have create_page. If the page exists, it redirect the user to that page using web.seeother. This redirect is very convenient. If the page doesn't exist, it creates one with the given title and blank content. Then, using web.seeother, the user is redirected to a page in which they can edit the page they just created. edit_page is a bit more exciting as it shows how to use forms, as well as retrieve POST data. With Django, to process POST data, you need to begin your view with a line similar to if request.method == 'POST':. With web.py simply create two functions within a page class: GET(self) and POST(self). The GET function will be used to send the form to the template edit.html:
$def with (form, page)

Editing $page.page_title

$:form.render()
$:form.render() is very similar to Django's form rendering function. One thing web.py seems to be missing is the ability to choose how to render the form. With web.py it will always render as a table. Django lets you render as a series of p tags. Notice the three lines that start with f.get. These allow you to set default attributes for the form input fields. By not setting the "action" attribute in the form, it is automatically set to itself. If edit_page detects POST data, it runs the post function using web.input() to retrieve the data. And here is the full code:
#code.py
import web
from web import form
import sqlite3

render = web.template.render('templates/')

db = web.database(dbn='sqlite', db='database.sqlite')

urls = (
        '/', 'index',
        '/create/([a-zA-Z0-9_]+)', 'create_page',
        '/([a-zA-Z0-9_]+)', 'view_page',
        '/edit/([a-zA-Z0-9_]+)', 'edit_page'
)

app = web.application(urls, globals())

edit_form = form.Form(
	form.Textarea("content", description=""),
	form.Hidden("id", description=""),
	form.Button("save", type="submit", description="")
)

def get_page(name):
	myvar = dict(page_title="%s" % name)
	pages = db.select('page', myvar, where="page_title = $page_title")
	v = []
	for page in pages:
		v.append(page)
	if v != []:
		return page
	else:
		return False 

class index:
	def GET(self):
		pages = db.select('page')
		return render.index(pages)

class create_page:
	def GET(self, name):
		if get_page(name) != False:
			raise web.seeother('/%s' % name)
		else:
			db.insert('page', page_title=name, page_content="")
			raise web.seeother('/edit/%s' % name)

class view_page:
	def GET(self, name):
		if get_page(name) != False:
			page = get_page(name)
			return render.view(page)
		else:
			return render.create(name)

class edit_page:
	def GET(self, name):
		if get_page(name) != False:
			page = get_page(name)
			f = edit_form()
			f.get('content').value=page.page_content
			f.get('content').attrs['class'] = 'content'
			f.get('id').value=page.page_id
			return render.edit(f, page)
		else:
			raise web.seeother('/%s' % name)

	def POST(self, name):
		f = edit_form()
		if not f.validates():
			return render.edit(f)
		else:
			i = web.input()
			db.update('page', where="page_id = %s" % i.id, page_content = i.content )
			page = get_page(name)
			raise web.seeother('/%s' % page.page_title)

if __name__ == "__main__": app.run()

#index.html
$def with (pages)


#view.html
$def with (page)

$page.page_title

$page.page_content edit #edit.html $def with (form, page)

Editing $page.page_title

$:form.render()
#create.html $def with (name)

Page $name does not exist

create?
Further reading: UPDATE: The syntax highlighter is messing with some of the template files so here is the dpaste version of my code: http://dpaste.com/hold/102787/
Alan 27 December 2008