The Model2+1 design is a bit like the ModelViewController paradigm, although maybe a bit better suited for web design.
The idea, is to seperate display logic (pulling data from a db... etc), display and control logic (inserting data into a db). Luckly Webware makes this very easy to do.
My approach to the Model2+1 paradigm is based on my experience with Jakarta Turbine.
This approach features:
Site logic: You provide almost all of your logic in utility classes. For example, you might have a UserUtils class that handles things like authentication. Using MiddleKit derived classes to access a database is a very good example of site logic.
View: This is where all your front end design goes. Almost NO logic should exist in your view except the min required to display data from a logic class.
Actions: Actions are the reverse of Views. The action will never directly display anything to the user. They handle things like forms where an action has to be performed. For example... Say you have an authentication form. When the user submits their login data, the data is sent directly to your login action. If the login action found errors in the submited data, it would return the user to the login form. If the action found no errors, it would perform authentication via your Auth/UserUtils class, then redirect the user to the next page.
I am going to demonstrate using this approach, by showing you a simple WebKit site that uses a basic form of authentication. The code included on this page might not be perfect. I just wanted to provide a working example of how to use this model =)
PLEASE NOTE:
This example is based on a copy of Webware from CVS as of 01/07/2002. You might want to use a current version of Webware form CVS if wish to use the examples provided. In addition to this, the version I am using requires the following patch to let the Security mixin work right with PSP. This patch has been commited to CVS, so if your CVS tree is newer than 01/07/2002 you should not need the patch.
--- ParseEventHandler.py 2002-10-24 15:24:18.000000000 -0700 +++ ParseEventHandler_lholden.py 2003-01-07 20:26:22.000000000 -0800 @@ -328,11 +328,19 @@ self._writer.println() if not AwakeCreated: self._writer.println('def awake(self,trans):') self._writer.pushIndent() - self._writer.println('self.__class__.__bases__[0]'+'.awake(self, trans)\n') +## self._writer.println('self.__class__.__bases__[0]'+'.awake(self, trans)\n') + self._writer.println('for baseclass in self.__class__.__bases__:') + self._writer.pushIndent() + self._writer.println('if hasattr(baseclass, "awake"):') + self._writer.pushIndent() + self._writer.println('baseclass.awake(self, trans)') + self._writer.println('break\n') + self._writer.popIndent() + self._writer.popIndent() ##commented out for new awake version per conversation w/ chuck ## self._writer.println('if "init" in dir(self) and type(self.init) == type(self.__init__):\n') ## self._writer.pushIndent() ## self._writer.println('self.init()\n') ## self._writer.popIndent()
Here is an example of what the directory structure of this site looks like:
+ public_html/ |-+ SiteContext/ | |-+ action/ | | |- Login.py | | |- Logout.py | |-+ layout/ | | |- BaseLayout.py | | |- SiteLayout.py | |-+ navigation/ | | |- nav.psp | |- login.psp | |- Main.psp |-+ lib/ | |-+ Security/ | | |-+ GeneratedPy/ | | | |- ... | | |-+ GeneratedSQL/ | | | |- ... | | |-+ Security.mkmodel/ | | | |- Classes.csv | | | |- Samples.csv | | | |- Settings.config | | |- ... | | |- SecureMixIn.py | | |- User.py | | |- UserFactory.py | | |- ... |-+ WebKit/ | |-+ Cache/ | |-+ Configs/ | |- ...
SiteContext/action/: This is where specific actions are stored. For example, actions for dealing with a specific form.
SiteContext/layout/: Simple classes which contain the basic layout(s) of the site.
SiteContext/navigation/: This is where you have the bits and peices that make up a large portion of the design. Most of your navigations links/etc will go here.
Main.psp, login.psp, ...: These files hold the design portion of the sites pages. Very little logic should exist inside these pages. Instead, call logic from other classes and/or use Action logic to handle working with data.
lib/: This is where most of your site logic goes. Most of the modules in this directory will probably have Util in their name.
WebKit/: this is a directory I created with Webware's MakeAppWorkDir command. It contains some of the WebKits Configs, logs and sessions for this specific site.
Note that I am using mod_rewrite (see mod_rewrite recipes). This allows me to have apache serve up static content in addition to using WebKit:
Alias /ext /home/alterself/public_html/ExternalContext RewriteRule ^/ext(.*) - [L] RewriteRule ^/ExternalContext(.*) /ext$1 [R] RewriteRule ^/wk/(.*) /$1 [R] RewriteRule ^/(.*) /wk/$1 [L,PT] <Location /wk> WKServer localhost 8087 SetHandler webkit-handler </Location>
First we will start by setting up our Security system. We are going to use MiddleKit to help us do this. To start, you should Create the directories:
lib/Security lib/Security/Security.mkmodel
In the Security.mkmodel directory, we need to define our specification files. These are in comma-separated value format and you can use your favorite spread sheet program to edit them. I personally use OpenOffice. See the MiddleKit documentation for more details.
This contains our model definition:
| *Class* | *Attribute* | *Type* | *isRequired* | *Min* | *Max* | *Extras* | | User | | | | | | | | | name | string | 1| 1| 100| | | | passwd | string | 1| 1| 100| | | | roles | list of <nop>UserRoles | 0| | | | | | preferences | list of <nop>UserPreferences | 0| | | | | Role | | | | | | | | | name | string | 1| 1| 100| | | | permissions | list of <nop>RolePermissions | 0| | | | | | info | string | 0| 1| 255| | | Permission | | | | | | | | | name | string | 1| 1| 100| | | | info | string | 0| 1| 255| | | Preference | | | | | | | | | name | string | 1| 1| 100| | | | info | string | 0| 1| 255| | | RolePermissions | | | | | | | | | role | Role | 1| | | | | | detail | Permission | 1| | | | | UserRoles | | | | | | | | | user | User | 1| | | | | | detail | Role | 1| | | | | UserPreferences | | | | | | | | | user | User | 1| | | | | | detail | Preference | 1| | | | | | data | string | 1| 1| 255| |
This contains example data for use with our model:
| *User objects* | | | *name* | *password* | | guest | guest | | root | testpass | | | | | *Role objects* | | | *name* | *info* | | guest | | | user | Registered user | | | | | *name* | *info* | | registered | user is registered | | | | | *RolePermissions objects* | | | *role* | *detail* | | 2| 1| | | | | *UserRoles objects* | | | *user* | *detail* | | 2| 2|
This is used to set some settings in reguard to our model:
{ 'Package': 'Security', #'SQLLog': { 'File': 'Auth-sql.log' }, }
Once you have these files setup... you will need to generate the python and SQL source. You can do this by typing:
python (PATH-TO-WEBWARE)/MiddleKit/Design/Generate.py Security
Once this is done... you should have a bunch of files sitting in your Security, Security/GeneratedPy and Security/GeneratedSQL directories.
Now would be a good time to setup the database with the tables needed for our security system... This can be done by doing the fillowing:
cd lib/Security/GeneratedSQL mysql -u root -p < Create.sql mysql -u root -p < InsertSamples.sql
If there where no errors, things should be going well.
We have three steps left before the security system is done... Edit User.py and create the SecureMixIn and UserFactory classes. All of these files live in the lib/Security directory.
We are going to add a method to this class so we can check if a user has the permissions we need:
# User.py from GeneratedPy.GenUser import GenUser class User(GenUser): def __init__(self): GenUser.__init__(self) def hasPermissions(self, reqPermissionNames): permissionCount = 0 reqPermissionCount = len(reqPermissionNames) for role in self.roles(): for permission in role.detail().permissions(): for reqPermissionName in reqPermissionNames: if reqPermissionName == permission.detail().name(): permissionCount += 1 if reqPermissionCount == permissionCount: return True else: return False
This class is used to add security features to a page. You will see later how this is mixed into the PSP pages.
Note that the page can overwrite the reqPermissions method to require any specific permissions needed:
# SecureMixIn.py from User import User from UserFactory import UserFactory class SecureMixIn: def preLayer(self): auth = False if self.session().isExpired(): # Message for login screen about expired session here pass else: if self.session().hasValue('username'): username = self.session().value('username') userfactory = UserFactory() user = userfactory.userFromName("root") if user.hasPermissions(self.reqPermissions()): auth = True if not auth: self.forward("/login") pass def reqPermissions(self): return ["registered",]
The UserFactory is used to create new User objects from a username. It would be best to have this as a singleton class... but I will leave that up to the user =)
NOTE: You should replace (USER) and (PASSWD) with that of a user which has proper access to the Security table we added earlier.
# UserFactory.py from MiddleKit.Run.MySQLObjectStore import MySQLObjectStore class UserFactory: def __init__(self): self.store = MySQLObjectStore(user='(USER)', passwd='(PASSWD)') self.store.readModelFileNamed('../lib/Security/Security') def userFromName(self, username): userResults = self.store.fetchObjectsOfClass('User', clauses="WHERE name = '"+ username +"'") if len(userResults) < 1: return 0 else: return userResults[0]
This is the core layout class. layout Functionality generic to all the layout classes should be put in this file:
# BaseLayout.py from WebKit.Page import Page class BaseLayout(Page): def awake(self, transaction): Page.awake(self, transaction) def preLayer(self): pass def postLayer(self): pass def writeHTML(self): self.preLayer() # There is a space in the body tag because for some reason # without it, TWiki or Mozilla messes up how the Model2+1 page # is rendered. You are welcome to remove this space of course =) self.writeln(''' <html> <head> <title>''' + self.title() + '''</title> </head> < body>''') self.writeHTMLBody() self.writeln(''' </body> </html>''') self.postLayer() def writeScreen(self): """ Write screen contents inside the page""" self.writeln('<h1>No page content defined</h1>') def writeHTMLBody(self): self.writeScreen()
This is the main layout class for the site. We use this to include all of our navigation components:
# SiteLayout.py from BaseLayout import * class SiteLayout(BaseLayout): def writeHTMLBody(self): self.includeURL("navigation/nav") self.writeScreen()
The home page for the site. This page requires the user to be authenticated before it can be displayed. Notice how the SecureMixIn class is the first class to be extended. We need to do this so it can overwrite the preLayout method from SiteLayout:
<%-- Main.psp --%> <%@ page method = "writeScreen" %> <%@ page imports = "layout:SiteLayout:SiteLayout, Security.SecureMixIn:SecureMixIn" %> <%@ page extends = "SecureMixIn,SiteLayout" %> <%@ page indentType="braces" %> <psp:method name="title">return "Test page"</psp:method> Great! We are authorized!
This is our login page:
<%-- login.psp --%> <%@ page imports = "action.Login:Login" %> <%@ page indentType="braces" %> <% loginForm = self.request().arg('loginForm', Login.formTmpl()) %> <% if self.session().hasValue('username'): { %> Username already exists: <%= self.session().value('username') %><br><br> <% } %> Please login <br> <form method="post" action="/action/Login"> <table> <tr> <td valign="top">username: </td> <td valign="top"><%if loginForm['username']['error']:{%><%=loginForm['username']['error']%><br><%}%> <input type="text" name="username" value="<%= loginForm['username']['data']%>"></td> </tr> <tr> <td valign="top">password: </td> <td valign="top"><%if loginForm['passwd']['error']:{%><%=loginForm['passwd']['error']%><br><%}%> <input type="text" name="passwd" value="<%= loginForm['passwd']['data']%>"></td> </tr> </table> <input name="_action_login" type="submit" value="Login"><br> </form>
This is the action we use to handle logging the user in. This action should do the following:
Make sure the form data is valid
Make sure the requested user is valid
Log the user in
If the data/user is not valid, go back to login page.
# Login.py from WebKit.Page import Page from Security.UserFactory import UserFactory import string class Login(Page): """ This class handles login requests """ def formTmpl(): return {'username':{'data':'', 'error':''}, 'passwd': {'data':'', 'error':''}, 'valid': True} formTmpl=staticmethod(formTmpl) def actions(self): return ['login',] def isValidStr(self, string): return string != "" def isValidUsername(self, username): return self.isValidStr(username) def isValidPasswd(self, passwd): return self.isValidStr(passwd) def login(self): form = self.formTmpl() req = self.request() ses = self.session() acceptLogin = False # Populate our form dictionary with data if req.hasField('username'): form['username']['data'] = string.strip(req.field('username')) if req.hasField('passwd'): form['passwd']['data'] = string.strip(req.field('passwd')) # Validate the contents of our form dictionary if not self.isValidUsername(form['username']['data']): form['valid'] = False form['username']['error'] = "Not a valid username" if not self.isValidPasswd(form['passwd']['data']): form['valid'] = False form['passwd']['error'] = "Not a valid password" # If the contents is valid, perform login if form['valid']: user = UserFactory().userFromName(form['username']['data']) if user: if user.passwd() == form['passwd']['data']: acceptLogin = True else: form['passwd']['error'] = "Wrong password" else: form['username']['error'] = "Unknown username" if acceptLogin: ses.setValue('username', username) self.forward("../test") else: req.setArg('loginForm', form) self.forward("../login")
And finally... the logout action. This action is a lot simpler because we do not care about form data:
# Logout.py from WebKit.Page import Page class Logout(Page): def writeHTML(self): self.session().invalidate() self.forward("../test")
By now you should have a basic site using the Model2+1 paradigm. Although it is not perfect, you should have an idea on how to use this design within your own site.
More information on the Model2+1 paradigm can be found on the Jakarta Turbine website.
If you have any questions reguarding any of the information here, just shoot me an email =)
-- LukeHolden - 07 Jan 2003
01/08/2003 6:53 pm: Login.py now returns error data back to login.psp
-- LukeHolden