In the previous post I've showed you how to create the development environment, and this time we'll create an editting page for our blog.
This page can either create a new post or edit an existing post. Let's name it "compose".
To make life easier, I will not focus on the styling of our blog, so I'll just use the default master template that quickstart has generated for us. First, go to templates folder, and open master.kid. Turbogrears uses a templating system called kid, which is very well designed and easy to use. The master.kid acts like a framework of all the pages(of course you can choose not to inherit from it too), which includes a common header and a common footer, etc. We'll keep the header, content and footer, and remove all the unnecessary parts in the body tag like this(you can skip this part if you are happy with them):
<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'" py:attrs="item.items()">
<div id="header"> </div>
<div id="main_content">
<div py:replace="[item.text]+item[:]">page content</div>
</div>
<div id="footer">
<img src="${tg.url('/static/images/under_the_hood_blue.png')}"
alt="TurboGears under the hood" />
<p>TurboGears is a open source front-to-back web development framework
written in Python</p>
<p>Copyright © 2007 Kevin Dangoor</p>
</div>
</body>
Much better. The next step is to create a compose page of our own. In the templates directory, rename welcome.kid to compose.kid, then open it. First, delete the unnecessary parts like this:
<!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" xmlns:py="http://purl.org/kid/ns#"
py:extends="'master.kid'">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
<title>New post - Stone's Blog</title>
</head>
<body>
</body>
</html>
Notice 'py:extends="'master.kid'"' indicates this page inherits from the master.kid, which means the header and footer and other stuff will be carried on to our compose page.
Then open controller.py, delete the unnecessary parts like this:
import turbogears as tg
from turbogears import controllers, expose, redirect, paginate
from blog.model import *
class Root(controllers.RootController):
then we are ready to write the server side code for our compose page, which is defined in a function called compose. This function will be called when we visit http://localhost:8080/compose.
@expose(template="blog.templates.compose")
def compose(self, bid=None, **kw):
if bid:
blog = Blog.get(int(bid))
else:
blog = None
return dict(blog=blog,categories=list(Category.select()))
@expose is a special kind of function in python called the decorator. In this decorator, we specified a parameter called templates, indicates the template we are going to use. compose takes 1 or no parameter. bid is the id of the post we'll edit, and if it is not provided, we are creating a new one. We have to assign a category to our post when editing, so we need all the categories too. Finally, we return this blog object and all the categories back to our template file - compose.kid.
<body>
<h1 py:if="blog">Edit Post</h1>
<h1 py:if="not blog">Compose</h1>
<form action="/saveblog" method="post">
<input py:if="blog" type="hidden" name="bid" value="${blog.id}"/>
<div>Subject: <input name="subject" value="${blog.subject if blog else None}"/></div>
<div>Author: <input name="author" value="${blog.author if blog else None}"/></div>
<div>Category:
<select name="category">
<option py:for="c in categories" value="${c.id}" selected="${'selected' if blog and blog.category==c else None}">${c.name}</option>
</select>
</div>
<div>Content:</div>
<div><textarea name="content" cols="50" rows="10">${blog.content if blog else None}</textarea></div>
<div><input type="submit" value="Submit"/></div>
</form>
</body>
Let's walk through this line by line.
<h1 py:if="blog">Edit Post</h1>
<h1 py:if="not blog">Compose</h1>
this indicates, if the blog is None, then the page title will be Compose, meaning that we are creating a new post; else page name will be edit post, meaning that we are editing an existing post. The whole tag will not be created if the expression in py:if is not true.
<input py:if="blog" type="hidden" name="bid" value="${blog.id}"/>
if we are editing a post, we need to carry the id of our post on or there's no way we'll should update the changes to an existing post instead of creating a new one when we save it.
Subject: <input name="subject" value="${blog.subject if blog else None}"/>
this indicates the subject textbox will show the blog's subject if we are editing it, or will show nothing if we are creating a new post. Notice that if None is the value in ${}, then the whole value attribute will not be created. This is a very neat design because it won't leave a 'value=""' or 'value=' in the html generated, which could cause some trouble if we are dealing with attributes like 'selected' and 'checked', because if something like 'selected=""' is generated, the browser will regard it as the selected item just like any other items with 'selected="selected"' in their attributes, and this is not what is desired.
<select name="category">
<option py:for="c in categories" value="${c.id}" selected="${'selected' if blog and blog.category==c else None}">${c.name}</option>
</select>
notice the py:for in the option. This means the kid template will generate len(categories) option tags, each with its category's id assigned to the value attribute, and a display text of the category's name. The generated code may be something like this:
<select name="category">
<option value="0"> Default </option>
<option value="1" selected="selected"> Web development </option>
<option value="2"> Funny </option>
</select>
Look how simple it is to generate a dropdown list with the kid template. Be ware that the kid template is a xml document so we cannot offer to generate code like: <option selected>Funny</option>.
And now we are ready to see what we've got here. Save the files and start the web server. Navigate to http://localhost:8080/compose, we should be able to see the compose page now:

Cool! Now we'll have to define a function that can save the post. Remember the action attribute of our form in the compose page? Yes, we are going to create a function called 'saveblog'.
@expose()
def saveblog(self,bid=None,subject=None,author=None,category=None,content=None,**kw):
if category: #category is selected
cate = Category.get(int(category))
else: #category is not provided
if Category.selectBy(name="Default").count()==0: #if Default category does not exist
cate = Category(name="Default") #then create it
else: #Default category exists
cate = Category.selectBy(name="Default").getOne() #get the Default category
if bid: #editing an existing post
blog = Blog.get(int(bid)) #get the post
blog.set(subject=tostr(subject),author=tostr(author),category=cate,content=tostr(content)) #update
else:
blog = Blog(subject=tostr(subject),author=tostr(author),category=cate,content=tostr(content)) #create a new post
raise redirect('/viewpost',bid=blog.id)
notice that we have a "tostr" function here(defined outside the Root class):
def tostr(s):
if type(s)==unicode:
return s.encode('utf8','ignore')
else:
return s
This function is designed to handle unicode characters.
I've left a comment behind every line of saveblog, which I think is already very self-explanatory. The only thing I want to point out is that we did not pass any parameters to the expose decorator, because there's no page to show. What this function does is to save the blog and then guide you the page where you can read it. After adding these 2 functions to your controllers.py, and hit the submit button on the compose page, you'll probably get a 404 error. That is because we haven't got the viewpost funcion ready yet. We'll create this page later.