Part of my tangeant using zope.interface was to avoid remaking list/edit admin pages for my SQLObject objects. Very little object-specific was left, and so I've now made an EntityPage that entity list/edit pages can inherit from.
The pattern I'm using is that you can manage your objects of a certain type under /admin/people. When you go to that URI, you'll get a list of the people. When you click on one of those, you'll go to /admin/people/1 or /admin/people/nbm, depending on what you use for your path names.
from samoosa.webware import WebwarePage
from samoosa.webware.templates import *
from samoosa.user import User
from FunFormKit.Form import *
from FunFormKit.Field import *
# Can at least pretend to be portable...
try:
from sqlobject import SQLObject
except:
SQLObject = None
class EntityPage(WebwarePage, FormServlet):
contentTemplate = ListContentTemplate
userTemplate = EditContentTemplate
handleExtraPathInfo = True
# Class being edited/listed
_samoosa_class = None # Person
# How to look up the object by path
_samoosa_lookup = None # Person.byShortName
# Whether we are editing or not
_samoosa_edit = True
# How to sort results
_samoosa_sort = 'id' # 'display_name'
# Anything else about the object to show in list is here...
_samoosa_extra = None # 'shortName'
# Are we the admin or public version?
_samoosa_adminView = False
def __init__(self):
formDef = self.createForm()
FormServlet.__init__(self, formDef)
WebwarePage.__init__(self)
def createForm(self):
fields = self._samoosa_class.ffkFields()
fields.append(SubmitButton('submit', description='Save'))
formDef = FormDefinition('', # the form action
fields, name='form')
return formDef
def filterData(self, data):
return data
def content_template(self):
username = self.session().value('authenticated_user_admin', None)
admin = False
if username:
admin = User.byUsername(username).admin
if self._samoosa_adminView:
path = self._samoosa_class._samoosa_adminView
else:
path = self._samoosa_class._samoosa_publicView
if not self.request().extraURLPath() or self.request().extraURLPath() == "/":
mySearchList = {
'admin': admin,
'path': path,
}
return self.contentTemplate(searchList=[mySearchList, self])
pathName = self.request().extraURLPath()
while pathName[0] == '/':
pathName = pathName[1:]
while pathName[-1] == '/':
pathName = pathName[:-1]
try:
item = self._samoosa_lookup(pathName)
except:
return self.sendRedirectAndEnd(self._samoosa_class._samoosa_publicView)
submitted, data = self.processForm()
if str(data) == data:
data = {}
data = self.filterData(data)
if submitted:
item.set(**data)
newPath = item._samoosa_getPathName()
if pathName != newPath:
return self.sendRedirectAndEnd('%s/%s' % (path, newPath))
defaults = {}
for column in item._columns:
name = column.kw['name']
value = getattr(item, name)
if value is not None and isinstance(value, SQLObject):
value = value.id
defaults[name] = value
df = self.renderableForm(defaults = defaults)
dl = df.htFormTable()
mySearchList = {
'item': item,
'dl': dl,
'admin': admin,
'path': path,
}
return self.userTemplate(searchList=[mySearchList, self])
This allows you to write a pretty simple context page:
from samoosa.webware import EntityPage
from samoosa.person import Person
class people(EntityPage):
_samoosa_class = Person
_samoosa_lookup = Person.byShortName
_samoosa_edit = True
_samoosa_singular = 'person'
_samoosa_plural = 'people'
_samoosa_sort = 'display_name'
_samoosa_extra = 'shortName'
require_login = True
require_admin = True
Actually, that's before I remove some OBEd attributes.
The Person class now has a bit more intelligence:
from sitesqlobject import *
from sql import connection
class Person(SiteSQLObject):
_connection = connection
shortName = StringCol(length=9, alternateID=True)
displayName = StringCol(length=100, alternateID=True)
webpageUrl = StringCol(length=255, default = None)
blogUrl = StringCol(length=255, default = None)
tagLine = StringCol(length=255, default = None)
summary = StringCol(default = None)
# Samoosa-specific hackery
_samoosa_columnsInOrder = ['shortName', 'displayName', 'webpageUrl', 'blogUrl', 'tagLine', 'summary']
_samoosa_displayName = 'displayName'
_samoosa_pathName = 'shortName'
_samoosa_lookup = 'byShortName'
_samoosa_publicView = '/people'
_samoosa_adminView = '/admin/people'
_samoosa_createPath = '/admin/newPerson'
_samoosa_singular = "person"
_samoosa_plural = "people"
Person.createTable(ifNotExists = True)
if Person.select().count() == 0:
p = Person(shortName="nbm", displayName="Neil Blakey-Milner",
webpageUrl="http://mithrandr.moria.org/",
blogUrl="http://mithrandr.moria.org/")
I've expanded SiteSQLObject a bit to simplify and future-proof some stuff:
from sqlobject import *
from sql import connection as __connection__
from zope.interface.adapter import AdapterRegistry
from zope.interface import classImplements, implements, Interface
registry = AdapterRegistry()
class SiteSQLObject(SQLObject):
_samoosa_columnsInOrder = []
_samoosa_displayName = 'id'
_samoosa_pathName = 'id'
_samoosa_publicView = None
_samoosa_adminView = None
_samoosa_lookup = 'get'
def ffkFields(cls):
ret = []
columnDict = {}
for col in cls._columns:
columnDict[col.kw['name']] = col
for colName in cls._samoosa_columnsInOrder:
col = columnDict[colName]
col.form = cls
display = registry.queryAdapter(col, IColumnDisplay, '')
ret.append(display.getField())
return ret
ffkFields = classmethod(ffkFields)
def _samoosa_getDisplayName(self):
return getattr(self, self._samoosa_displayName)
def _samoosa_getPathName(self):
return getattr(self, self._samoosa_pathName)
def _samoosa_getHTMLDisplayName(self):
if not self._samoosa_publicView:
return self._samoosa_getDisplayName()
path = '%s/%s' % (self._samoosa_publicView, self._samoosa_getPathName())
return '<a href="%s">%s</a>' % (path, self._samoosa_getDisplayName())
def _samoosa_pathnameLookup(self):
return getattr(self, self._samoosa_lookup)
class ICol(Interface): pass
class IBoolCol(Interface): pass
class IStringCol(Interface): pass
class IFKCol(Interface): pass
class IColumnDisplay(Interface): pass
from FunFormKit.Form import *
from FunFormKit.Field import *
class ColumnDisplay:
implements(IColumnDisplay)
def __init__(self, context):
print "DefaultColumnDisplay: %s" % (context)
self.context = context
class DefaultColumnDisplay(ColumnDisplay):
def getField(self):
return TextField(self.context.kw['name'], size=20, maxLength=100)
class BoolColumnDisplay(ColumnDisplay):
def getField(self):
return CheckboxField(self.context.kw['name'])
class StringColumnDisplay(ColumnDisplay):
def getField(self):
length = self.context.kw.get('length', None)
if not length:
return TextareaField(self.context.kw['name'], rows=10, cols=40, wrap="SOFT")
size = 20
if length < size:
size = length
return TextField(self.context.kw['name'], size=size, maxLength=length)
class FKColumnDisplay(ColumnDisplay):
def getField(self):
clsname = self.context.kw['foreignKey']
cls = getattr(self.context.form, "_SO_class_%s" % (clsname))
print dir(cls)
selections = []
for o in cls.select():
selections.append((o.id, o._samoosa_getDisplayName()))
return SelectField(self.context.kw['name'], selections = selections)
classImplements(Col, ICol)
classImplements(BoolCol, IBoolCol)
classImplements(StringCol, IStringCol)
classImplements(ForeignKey, IFKCol)
registry.register([ICol], IColumnDisplay, '', DefaultColumnDisplay)
registry.register([IBoolCol], IColumnDisplay, '', BoolColumnDisplay)
registry.register([IStringCol], IColumnDisplay, '', StringColumnDisplay)
registry.register([IFKCol], IColumnDisplay, '', FKColumnDisplay)
(Notice I had to put an attribute linking a column to its table, so that I could get access to _SO_class_Person for foreign keys. But now it works a charm showing the options and updating.)
The display is handled by two Cheetah Template templates, one for the list of items, and another for displaying/editing a single item (although it's pretty dumb since FunFormKit actually does the form).
#if $admin <a href="$_samoosa_class._samoosa_createPath">Add a new $_samoosa_class._samoosa_singular</a> #end if <ul> #for p in $_samoosa_class.select(orderBy=$_samoosa_sort) #set $itempath = $p._samoosa_getPathName #set $display = $p._samoosa_getDisplayName #if $_samoosa_extra: #set $extra = getattr($p, $_samoosa_extra) #else #set $extra = None #end if <li><a href="$path/$itempath">$display</a> #if $extra: ($extra) #end if </li> #end for </ul> #if $admin <a href="$_samoosa_class._samoosa_createPath">Add a new $_samoosa_class._samoosa_singular</a> #end if
(Ick, all those _samoosa_*, but I want to keep the custom stuff pretty obvious.)
#if $item #set $display = $item._samoosa_getHTMLDisplayName <h2>$display</h2> <h3>Details</h3> #end if $dl
All in all, this has made adding/editing data a lot easier than using phpMyAdmin, adding new entities is arbitrary, and ForeignKeys are dealt with intelligently.
Again, I hate going behind SQLObject's back so much. Perhaps I should be using _SO_columnDict and the SOCol-based objects - although these aren't exported from the module. But for now, what I've got will do.
1 old-style comments
Neil Blakey-Milner — January 21, 2005 at 09:30 PM.