Building a Web Service with pso


Version: $Revision: 1.7 $
Author: thanos vassilakis
Document URL: http://sourceforge.net/docman/display_doc.php?docid=11561&group_id=49265
Contact: thanos@businessfour.com
Copyright: thanos vassilakis 2001, 2002
Contributions: Ming Huang and Vadim Shaykevich.
Feed-back: pso-development@lists.sourceforge.net
$Id: pso-example.html, v 1.7 2003/01/23 19:16:02 thanos Exp $

Contents:

1. Our Aim:
2. Setting Things Up:
Download
setup.py
.htaccess
Testing
3. Building The Site
3.1 Site Structure
3.2 The Border Template
3.3 The Home Page Template
3.4 The Login Template
3.5 The Join Template
3.6 The Datamodel - Pass One
3.7 The Edit Template
3.8 Security
3.9 Relative Urls
3.10 Lists of Data
3.11 The Detail Template
3.12 Conclusion
4. Using a Database
4.1 gadfly
4.2 sqlite
4.3 mysql

 


1. Our Aim:

We would like to show how one would build a web service using pso. As an example we will build a small site. The sites requirements will be:

Although this site does nothing in particular it will illustrate the following:

2. Setting Things Up:

  1. create your project directory under your HTML root, which can be something like:
    $ mkdir ~/public_html/psotest
  2. $ cd ~/public_html/psotest/
  3. create sub-directory:
    $ mkdir templates
  4. Download and un-tar the latest pso package:
    $ wget http://pso.sourceforge.net/dist/current.tgz
    or for MS lovers: [http://pso.sourceforge.net/dist/current.exe]
    $ tar zvfx current.tgz
  5. cd into the pso directory and then install it locally into your psotest directory.
    $ cd pso-XX/ (Where XX is the pso version)
    $ python setup.py install --install-purelib=..
    
  6. In psotest/, create a .htaccess file with the following:
    #.htaccess for testing pso using CGI
    #
    # Options ExecCGI directs apache to allow cgi's to be run from this directory.
    # AddHandler cgi-script .py treat any .py file as a script
    
    Options ExecCGI Indexes FollowSymLinks
    AddHandler cgi-script .cgi 	
    SetEnv PSOServiceId MyPSOTest	
    SetEnv PSOSessionFileLoader_Path /to/where/you/want/to/store/session/data
    
  7. Now we will create a test handler and check that it works, we will call the file test.py , but create a link to it test.cgi. [We are doing this link, and setting the Options Indexes and AddHandler cgi-script.cgi for only one reason: to allow you to view and browse the source code from the web.]
    #!/usr/bin/env python 
    #
    from pso.service import ServiceHandler
    def testHandler(serviceRequest):
    	print "hello world"
    
    if __name__ == '__main__':
    	ServiceHandler().run(testHandler)
    
    Now we will set the file permissions, and do the link:
    $ chmod a+x test.py
    $ ln test.py test.cgi
    
    and try it from the command line, should give you this:
    python:~/public_html/testpso# ./test.cgi
    set-cookie: SESSION_ID=@60123.0SESSION_ID;
    content-type: text/html
    
    hello world
    python:~/public_html/testpso# 
    
    Now lets try it using a browser: http://www.yourhost.com/~yourid/testpso/test.cgi

Building The Site

Site Structure

The sites graphical design will be kept more than bassic, and it will use a static border template, with a nested dynamic context driven template. The basic template tree in our example will be:
border.html
login.html
testpso/test.cgi?action=login
join.html
testpso/test.cgi?action=join
home.html
testpso/test.cgi
list.html
testpso/test.cgi?action=list
detail.html
testpso/test.cgi?action=detail
edit.html
testpso/test.cgi?action=edit

The default template will be home.html, others will be parsed and rendered when indicated by the field action. For example: http://www.yourhost.com/~yourid/testpso/test.cgi?action=join should show the join screen, while http://www.yourhost.com/~yourid/testpso/test.cgi?action=login should show the login screen, etc.

The Border Template

This template will remain static
This template will change depending on the context

Our border template will look like this:

<html>
<body>
	<table border height=60% width=60%>
	<tr>
		<td colspan=3 align=right> 
		<!-- North --><pso pso="member:MyAccount" />  <pso pso="login:LoginLogout">Please login</pso>
		</td>
	</tr>

	<tr>
		<td>
			<!-- west -->
		</td>
		<td>
			<!-- center --> <pso pso="tags:Template" default="home" /> 
		</td>
		<td> 
			<!-- east-->

		</td>
	</tr>
	<tr>
		<td colspan=3 align=right> 
			<!-- South -->
		</td>
	</tr>
	</table>
</body>
</html>

The Home Page Template

Create a new file in templates, home.html:
<!-- home.html -->

<p align="center">
<pso pso="general:Time" >Current Server Time</pso>
</p>

Coding the Handler

Let's stop marking up the templates for a moment, and code a bit. First of all we will need to flesh out our handler, currently it just prints "hello World". What do we need?
  1. We need a handler that will load templates based on the action field value.
  2. We need these templates parsed (preferably only once) and then rendered
  3. We would like the visitor to be a member method of our handler (you will see why later).

Well it just so happened that pso has a simple ready made handler just for us:

#!/usr/bin/env python 
#
from pso.service import ServiceHandler
from pso.handlers import TemplateHandler

class TestHandler(TemplateHandler): 
	DEFAULT_TEMPLATE='border' 

if __name__ == '__main__':
	ServiceHandler().run(TestHandler().handle)

Lets try it: http://www.yourhost.com/~yourid/testpso/test.cgi

As you can see undefined tags are just ignored. It sometimes useful to enclose dummy data between tags. This helps layout the html. Lets continue with the templates.

Your First Simple Tag

So let's code the general.Time  tag used on the home page:
#file testpso/general.py
from pso.parser import Tag
import time

class Time(Tag):
	""" show the server current time"""
	
	def __call__(self, handler, cdata=''):
		return """<PRE>%s</PRE>""" % time.ctime()
Now tryout the home page: hittp://www.yourhost.com/~yourid/testpso/test.cgi. [ source ]

The Login Template

Now we will continue with our templates:
<form>
	<table border width=100%>
	<tr> <td align="right"> Login Id: </td> <td>  <input name="id">  </td> </tr>
	<tr> <td align="right"> password </td> <td>   <input name="passwd" type="password">  </td> </tr>
	<tr> <td></td><td>  <input name="login" type="submit" value=" Login ">  
	<pso pso="login:LoginForm" />
	<br><font color="red"><pso pso="login:Message" /></font>
</td> </tr> 
	</table>
</form>
Try it: http://www.yourhost.com/~yourid/testpso/test.cgi?action=login. [ source ]

First we must link this form to the home page by coding our login/logout tag:

#file: testpos/login.py
from pso.parser import Tag

class LoginLogout(Tag):
  """ if user is login in shows welcome otherwise shows link to login screen"""
  WELCOME="Welcome"
  def __call__(self, handler, cdata=''):
    action = handler.req().getInput('action','')
    if action in ('login', 'join'):
    # don't show anything in these screens
      return ''
    user = self.getLogin(handler)
    # login in ?
    if user:
      if handler.req().hasInputs('logout'):
      # does user want to logout
        self.logOut(handler)
      return """ <a href="test.cgi?logout=yes">%s""" %  "Logout"
    return """ <a href="test.cgi?action=login">%s""" % cdata

  def logIn(self, handler, id):
    " fill in later"
    handler.req().getSession()['user'] = id

  def logOut(self, handler):
    del handler.req().getSession()['user']
    handler.req().redirect('test.cgi')

  def getLogin(self, handler):
    return handler.req().getSession().get('user')

Now lets go back to the login form and add the following class to login.py:

class LoginForm(Tag):
	""" if no errors sets session with this user and redirects to home page 
	if errors sets handler's scratch with error message
	when form is needed prints hidden action field"""

	NOMATCH='wrong id or password'
	NOENTRY='please enter id and password'
	def __call__(self, handler, cdata=''):
		html =""
		if handler.req().hasInputs('login'): # check to see in login button pressed
			id = handler.req().getInput('id')
			passwd = handler.req().getInput('passwd')
			if id and passwd: 
				user =  self.checkUser(handler, id, passwd)
				if user:
					LoginLogout().logIn(handler, id )
					handler.req().redirect('test.cgi')
				else:
					handler.scratch()['message'] = self.getAttrs().get('nomatch', self.NOMATCH)
			else:
				handler.scratch()['message']  = self.getAttrs().get('noentry', self.NOENTRY)
		return """<input type="hidden" name="action" value="login">"""

	def checkUser(self, handler, id, passwd):
		" fill in later "
		return id == passwd
While we are at we might as well add the Message tag to general.py:
class Message(Tag):
	""" if message in scrap show it and erase it"""
	def __call__(self, handler, cdata=''):
		if handler.scratch().has_key('message'):
			message = handler.scratch()['message']
			del handler.scratch()['message']
			return message 
		return cdata

Notice that there is virtually no html in our code. When using pso we are always trying to keep the code independent from the graphical design. There is quite a lot going on in the above classes, maybe we would just go through them quickly:

Now we have finished with the this pretty painless login form and various other simple tags, we can try it out again, just remeber your password is your id!: http://www.yourhost.com/~yourid/testpso/test.cgi?action=login. [ source ]

The Join Template

In the real world it is very often a designer who marks up all the web services pages, and they could present you, the developer, with something like this:
<pre>
<form>
Your personal Details
	name:	<input size=40>
	email:	<input size=40>
	state:	<SELECT><option>NY<option>NJ</SELECT>
	zip:	<input size=10>

Your membership Details:
	Enter your member id:<input size=9>
	      Your Password : <input name="passwd" type="password" size=9>
	   Your confirmation: <input name="passwd" type="password" size=9>
	
Subscription Details:
Which OS do you use
	Linux: <input type="checkbox" name="os" value="PPC Linux">
	 OS X: <input type="checkbox" name="os" value="OS X">
	  Mac: <input type="checkbox" name="os" value="OS 9.2">

Would You like to be spammed: Yes <input type="radio" name="os" value="Yes"> or No <input type="radio" name="os" value="No" checked>
	
	<input type="submit" name="joinup" value="Join Up Now">
	</form>
</pre>
You could replace it with :
<pso pso="member:NewForm">
Your personal Details
	name:	<pso pso="member:Name" />
	email:	<pso pso="member:Email" />
	state:	<pso pso="member:State" />
	zip:		<pso pso="member:Zip" />

Your membership Details:
	<pso pso="memberMemberId">
	<pso pso="member:Password">
	
Subscription Details:
Which OS do you use
	<pso pso="member:Os" choice="OS X,Linux PPC"/>
	Would You like to be spamed: <pso pso="member:Spam"/>
	<pso pso="member:Join">Join Up Now</pso>  
</pso>
But you would not be very popular with the designers, once they come back to do some design changes. Ideally you would like to embed your tags without disturbing the browser look of the templates. One of the big coding design issues you will face using pso is achieving the right balance between the power and responsibility you give to each tag and the coupling of your python code with the design of the html pages. There must be a hundred ways to pso tag this html page, here are two which will still allow the designer to preview and manipulate the templates:
	name:	<pso pso="member:Name" size=40><input size=40></pso>
	name:	<input pso="member:Name" size=40 />
The second way looks a lot neater, but has the drawback that templates written this way are currently a lot slower to parse. Now we will move the pso  to  c  Real Soon, so you can develop for the future. Also if you are developing a service that will run under nsapi, mod_python, etc or for very fast platform, parsing times will not be an issue for you. For the sake of readablity we will continue with the second form of tag. Here is templates/join.html:
<pre>
<form pso="member:NewForm" enctype="multipart/form-data"  METHOD="POST">
<h4>Your personal Details:</h4>
		       name: <input pso="member:NameInput"  size=40 required=1 />
		      email: <input pso="member:EmailInput" size=40 required=1 />
		      state: <select pso="member:StateSelect"><option>NY<option>NJ</select>
	       	        zip: <input pso="member:ZipInput" size=10  name="zip" required=1 />

<h4>Your membership Details:</h4>
             Your member id: <input pso="member:MemberIdInput" size=6 /> <pso pso="member:PasswordGroup"> 
              Your Password: <input pso="member:PasswordInput" type="password" size=6 />
             Please Confirm: <input pso="member:PasswordConfirmInput" type="password" size=6 />
</pso>
	
<h4>Subscription Details:</h4>

        Which OS do you use: <pso pso="member:Os" name="os"> Linux <input pso="member:OsCheckBox" type="checkbox" value="PPC Linux" />
	OS X <input pso="member:OsCheckBox" type="checkbox" value="OS X" /> Mac <input pso="member:OsCheckBox" type="checkbox" value="OS 9.2" /> </pso>

	Would You like to be spammed: <pso pso="member:Spam" name="spam" > Yes <input pso="member:SpamRadio" type="radio" name="spam" value="Yes" />
	or No <input pso="member:SpamRadio" type="radio" name="spam" value="No" checked /> </pso>
	


Your Photo: <input pso="member:PhotoFile" type="file" />
Your message: <textarea cols="30" rows="10" pso="member:MessageTextArea"> </textarea>

	
	                    <input pso="member:Submit" type="submit"  value="Join Up Now" />
	</form>
</pre>
Now for the supporting code:

The Datamodel - Pass One

You are probably saying: "Having a join form is all very, but where goes the data I'm entering ?" The answer is: "In the datamodel". DataModel is an abstraction around the data that your portal will process pnd represented. Now our simple site has members.
class Member We need to create them:
aMember = Member(id, some data)
and insert them:
aMember.insert()
retrieve a members:
aMember = Member.load(id)
change their details:
aMember.update(some data)
and then save:
aMember.save()
To do things quick and keep things simple, we will also abstract the filing mechanism so for now we will just dump the data in falt files, but later connect to a database without changing more than a line of code.
# --- datamodel.py ---


from pickle import load, dump
from os.path import join

class Loader:
        PATH = '/tmp'
        def load(self, id):
                return load(file(join(self.PATH, id)))
        def save(self, id, obj):
                dump(obj, file(join(self.PATH, id), 'w'))


class Member(dict):
        IDKEY='memberId'
        LOADER = Loader()
        def load(classObj, id, loader=None):
                if loader is None:
                        loader = classObj.LOADER
                try:
                        member =  loader.load(id)
                except IOError:
                        member =  classObj()
                        member[classObj.IDKEY] = id
                return member
        load = classmethod(load)

        def __init__(self, **kwa):
                dict.__init__(self, kwa)

        def save(self, loader = None):
                if loader is None:
                        loader = self.LOADER
                loader.save(self[self.IDKEY], self)

if __name__ =='__main__':
	import sys
	id = sys.argv[1]
        member = Member.load(id)
        print member
        member['name']= 'Bond'
        member.save()
        secondComming = Member.load(id)
        print secondComming

As you can see it handles the storage and retrieval of records and knows nothing about pso. Try it out:

-bash-2.05b$ python datamodel.py 007
{'id': '007'}
{'id': '007', 'name': 'Bond'}
-bash-2.05b$

Now we will use the datamodel:

  1. Import the datamodel into the member module, then use it in NewForm
    
    #module: member.py
    #
    from pso import parser, form, fields, tags
    
    import login
    import datamodel
    class NewForm(form.Form):
            def goNext(self, handler, cdata):
                    handler.req().redirect('test.cgi')
    
            def submit(self, handler, cdata):
                    data = self.prepareData(handler)
    		member = datamodel.Member(data)
    		member.save() 
                    #while we are at it lets log the bugger in.
                    login.LoginLogout().logIn(handler, data['memberId'])
    
  2. Then rigg-up the login code.
  3. And we better change the login form to actually use our datamodel. Most programmers would go into our login.py module and hack the code to work, but with pso we don't need to do that, in fact we don't need to change any code:
    1. First just change the login template, templates/login.html form using
       <pso pso="login:LoginForm" /> 
      to using
       <pso pso="reallogin:LoginForm" /> 
    2. then we have to code reallogin.py:
      from login import LoginForm
      from datamodel import Member
      from  pso.form import PasswordEncoder
      
      class LoginForm(LoginForm):
      	def checkUser(self, handler, id, passwd):
      		user = Member.load(id)
      		if user:
      			passwd = PasswordEncoder().encode(passwd)
      			if  passwd == user['password']:
      				return user
      
  4. That's it! ~ http://www.yourhost.com/~yourid/testpso/test.cgi?action=join. [ source ]

    The Edit Template

    Our next example is to use the above template and code to allow a member to edit their records.
    • First, we will copy the templates/join.html template to templates/edit.html.
      $ cp templates/join.html templates/edit.html
      We could use the same template, but nearly always the site designer has other ideas, and anyway you will not want the member to change their id, so at least that tag has to be read-only. We will change the new template, just a bit:
      1. Change the tag
         <form pso="member:NewForm" enctype="multipart/form-data"  METHOD="POST"> 
        to
         <form pso="member:EditForm" enctype="multipart/form-data"  METHOD="POST"> 
      2. and change the tag
         <input pso="member:MemberIdInput" size=6 /> 
        to
         <input pso="member:MemberIdInput" readonly="yes" /> 
      3. And finally
         <input pso="member:Submit" type="submit"  value="Join Up Now" />
        to
         <input pso="member:Submit" type="submit"  value="Save Changes" />
    • Now, onto the code:
      1. In the module member.py let us create a new sub-class of pso.form.Form
        class NewForm(MemberForm): 
        	def submit(self, handler, cdata): 
        		data = self.prepareData(handler)
        		member = Member(**data)
        		member.save()
        		#while we are at it lets log the bugger in.
        		login.LoginLogout().logIn(handler, data['memberId'])
        
      2. Then change NewForm to
        class NewForm(MemberForm): 
        	def submit(self, handler, cdata): 
        		data = self.prepareData(handler)
        		member = Member(**data)
        		member.save()
        		#while we are at it lets log the bugger in.
        		login.LoginLogout().logIn(handler, data['memberId'])
        
      3. and add a new class to member.py
        class EditForm(MemberForm): 
        	def submit(self, handler, cdata): 
        		data = self.prepareData(handler)
        		member = Member.load(data[Member.IDKEY])
        		member.update(self.prepareData(handler))
        		member.save()	
        
      4. The last but not least job we will do is make sure that the form is seeded with data from the datamodel. pso has a simple mixin class just to help us, tags.DataMixin. We subclass it and override its getRecord and getKey methods, or the KEY class attribute. We will do this in member.py:
        class MemberMixin(tags.DataMixin):
        	KEY= Member.IDKEY
        	DATAMODEL=datamodel
        	def getRecord(self, handler, key):
        		return Member.load(key)
        
        You might say, "that looks expensive!". The key to developing in pso is to make every tag independent to each other, that way you can develop them and then just drop them into the application without changing other code. DataMixin caches your requests in the scratch. Using a caching system saves many hits to the datamodel and using scratch guarantees the data to be fresh on each http request.
        Since we have created the mixin we better use it and change all our tag classes from:
        class NameInput(form.Input):pass
        class EmailInput(fields.EmailInput):pass
        class StateSelect(fields.StateSelect):pass
        class ZipInput(fields.ZipInput):pass
        class MemberIdInput(fields.UserIdInput):pass
        class PasswordGroup(form.PasswordGroup):pass
        class PasswordInput(form.PasswordInput):pass
        class PasswordConfirmInput(form.PasswordConfirmInput):pass
        class Os(form.Options):pass
        class OsCheckBox(form.CheckBox):pass
        class Spam(form.Field):pass
        class SpamChoice(form.Field):pass
        class Submit(form.Submit):pass
        class MessageTextArea(form.TextArea):pass
        class PhotoFile(form.File): pass
        
        to
        class NameInput(form.Input, MemberMixin): pass
        class EmailInput(fields.EmailInput, MemberMixin):pass
        class StateSelect(fields.StateSelect, MemberMixin):pass
        class ZipInput(fields.ZipInput, MemberMixin):pass
        class MemberIdInput(fields.UserIdInput, MemberMixin):pass
        class PasswordGroup(form.PasswordGroup):pass
        class PasswordInput(form.PasswordInput, MemberMixin):pass
        class PasswordConfirmInput(form.PasswordConfirmInput, MemberMixin):pass
        class Os( form.Options, MemberMixin):pass
        class OsCheckBox(form.CheckBox, MemberMixin):pass
        class Spam(form.Options, MemberMixin):pass
        class SpamRadio(form.Radio, MemberMixin):pass
        class JoinUpSubmit(form.Submit):pass
        class MessageTextArea(form.TextArea, MemberMixin):pass
        class PhotoFile(form.File, MemberMixin): pass
        
      5. Now let us link it in. We will add a new tag to member,

        class MyAccount(parser.Tag): 
        	""" if user is logged in shows welcome with link to edit"""
        	WELCOME="Welcome"
        	def __call__(self, handler, cdata=''):
        		user = login.LoginLogout().getLogin(handler)
        		if user:
        			welcome =  self.getAttrs().get('welcome', self.WELCOME) 
        			return '<a href="test.cgi?action=edit&memberId=%s">%s</a>' % (user, user)
        		return ""
        
        and as you probably noticed it is already in the border.html template.

        Try It! ~ http://www.yourhost.com/~yourid/testpso/test.cgi. [ source ] We are done, well not quite.

    • I sure you are thinking "Where is the security", yes with what we have coded until now all you need to do is /test.cgi?action=edit&memberId=someId and you could edit the account. With pso this is really easy and elegant to stop:

    Security

    With pso security is simple:

    1. Just insert a new tag at the top edit.html
      <pso pso="member:Security" />
    2. Then code corresponding class in member.py
      class Security(parser.Tag):
      	def __call__(self, handler, cdata):
      		try:
      			if self.check(handler):
      				return '' # if so do nothing
      		except: # 
      			pass
      		# not authorized or some malicious exception ? bang him to the login page
      		handler.req().redirect('test.cgi?action=login')
      		
      	def check(self, handler):
      		user = login.LoginLogout().getLogin(handler)
      		# check if the user owns the record
      		if  user == handler.req().getInput('memberId'):
      			return 1 
      		return 0
      
    Yes it as easy as that! Now logout, http://www.yourhost.com/~yourid/testpso/test.cgi?logout=yes. then try and vist it the edit screen directly: http://www.yourhost.com/~yourid/testpso/test.cgi?action=edit&memberId=thanosv While we are on security, lets address a potential problem in our code.

    Relative Urls

    Up till now I have hard coded the urls, just so I could get on with other issues. A quick grep -n for "test.cgi" results in

    login.py:16:				handler.req().redirect('test.cgi')
    login.py:42:					handler.req().redirect('test.cgi')
    member.py:18:		handler.req().redirect('test.cgi')
    member.py:78:		handler.req().redirect('test.cgi?action=login')
    member.py:85:		return '<a href="test.cgi?action=edit&memberId=%s">%s</a>' % (user, user)
    
    This is horrendous! Well pso.RequestService has a few methods to solve this problem. You have already seen us use the pso.tags.CgiLink tag to generate a tags to the script. Now we will use a method od pso.request.ServiceRequest:
    login.py:16:				handler.req().redirect(handler.req().getUrl().script)
    login.py:42:					handler.req().redirect(handler.req().getUrl().script)
    member.py:18:		handler.req().redirect(handler.req().getUrl().script)
    member.py:78:		handler.req().redirect(handler.req().getUrl().uri(action='login'))
    member.py:85:		return handler.req().getUrl().aHref(user, action='edit', memberId=user) 
    
    Although this might be a lot more typing, it a few major advantages:
    1. Changes in the host address or script name, or even the info path, wont effect the working of your code.
    2. It is less type error prone.
    3. You can override these methods to add special variables, such as the session id.
    4. Finally you can encrypt your query string, and decrypt it be overriding pso.url.Url.encryptQuery, and pso.url.Url.decryptQuery, and that is why I have mentioned this topic after talking about security.

    Lists of Data

    Once someone has joined, I guess they will want to see a list of the other members. So what's involved?
    1. First you will want to hit the database for a list of members.
    2. Create a new template called templates/list.html:
      <pso pso="member:ListSecurity" />
      <table>
      <pso pso="member:List">
      <tr><td>%(memberId)s</td><td>%(state)s</td><td>%(zip)s</td></tr>
      </pso>
      </table>
      
    3. Now you will want to add a List class to your member.py, and pso.tags has a class that will do most of the work for you. Just subclass it and override pso.tags.List.fetch, and pso.tags.List.prepareRow.
      class List(tags.List, MemberMixin):
      	def fetch(self, handler, line, pageSize):
      		" must return a list of somethings"
      		cursor = self.selection(handler)
      		return cursor.fetch(line=line, pageSize= pageSize)
      		
      	def prepareRow(self, handler, record):
      		" must return a dictionary of fields that will be merged with the cdata"
      		record['uri'] = handler.req().serviceUri(clean=1, action='detail', memberId= record['memberId'])
      		record['zip']= """<a href="%s">%s</a>""" % ( handler.req().serviceUri(clean=1, action='list', zip= record['zip']), record['zip'])
      		record['state']= """<a href="%s">%s</a>""" % ( handler.req().serviceUri(clean=1, action='list', state= record['state']), record['state'])
      		record['memberId']= """<a href="%(uri)s">%(memberId)s</a>""" % record 
      		return record
      
    4. and its bouncer:
      class ListSecurity(Security):
      	def check(self, handler):
      		return login.LoginLogout().getLogin(handler)
      
    5. Finally, check it out http://www.yourhost.com/~yourid/testpso/test.cgi?action=list [ source ]

    The Detail Template

    Before we link the list template to the site we better code a details page. The simplest way is to just reuse the edit.html template, and make all the fields readonly, reshuffle them, remove the irrelavent ones and change PhotoFile to a new tag Photo:

    1. <pre>
      <pso pso="member:ListSecurity" />
      		     member: <input pso="member:MemberIdInput" readonly="yes" /> 
      
      
      		       name: <input pso="member:NameInput"  size=40 readonly=1 />
      		      email: <input pso="member:EmailInput" size=40 readonly=1 />
      		      state: <select pso="member:StateSelect"  readonly=1 ><option>NY<option>NJ</select>
      	       	        zip: <input pso="member:ZipInput" size=10  name="zip" readonly=1 />
      
      
      	message: <textarea cols="30" rows="10" pso="member:MessageTextArea" readonly=1 > </textarea>
      	<pso pso="member:Photo" />
      </pre>
      
    2. here is the Photo tag class:
      class Photo(MemberMixin, parser.Tag): 
      	def __call__(self, handler, cdata):
      		photo = self.record(handler).getPhoto()
      		if photo:
      			return """<img src="%s">""" %  photo
      		return ''
      
    3. Now we will link it into the site. We will put a little link in the west border:
      <html>
      <body>
      	<table border height=60% width=60%>
      	<tr>
      		<td colspan=3 align=right> 
      		<!-- North --> <pso pso="member:MyAccount" /> <pso pso="login:LoginLogout">Please Login</pso>
      		</td>
      	</tr>
      
      	<tr>
      		<td width=100 valign=top>
      			<!-- west -->
      			<small>
      			<p align=right>
      			<a href="test.cgi?action=list">view members</a>
      			</p>
      			</small>
      		
      		</td>
      		</td>
      		<td>
      			<!-- center --> <pso pso="tags:Template" default="home" /> 
      		</td>
      		<td> 
      			<!-- east-->
      
      		</td>
      	</tr>
      	<tr>
      		<td colspan=3 align=right> 
      			<!-- South -->
      		</td>
      	</tr>
      	</table>
      </body>
      </html>
      

      Finally our last test: http://www.yourhost.com/~yourid/testpso/test.cgi. [ source ]

Conclusion

We have infact finished the first poart of the tutorial. Of course there is much more we can do, but a basic site has been built. It included It took 258 lines of code, using 31 classes that you coded subclassing 18 pso classes. This is pretty good, esspecially when you consider the reuse rate was quite high.

-bash-2.05b$ wc *.py
  73  163 1674 datamodel.py
  20   53  484 general.py
  60  203 1931 login.py
  94  264 2856 member.py
  11   33  299 reallogin.py
 258  716 7244 total

Using A Database

In our example up we have been using a simple data model. In the real world you would probably need to connect to a real database. In this section we shall show you how to do this with the minimum of fuss. First we will define the SQL schema:

CREATE TABLE member (
	memberId varchar, 
	name varchar, 
	email varchar, 
	zip varchar, 
	id varchar, 
	password varchar, 
	os varchar, 
	message varchar, 
	spam varchar, 
	image varchar);
Not very impressive but this will do the job, next we have to subclass our datamodel.Loader:
class SQLLoader(Loader):

	FIELDS="memberId,name,email,zip,id,passowrd,os,message,spam,image"
	LOAD="SELECT * WHERE memberId='%s'"
        def load(self, id):
                members= self.fetch(critrea = LOAD % id)

        def save(self, id, obj):
		previous = self.load(id)
		if previous:
			self.update(obj)
		else:
			self.insert(obj)

	def fetch(self, line=0, pageSize=None, criteria=""):
		if pageSize is None:
			pageSize = len(self.db)
		all = self.db.items()
		all.sort()
		return [record for id, record in all[line:line+pageSize]]
CREATE TABLE `account` (
  `accountno` INT UNSIGNED NOT NULL AUTO_INCREMENT, 
  `userId` CHAR(20) NOT NULL DEFAULT '""', 
  `name` CHAR(60) NOT NULL DEFAULT '""', 
  `email` VARCHAR(100) NOT NULL DEFAULT '""', 
  `zip` CHAR(10) NOT NULL DEFAULT '""', 
  `password` CHAR(20) NOT NULL DEFAULT '""', 
  `os` ENUM('Linux','OSX','Mac') NULL, 
  `spam` BOOL NOT NULL DEFAULT 'False', 
  `image` VARCHAR(100) NOT NULL DEFAULT '""', 
  `date` TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 
  `message` VARCHAR(250) NOT NULL DEFAULT '""',
  PRIMARY KEY (`userId`),
  INDEX (`accountno`),
  UNIQUE (`email`)
)
TYPE = myisam;