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 $ |
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:
$ mkdir ~/public_html/psotest
$ cd ~/public_html/psotest/
$ mkdir templates
$ wget http://pso.sourceforge.net/dist/current.tgz or for MS lovers: [http://pso.sourceforge.net/dist/current.exe] $ tar zvfx current.tgz
$ cd pso-XX/ (Where XX is the pso version) $ python setup.py install --install-purelib=..
#.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
#!/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.cgiand 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
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.
This template will remain static
|
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>
<!-- home.html --> <p align="center"> <pso pso="general:Time" >Current Server Time</pso> </p>
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 TemplateHandlerLets 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.class TestHandler(TemplateHandler): DEFAULT_TEMPLATE='border' if __name__ == '__main__': ServiceHandler().run(TestHandler().handle )
#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 ]
<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 == passwdWhile 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:
class FrenchLogin(Login): WELCOME="Bonjour"or the designer
<!-- North --><pso pso="login:Login" welcome="User: " >Please login</pso>
handler.scratch()['message'] = self.getAttrs().get('nomatch', self.NOMATCH)
handler.req().getSession()['user'] = idand redirect to the home page
handler.req().redirect('test.cgi')
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 ]
<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 />
<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:
#module: member.py # from pso import parser, form, fields, tags import datamodel, login class NewForm(form.Form): def goNext(self, handler, cdata): handler.req().redirect('test.cgi') def submit(self, handler, cdata): data = self.prepareData(handler) #save new member's details here #while we are at it lets log the bugger in. login.LoginLogout().logIn(handler, data['memberId']) 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.Options):pass class SpamRadio(form.Radio):pass class MessageTextArea(form.TextArea):pass class PhotoFile(form.File): passYES that is all you have to do to get the form working. form.Form has a default working cycle:
# --- 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:
#module: member.py # from pso import parser, form, fields, tags import loginimport 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'])
<pso pso="login:LoginForm" />to using
<pso pso="reallogin:LoginForm" />
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
$ cp templates/join.html templates/edit.htmlWe 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:
<form pso="member:NewForm" enctype="multipart/form-data" METHOD="POST">to
<form pso="member:EditForm" enctype="multipart/form-data" METHOD="POST">
<input pso="member:MemberIdInput" size=6 />to
<input pso="member:MemberIdInput" readonly="yes" />
<input pso="member:Submit" type="submit" value="Join Up Now" />to
<input pso="member:Submit" type="submit" value="Save Changes" />
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'])
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'])
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()
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): passto
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
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:
With pso security is simple:
<pso pso="member:Security" />
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
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(Although this might be a lot more typing, it a few major advantages: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: returnhandler.req().getUrl().aHref(user, action='edit', memberId=user)
<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>
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
class ListSecurity(Security): def check(self, handler): return login.LoginLogout().getLogin(handler)
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:
<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>
class Photo(MemberMixin, parser.Tag): def __call__(self, handler, cdata): photo = self.record(handler).getPhoto() if photo: return """<img src="%s">""" % photo return ''
<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 ]
-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
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;