Personal tools
You are here: Home Documentation Tutorials b-org: Creating content types the Plone 2.5 way The extension story
Support

Get Help

Join our chat rooms or support forums if you have more specific questions.

Plone Training
Learn how to design, build, and deploy a website in Plone through one of the numerous Plone training sessions around the world.
Find Plone training…
 
Document Actions

The extension story

One of the main drivers behind the componentisation of b-org is that it should be easy to extend and customise for third party developers. We'll take a look at how such customisations may look, before considering how we made it possible.

Martin Aspeli

Plone 2.5 brings us closer to the promised land of Zope 3. Zope 3 brings us a new way of working. This tutorial will show how to marry the old and the new, to make Plone products that are more extensible, better tested and easier to maintain.
Page 5 of 15.

b-org ships with an example called charity, found in the examples/charity directory, which demonstrates one use-case specific implementation of b-org. This is quite simple, consisting of the following top-level files and directories:

configure.zcml
Registers the schema extension adapters (see below) and references the browser package
Extensions/
Contains an Install.py script that configures the Factory Type Information for the Department, Employee and Project content types. It does so by using GenericSetup XML files, but invokes the import handlers explicitly rather than through a GenericSetup profile.
 browser/
Contains Zope 3 views for the charity department, employee and project content types, and a configure.zcml to register these. More on views in a later section.
schema/
Contains adapters that extend the schemas for Departments, Employees and Projects with use-case specific fields.

To use charity you should copy or symlink it from Products/borg/examples/charity to Products/charity. It can be installed as normal, but you must install b-org first. See borg/README.txt for the full install instructions!

A key aim is to make it possible to meaningfully extend b-org without needing to subclass all its types. Of course, you can do that, but in most cases it's not necessary. Unfortunately, the mechanisms and techniques described here will be "global" in nature. That is, you will not be able to have two different modes of customisation for two different Plone instances in the same Zope instance. This is because prior to Zope 2.10 (which Plone 2.5 does not support - it wasn't out until several months after Plone 2.5 was released), the "local" components story in Zope 3 was not fully developed. There is also a specific problem with the way the schema extension mechanism works which makes it inherently global.

When Plone 3.0 rolls around, it will support local components much better, and Archetypes 1.5, in conjunction with a third-party product called ContentFlavors (or possibly another similar tool), will enable the kind of extension story described here to work on almost any type. At that point, the forerunner you see in b-org now will be obsolete.

Of course, if you don't need two different b-org customisations for two different Plone sites in the same Zope instance (which I suspect most people can work around - having two separate Zope instances of course isolates you from all of this), you should be fine.

The schemas extenders

If you look at charity/configure.zcml you will see the following registrations:

  <adapter factory=".schema.department.DepartmentSchemaExtender" />
  <adapter factory=".schema.employee.EmployeeSchemaExtender" />
  <adapter factory=".schema.project.ProjectSchemaExtender" />

These schema extenders are adapters that hook into a specific part of b-org. We will describe this in more detail later, but here is how they look from the point of view of the extending product:

from zope.interface import implements
from zope.component import adapts

from Products.Archetypes.atapi import *

from Products.borg.interfaces import IEmployeeContent
from Products.borg.interfaces import ISchemaExtender

CharityEmployeeSchema = Schema((

StringField('title',
accessor='Title',
required=True,
user_property='fullname',
widget=StringWidget(
label=u"Full name",
description=u"Full name of this employee",
),

),

StringField('email',
validators=('isEmail',),
required=True,
searchable=True,
user_property=True,
widget=StringWidget(
label=u"Email address",
description=u"Enter the employee's email address",
),
),

StringField('phone',
required=False,
searchable=True,
user_property=True,
widget=StringWidget(
label=u"Phone number",
description=u"Enter the employee's phone number",
),
),

StringField('mobilePhone',
required=False,
searchable=True,
user_property=True,
widget=StringWidget(
label=u"Mobile phone number",
description=u"Enter the employee's mobile phone number",
),
),

StringField('location',
searchable=True,
user_property=True,
widget=StringWidget(
label=u"Location",
description=u"Your location - either city and country - or in a company setting, where your office is located.",
),
),

StringField('language',
user_property=True,
vocabulary="availableLanguages",
widget=SelectionWidget(
label=u"Language",
description=u"Your preferred language.",
),
),

TextField('description',
required=True,
searchable=True,
user_property=True,
default_content_type='text/html',
default_output_type = 'text/x-html-safe',
allowable_content_types = ('text/html', 'text/structured', 'text/x-web-intelligent',),
widget=RichWidget(
label=u"Biography",
description=u"Enter a short biography of the employee",
),
),

))

