I miss Quixote's object-publisher feel in Webware. I'm beginning to dislike the amount of work I need to do to discover the path that was used to get to my page/servlet. Anyway, since I found Rails' entity URL interface better than mine, I decided I'd steal it.

First problem - understanding what part of the URL was used to get to you. This way, you have a base URL you can use in links for the other functions, as well as being able to have a grip later on about which bits of the URL haven't been consumed yet. Forgive me if I missed something in Webware that does this.

So, I've changed the _respond method...:


    def _respond(self, transaction):
        eup = self.request().extraURLPath()
        eupa = [p for p in posixpath.normpath(eup).split('/') if p and p != '.']
        mypath = self.request().urlPath()
        if eupa:
            if mypath.rfind(eup) != -1:
                mypath = mypath[:mypath.rfind(eup)]
            if eupa[0]:
                if self.handleExtraPathInfo == False:
                    raise HTTPNotFound
                if self.handleExtraPathInfoMethod:
                    func = getattr(self, "child_" + eupa[0], None)
                    if not func:
                        raise HTTPNotFound
                    transaction.content_template = func
                    eup = eupa[1:]
        transaction.myPath = mypath
        transaction.unusedPath = eup
        return Page._respond(self, transaction)

I've added a handleExtraPathInfoMethod attribute to my page class, which says whether I expect the extra path info to correspond to a function. If so, I check the function exists (using child_ as a prefix, a la Twisted to avoid unintentional publishing), and attach the function that will be used later to the transation object.

I also save the bit of the URL that corresponds to the page itself.

I'm not really happy - I want to be able to replace just the contents of the page (not the external layer of template), but I'm sure I'd want to replace/change the external layer if I wanted to. But I don't want to create a new class and object, as I like how available shared functions are. So, for now, the function just overrides the contents.

Ok, so now I can make child_list, child_show, child_edit, and child_new in EntityPage for the URL interface.

class EntityPage(WebwarePage, FormServlet):
    listTemplate = ListContentTemplate
    editTemplate = EditContentTemplate
    showTemplate = ShowContentTemplate
    handleExtraPathInfo = True
    handleExtraPathInfoMethod = True

    def child_list(self):
        path = '%s/edit' % (self.myPath())

        mySearchList = {
            'admin': self.getAdmin(),
            'path': path,
        }
        return self.listTemplate(searchList=[mySearchList, self])

    def child_edit(self):
        path = '%s/edit' % (self.myPath())

        if not self.transaction().unusedPath:
            self.sendRedirectAndEnd('%s/list' % (self.myPath()))

        pathName = self.transaction().unusedPath.pop(0)

        try:
            item = self._samoosa_class._samoosa_pathnameLookup(pathName)
        except:
            return self.sendRedirectAndEnd('%s/list' % (self.myPath()))

        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': self.getAdmin(),
            'path': path,
        }
        return self.editTemplate(searchList=[mySearchList, self])

    def child_new(self):
        submitted, data = self.processForm()
        if str(data) == data:
            data = {}

        data = self.filterData(data)

        if submitted:
            item = self._samoosa_class(**data)
            return self.sendRedirectAndEnd('%s/edit/%s' % (self.myPath(), item._samoosa_getPathName()))

        defaults = {
        }

        df = self.renderableForm(defaults = defaults)
        dl = df.htFormTable()

        mySearchList = {
            'item': None,
            'dl': dl,
        }
        return self.editTemplate(searchList=[mySearchList, self])

    def child_show(self):
        path = '%s/edit' % (self.myPath())

        if not self.transaction().unusedPath:
            self.sendRedirectAndEnd('%s/list' % (self.myPath()))

        pathName = self.transaction().unusedPath.pop(0)

        try:
            item = self._samoosa_class._samoosa_pathnameLookup(pathName)
        except:
            return self.sendRedirectAndEnd('%s/list' % (self.myPath()))

        mySearchList = {
            'item': item,
        }
        return self.showTemplate(searchList=[mySearchList, self])

    def content_template(self):
        return self.child_list()

Nice and simple - the default entrypoint will just use the list, otherwise we use the named functions.

I needed a 'show' version, so I used much the same method as I used to build a FunFormKit form for 'edit':

class SiteSQLObject(SQLObject):
    def fieldValues(self):
        ret = []
        columnDict = {}
        for col in self._columns:
            columnDict[col.kw['name']] = col

        _capitalRE = re.compile(r'[A-Z]')
        for colName in self._samoosa_columnsInOrder:
            words = []
            template = colName
            while 1:
                match = _capitalRE.search(template)
                if match:
                    words.append(template[:match.start()])
                    template = template[match.start()].lower() + template[match.start()+1:]
                else:
                    words.append(template)
                    break
            description = " ".join(words).capitalize()
            col = columnDict[colName]
            display = registry.queryAdapter(col, IColumnDisplay, '')
            ret.append((description, display.displayValue(getattr(self, colName))))
        return ret

class ICol(Interface): pass
class IColumnDisplay(Interface): pass
classImplements(Col, ICol)

class ColumnDisplay:
    implements(IColumnDisplay)

    def __init__(self, context):
        self.context = context

class DefaultColumnDisplay(ColumnDisplay):
    def displayValue(self, value):
        return value

registry.register([ICol], IColumnDisplay, '', DefaultColumnDisplay)

I had to take the code from FunFormKit to convert SQLObject-style column names to English. I was totally amazed I never realised FunFormKit did that for me. And now it "just works" for show too.

I have an interesting idea for a minor tweak, placing the SQLObject update statement in a separate function. Then, I can have certain people edit the object and have it update into the database, and others will appear to be editing the object and create an "update request", which someone else can accept/reject into the database. And I can easily put in audit trails too. All that's going to be a win for my secret project...