Model Two plus One

Introduction

The Model2+1 design is a bit like the ModelViewController paradigm, although maybe a bit better suited for web design.

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:

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/
| |- ...

Section from apache.conf to handle WebKit:

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>

Security classes

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.

Classes.csv

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|          |

Samples.csv

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|

Settings.config

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.

User.py

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

SecureMixIn.py

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",]

UserFactory.py

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]

SiteContext/navigation/nav.psp

This is a simple navigation bar for our site. It displays the users username, and provides a way to log out:

<%-- topbar.psp --%>
<%@ page indentType="braces" %>
<table>
       <tr>
                <td>Site navigation&nbsp;|</td>
                <% if self.session().hasValue('username'): {%>
                <td>User: <%= self.session().value('username') %>&nbsp;|</td>
                <td><a href="/action/Logout">Logout</a>
                <% } else: { %>
                <td>Not logged in</td>
                <% } %>
       </tr>
</table>

SiteContext/layout/BaseLayout.py

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()

SiteContext/layout/SiteLayout.py

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()

SiteContext/Main.psp

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!

SiteContext/login.psp

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:&nbsp;</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:&nbsp;</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>

SiteContext/action/Login.py

This is the action we use to handle logging the user in. This action should do the following:

# 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")

SiteContext/action/Logout.py

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")

Conclusion

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

Changelog

01/08/2003 6:53 pm: Login.py now returns error data back to login.psp

-- LukeHolden