class EmployeeSchemaExtender(object):
"""Extend the schema of an employee to include additional fields.
"""
implements(ISchemaExtender)
adapts(IEmployeeContent)

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

def extend(self, schema):
schema = schema + CharityEmployeeSchema
# Reorder some fields
schema.moveField('description', after='mobilePhone')
schema.moveField('location', before='description')
schema.moveField('language', before='description')
schema.moveField('roles_', after='description')
return schema


This example is employee.py. The other extensions are simpler, and work on the exact same principle. When calculating the schema of a content type, the b-org types (by virtue of Products.borg.content.schema.ExtensibleSchemaSupport, a mix-in class that all the b-org types uses, and which the aforementioned changes to Archetypes should make obsolete) will look up an adapter from the content object (which is marked with IEmployeeContent, in this case), to ISchemaExtender. This will be given the chance to extend (and modify) the schema of the type.

The returned value is cached (to avoid an expensive re-calculation each time the schema is used). This cache can be invalidated upon an event, which you will see in charity/Extensions/Install.py:

from zope.event import notify
from Products.borg.content.schema import SchemaInvalidatedEvent
from Products.borg.content.employee import Employee

...

def install(self, reinstall=False):
...
notify(SchemaInvalidatedEvent(Employee))
The event is an instance of a class that implements ISchemaInvalidatedEvent, and takes a class as an argument to know which class the schema is being invalidated for.

Defining new views and type information

We have now managed to add new schema fields to Department, Employee and Project. The auto-generated edit form will pick these up for editing, but we probably also want some custom views. We may also want to change other aspects of the Factory Type Information (FTI) which controls how the type is presented within Plone's UI (an FTI is an object in portal_types).

First, we define some views in the browser package. These are described in a later section, but lookin at charity/configure.zcml, you will see:
<include package=".browser" />
This will bring in charity/browser/configure.zcml, which contains several directives like:
  <page  
name="charity_employee_view"
for="Products.borg.interfaces.IEmployeeContent"
class=".employee.EmployeeView"
template="employee.pt"
permission="zope2.View"
/>
This, along with the class Products.charity.browser.employee.EmployeeView and the template charity/browser/employee.pt will make a view @@charity_employee_view (the @@ is optional, but serves to disambiguate views from content objects, for example) available on any employee (or rather, any object providing IEmployeeContent).

We then need to tell Plone that this view should be invoked when you view an Employee object or click its 'View' tab. This is done by setting the (Default) and view method aliases for the Employee type. See this page of the RichDocument tutorial for some background.

To achieve this, we could modify portal_types/Employee in Python during the Install.py script. However, to make it easier to define the FTI, we use a GenericSetup XML file instead. Take a look at charity/Extensions/setup/types/Employee.py, for example:
<?xml version="1.0"?>
<object name="Employee"
meta_type="Factory-based Type Information"
xmlns:i18n="http://xml.zope.org/namespaces/i18n">

<property name="title">Employee</property>
<property name="description">A charity employee or volunteer.</property>
<property name="content_icon">employee.gif</property>
<property name="content_meta_type">Employee</property>
<property name="product">borg</property>
<property name="factory">addEmployee</property>
<property name="immediate_view">base_edit</property>
<property name="global_allow">False</property>
<property name="filter_content_types">False</property>
<property name="allowed_content_types" />
<property name="allow_discussion">False</property>

