July 9, 2014

Plone - the broken parts - Member schema extenders and plone.api

This is a loose series of blog posts about parts of Plone that I consider as broken from the prospective of a programmer. The blog entries are based on personal experiences with Plone over the last few months collected in new Plone 4.3 projects and some legacy projects but they also reflect experienced learned from other non-core Plone developers involved in these projects (developers on the customer side).

It is a common project requirement to extend the Plone user memberdata schema. The common approach is perhaps documented here (https://pypi.python.org/pypi/collective.examples.userdata). The additional fields of the memberdata must be defined as a zope.schema. Nothing special - same as definining forms using z3c.form or writing content-types with Dexterity. In a recent project we had a working memberdata  extender and had to extend it with two schema.List fields. Both fields were added to the schema together with the following (working) adapter implementation:

from plone.app.users.browser.personalpreferences import UserDataPanelAdapter

class EnhancedUserDataPanelAdapter(UserDataPanelAdapter):
    """ Adapter for extended user schema """

    def __init__(self, context):
        super(EnhancedUserDataPanelAdapter, self).__init__(context)
        self.add_property('academic')
        self.add_property('gender')
        self.add_property('phone')
        self.add_property('academic')
        self.add_property('expertise')
        self.add_property('title')
        self.add_property('firstname')
        self.add_property('lastname')
        self.add_property('position')
        self.add_property('phone')
        self.add_property('db_projects')
        self.add_property('specialties')
        self.add_property('institution')
        self.add_property('institution_location')
        self.add_property('memberships')
        self.add_property('projects')
        self.add_property('gender')
        self.add_property('birthday')
        self.add_property('cooperation_interests')
        self.add_property('locations')        

    def add_property(self, name, value=None):
        fget = lambda self: self._get_property(name)
        fset = lambda self, value: self._set_property(name, value)
        setattr(self.__class__, name, property(fget, fset))
        setattr(self, '_' + name, value)

    def _set_property(self, name, value):
        return self.context.setMemberProperties({name: value})

    def _get_property(self, name):
        return self.context.getProperty(name, None)

After restarting Plone, the @@user-information view  crashed directly with the following non-speaking traceback:

Module ZPublisher.Publish, line 138, in publish
Module ZPublisher.mapply, line 77, in mapply
Module ZPublisher.Publish, line 48, in call_object
Module zope.formlib.form, line 795, in __call__
Module five.formlib.formbase, line 50, in update
Module zope.formlib.form, line 758, in update
Module plone.fieldsets.form, line 30, in setUpWidgets
Module zope.formlib.form, line 402, in setUpEditWidgets
Module zope.formlib.form, line 332, in _createWidget
Module zope.component._api, line 107, in getMultiAdapter
Module zope.component._api, line 120, in queryMultiAdapter
Module zope.component.registry, line 238, in queryMultiAdapter
Module zope.interface.adapter, line 532, in queryMultiAdapter
Module zope.component.security, line 77, in factory
Module zope.formlib.itemswidgets, line 52, in CollectionInputWidget
Module zope.component._api, line 109, in getMultiAdapter
ComponentLookupError: ((, None, ), , u'') 

What is the problem here from the programmer's prospective: no information about the real problem, no information about the schema field causing the problem. Further investigations using the Python debugger then showed that the problem is related to the two new schema.List fields. "Related" means that we still don't know the reason for the real problem after two or three hours debugging with two persons. Bad programmer experience because we do not get reasonable information from the underlaying view or call it user-information subsystem.

What makes the memberdata extension mechanism even more complicated:

  • exposure of many ZCA related magic or explicit configuration to the programmer
  • the programmer still needs to care about additional boilerplate like the adapter implementation
  • the programmer still needs to care about the memberdata_schema.xml file

From the high-level prospective: the schema definition of the extender should be complete enough for defining the memberdata extender. A programmer must be not be confronted with two or three levels of complexity for common tasks. In a perfect world this complexity would be hidden behind some reasonable API providing some reasonable consistency checks and error handling. Right now half-talented programmers are exposed to the full evil of the Zope Component Architecture. Key point of a programmer friendly environment are reasonable APIs. The Zope Component Architecture is not an API, it is an framework. In a perfect world a programmer must not know about the ZCA.plone.api is a step in the right direction however partly half-baked and not well-defined.

  • plone.api.user.get_permissions() returns a list of all permissions granted to a user (on a particular object). The common usecase is that you want to check one particular permission. The "old" user.has_permission(permission, context) API is more closely to real usecases than the implemented functionality of plone.api.
  • plone.api.user.get() claims to return the current user object. This is true as long as the user is defined within the context of the Plone site. The method raises an exception for users defined on the Zope root level. You might argue that this an intentional behavior - possibly it is intential but in reality I find this method completely unusable because some code parts are possibly called by site administrators and Zope manager accounts. In both case I want the same code to work with one way to retrieve the current user instead having to provide fallback code for dealing with non-Plone users accounts.