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`_.
.. _Jakarta Turbine: http://jakarta.apache.org/turbine/turbine-2/
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.
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]
WKServer localhost 8087
SetHandler webkit-handler
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.
.. _documentation: http://webware.sourceforge.net/Webware-0.7/MiddleKit/Docs/index.html
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 UserRoles | 0| | | |
| | preferences | list of UserPreferences | 0| | | |
| Role | | | | | | |
| | name | string | 1| 1| 100| |
| | permissions | list of 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" %>
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('''
''' + self.title() + '''
< body>''')
self.writeHTMLBody()
self.writeln('''