<alias from="(Default)" to="@@charity_employee_view"/>
<alias from="view" to="@@charity_employee_view"/>
<alias from="edit" to="base_edit"/>
<alias from="properties" to="base_metadata"/>
<alias from="sharing" to="folder_localrole_form"/>

<action title="View" action_id="view" category="object" condition_expr=""
url_expr="string:${object_url}" visible="True">
<permission value="View"/>
</action>

<action title="Edit" action_id="edit" category="object" condition_expr=""
url_expr="string:${object_url}/edit" visible="True">
<permission value="Modify portal content"/>
</action>

<action title="Properties" action_id="metadata" category="object" condition_expr=""
url_expr="string:${object_url}/properties" visible="True">
<permission value="Modify portal content"/>
</action>

<action title="Sharing" action_id="local_roles" category="object" condition_expr=""
url_expr="string:${object_url}/sharing" visible="True">
<permission value="Modify portal content"/>
</action>

</object>
This defines the various aspects of the FTI, and is basically a modified copy of the equivalent file from the b-org extension profile. You'll learn more about these in the section on GenericSetup, but for now observe that we invoke this explicitly in Install.py, via some boilerplate utility code:
from Products.charity.Extensions.utils import updateFTI

def install(self, reinstall=False):
...
if not reinstall:
updateFTI(self, charity, 'Department')
updateFTI(self, charity, 'Employee')
updateFTI(self, charity, 'Project')
This will update the FTIs by examing Products/charity/Extensions/setup/types. Each file there is named corresponding to the name of the FTI it modifies.

Adding new functionality

Extending the schema and modifying the FTI to support different views is probably enough for a large number of use cases. If you find yourself thinking "I wish I could add a method to the Employee class to support ...", take your left hand, hold it out, raise you right hand and slap your left wrist sternly, then read the section on adapters again.

For example, let's say you wanted to send an email to administrators when a particular button in the view was clicked. You could do that in an adapter. For examples, in your interfaces module, you could could have:
from zope.interface import Interface

class IAdministratorNagging(Interface):
"""Someone who will nag the admin
"""

def nag(message):
"""Send nagging email
"""

Then, an adapter from IEmployee in module nag.py:

from zope.interface import implements
from zope.component import adapts

from interfaces import IAdministratorNagging
from Products.borg.interfaces import IEmployeeContent

from Products.CMFCore.utils import getToolByName

class NaggingEmployee(object):
implements(IAdministratorNagging)
adapts(IEmployeeContent)

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

def nag(self, message):
mailHost = getToolByName(self.context, 'MailHost')
...

And finally, in your configure.zcml:

<adapter factory=".nag.NaggingEmployee" />

Then, in the form handler that is about to nag the employee, you would do:

from Products.myproduct.interfaces import IAdministratorNagging
nagger = IAdministratorNagging(employee)
nagger.nag("Give me more disk space!")

Obviously, this is a somewhat contrived example, but hopefully you get the gist.

Modifying workflow and other configuration

The b-org workflows are not special. In your Install.py, you could modify them or change the workflow assignments as you would any other content type. You can also use CMFPlacefulWorkflow to assign different workflows depending on context, if need be.

Similarly, if you need to modify the behaviour of the Department, Employee and Project types in other ways, for example by modifying settings in portal_properties, you are of course free to do so. The intended pattern is that your b-org customisation product encapsulates the various settings and extensions that describe your use case.

Changing fundamental b-org behaviour

Lastly, as you learn about b-org you will see how it uses adapters to hook into membrane. If you need to override its behaviour, you can add an overrides.zcml to your product, which is otherwise identical to a configure.zcml in format, but is able to override earlier registrations (such s those in b-org). For example, you could override the adapter from IEmployeeContent to IUseRelated to change the way in which user ids is assigned, or the adapter to IUserAuthentication to change the way in which authentication is performed.

 
by Martin Aspeli last modified December 1, 2006 - 16:44 All content is copyright Plone Foundation and the individual contributors.

For any issues with the web site functionality, please file a ticket.

Please consult the policy on plone.org content if you want your content published on this site.

Servers and hosting by