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.