Zope Component Architecture basics with five.grok
This manual describes the basics of the Zope Component Architecture using five.grok, which brings convention-over-configuration to Zope 2 and Plone. This information is not specific to Dexterity, but will be useful to Dexterity developers who want to move beyond types and schemata.
1. Background
What is five.grok all about?
Table of Contents
Introduction
Dexterity wants to make some things really easy. These are:
- Create a "real" content type entirely through-the-web without having to know programming.
- As a business user, create a schema using visual or through-the-web tools, and augment it with adapters, event handlers, and other Python code written on the filesystem by a Python programmer.
- Create content types in filesystem code quickly and easily, without losing the ability to customise any aspect of the type and its operation later if required.
- Support general "behaviours" that can be enabled on a custom type in a declarative fashion. Behaviours can be things like title-to-id naming, support for locking or versioning, or sets of standard metadata with associated UI elements.
- Easily package up and distribute content types defined through-the-web, on the filesystem, or using a combination of the two.
Philosophy
Dexterity is designed with a specific philosophy in mind. This can be summarised as follows:
- Reuse over reinvention
- As far as possible, Dexterity should reuse components and technologies that already exist. More importantly, however, Dexterity should reuse concepts that exist elsewhere. It should be easy to learn Dexterity by analogy, and to work with Dexterity types using familiar APIs and techniques.
- Small over big
- Mega-frameworks be damned. Dexterity consists of a number of specialised packages, each of which is independently tested and reusable. Furthermore, packages should has as few dependencies as possible, and should declare their dependencies explicitly. This helps keep the design clean and the code manageable.
- Natural interaction over excessive generality
- The Dexterity design was driven by several use cases that express the way in which we want people to work with Dexterity. The end goal is to make it easy to get started, but also easy to progress from an initial prototype to a complex set of types and associated behaviours through step-wise learning and natural interaction patterns. Dexterity aims to consider its users - be they business analysts, light integrators or Python developers, and be they new or experienced - and cater to them explicitly with obvious, well-documented, natural interaction patterns.
- Real code over generated code
- Generated code is difficult to understand and difficult to debug when it doesn't work as expected. There is rarely, if ever, any reason to scribble methods or 'exec' strings of Python code.
- Zope 3 over Zope 2
- Although Dexterity does not pretend to work with non-CMF systems, as many components as possible should work with plain Zope 3, and even where there are dependencies on Zope 2, CMF or Plone, they should - as far as is practical - follow Zope 3 techniques and best practices. Many operations (e.g. managing objects in a folder, creating new objects or manipulating objects through a defined schema) are better designed in Zope 3 than they were in Zope 2.
- Zope concepts over new paradigms
- We want Dexterity to be "Zope-ish". Zope is a mature, well-designed (well, mostly) and battle tested platform. We do not want to invent brand new paradigms and techniques if we can help it.
- Automated testing over wishful thinking
- "Everything" should be covered by automated tests. Dexterity necessarily has a lot of moving parts. Untested moving parts tend to come loose and fall on people's heads. Nobody likes that.
Getting started
Please read the installation guide to get Dexterity up and running.
Then log in to Plone, go to Site Setup, and go to the Dexterity Types control panel to get started creating content types through the web.
Or read the Dexterity developer manual to get started developing Dexterity content types on the filesystem.
This release of Dexterity is compatible with Plone 3, 4, and 4.1.
Upgrading
If you are upgrading from a previous release of Dexterity, you need to:
- Update your buildout with the new versions (or extend the updated KGS), and re-run it.
- Restart Zope.
- Go to the Add-ons control panel in Plone Site Setup, and run the upgrade steps for "Dexterity Content Types" if there are any available.
Documentation
Various documentation is available:
The following documents are not Dexterity-specific, but will likely be useful to users of Dexterity:
Mailing list
The dexterity-development group provides a place to discuss development and use of Dexterity.
Issue tracker
Please report issues in our Google Code issue tracker.
Contributed Packages
The Dexterity known good set (KGS) of version pins includes a number of contributed packages that are not installed by default:
- plone.app.referenceablebehavior
- Adds support for the Archetypes reference engine to Dexterity content, so that Dexterity items can be referenced from Archetypes items. Requires Plone 4.1.
- plone.app.stagingbehavior
- Adds support for staging Dexterity content, based on plone.app.iterate. Requires Plone 4.1.
- plone.app.versioningbehavior
- Adds support for storing historic versions of Dexterity content, based on Products.CMFEditions. Requires Plone 4.0 or greater.
- collective.z3cform.datagridfield
- A z3c.form widget for editing lists of subobjects via a tabular UI.
Contributing
Most Dexterity code is owned by the Plone Foundation and maintained in the Plone svn repository. We're happy to share commit access so that you can share code with us, but first you must sign the Plone contributor agreement.
Dexterity wouldn't be possible without the hard work of a lot of people, including:
- Martin Aspeli
- Jian Aijun
- Wichert Akkerman
- Jonas Baumann
- JC Brand
- David Brenneman
- Thomas Buchberger
- Joel Burton
- Roche Compaan
- Vincent Fretin
- Rok Garbas
- Anthony Gerrard
- Nathan van Gheem
- David Glick
- Craig Haynal
- Wouter Vanden Hove
- Jean-Michel Francois
- Jim Fulton
- Jamie Lentin
- Alex Limi
- Marco Martinez
- Steve McMahon
- Jason Mehring
- Alec Mitchell
- Daniel Nouri
- Ross Patterson
- Franco Pellegrini
- Martijn Pieters
- Maurits van Rees
- Johannes Raggam
- Lennart Regebro
- Laurence Rowe
- Israel Saeta Perez
- Hanno Schlichting
- Christian Schneider
- Carsten Senger
- Jon Stahl
- Carsten Senger
- Eric Steele
- Gaudenz Steinlin
- Dorneles Tremea
- Sean Upton
- Hector Velarde
- Sylvain Viollon
- Matthew Wilkes
- Matt Yoder
- Andi Zeidler
(Please add your name if we have neglected to.)
Release Notes
This release is compatible with Plone 3, 4, 4.1, and 4.2, as long as you use the correct set of version pins from good-py (see the installation guide).
Changes
The following packages have been updated since Dexterity 1.1:
plone.app.dexterity 1.2 & 1.2.1
- Add missing upgrade description to restore Plone 3 compatibility. [davisagli]
- Give a more explicit warning before deleting content types that have existing instances. [davisagli]
- Add validation to prevent giving a type the same name as an existing type. [davisagli]
- Make sure the title and description of new FTIs are stored encoded, and with a default i18n domain of 'plone'. [davisagli]
- Install the profile from collective.z3cform.datetimewidget to enable the Jquery Tools date picker for date/time fields. [davisagli]
- Bugfix: Make sure type short names are validated. [davisagli]
- Bugfix: Fix display of type descriptions in the types control panel. [davisagli]
- Add intro message to Dexterity control panel. [jonstahl, davisagli]
plone.dexterity 1.1.2
- Fix UnicodeDecodeError when getting an FTI title or description with non-ASCII characters. [davisagli]
- When deleting items from a container using manage_delObjects, check for the "DeleteObjects" permission on each item being deleted. This fixes http://code.google.com/p/dexterity/issues/detail?id=252 [davisagli]
plone.schemaeditor 1.2.0
- Display fields from behaviors in the schema preview too. [davisagli]
- Prevent the user from creating fields with names that are reserved for Dublin Core metadata. title and description can still be used as long as the fields are of the correct type. [davisagli]
- Remove unhelpful help text for min_length and max_length fields. [davisagli]
- The schema listing preview now respects autoform hints (such as custom widgets). [davisagli]
- Make new boolean fields use the radio widget by default. The field now appears as "Yes/No" in the list of field types. [davisagli]
- Hide the 'read only' setting for fields. [davisagli]
- Edit field defaults from the schema listing instead of in the field overlays. This simplifies making sure that the default can't be set to invalid values. [davisagli]
- Limit the height of text areas in the schema listing to avoid extra scrolling. [davisagli]
- Fall back to normal traversal if a field isn't found when traversing the schema context. This fixes inline validation for forms on the schema context. [davisagli]
- Make it possible to make the schemaeditor not be the default view of the schema context, by specifying the schemaEditorView attribute on the schema context. [davisagli]
- Added Spanish translation. [hvelarde]
plone.autoform 1.1
- Added the AutoObjectSubForm class to support form hints for object widget subforms. [jcbrand]
plone.app.textfield 1.1
- Provide a version of the RichText field schema for use with plone.schemaeditor. Only the default_mime_type field is exposed for editing through-the-web, with a vocabulary of mimetypes derived from the AllowedContentTypes vocabulary in plone.app.vocabularies (which can be adjusted via Plone's markup control panel). [davisagli]
- Log original exception when a TransformError is raised. [rochecompaan]
plone.app.versioningbehavior 1.1
- Added French translations. [jone]
- Fixed SkipRelations modifier to also work with behaviors which are storing relations in attributes. [buchi]
- Added Spanish translation. [hvelarde]
plone.app.intid 1.0
- Remove includeOverride for five.intid. [thet]
- Fix import step intid-register-content to register content in all Languages if LinguaPlone is installed. [csenger]
plone.formwidget.autocomplete 1.2.3
- Fix <input /> element generation for Internet Explorer; in most cases, the generated element would be lacking the name attribute. [mj]
plone.formwidget.contenttree 1.0.5
- Added Spanish translation [hvelarde]
collective.z3cform.datagridfield 0.10
- Fix bug with moving the last row up. [m-martinez]
z3c.formwidget.query 0.8
- If one of the values to be displayed provides IRoleManager, then check for permission first. [frapell]
Changelog for plone.app.dexterity
1.2.1 - 2012-03-04
- Add missing upgrade description to restore Plone 3 compatibility. [davisagli]
1.2 - 2011-12-20
- Give a more explicit warning before deleting content types that have existing instances. [davisagli]
- Add validation to prevent giving a type the same name as an existing type. [davisagli]
- Make sure the title and description of new FTIs are stored encoded, and with a default i18n domain of 'plone'. [davisagli]
- Install the profile from collective.z3cform.datetimewidget to enable the Jquery Tools date picker for date/time fields. [davisagli]
- Bugfix: Make sure type short names are validated. [davisagli]
- Bugfix: Fix display of type descriptions in the types control panel. [davisagli]
- Add intro message to Dexterity control panel. [jonstahl, davisagli]
1.1 - 2011-11-26
- Added Italian translation. [giacomos]
- Make sure that the intids utility doesn't get destroyed if a product depending on Dexterity is reinstalled. This closes http://code.google.com/p/dexterity/issues/detail?id=239 [davisagli]
- Add upgrade step to add missing UUIDs to Dexterity content items if plone.uuid is present. [davisagli]
- Added Spanish translation. [hvelarde]
1.0.3 - 2011-09-24
- Fix bad release of 1.0.2. [davisagli]
1.0.2 - 2011-09-24
- Bugfix: Fix the deprecated IRelatedItems to still add a form field. [davisagli]
- Bugfix: Make sure subject can still be retrieved as unicode for the categorization behavior now that the Subject accessor returns a bytestring. [davisagli]
1.0.1 - 2011-07-02
- Deprecated the IRelatedItems behavior in this package. It moved to plone.app.relationfield. [davisagli]
- Add no-op "grok" and "relations" extras for forward-compatibility with Dexterity 2.0. [davisagli]
1.0 - 2011-05-20
- Fix publishing dates DateTime/datetime conversions so as not to drift by the timezone delta every save. [elro]
- Make sure cloned types get a new factory. [davisagli]
- Don't override overlay CSS in Plone 4. [davisagli]
- Fixed cloning of types with a period (.) in their short name. [davisagli]
- Allow specifying a type's short name when adding a type. [davisagli]
- Make sure the Basic metadata adapter accesses the content's title attribute directly so it doesn't get encoded. Also make sure encoded data can't be set via this adapter. [davisagli]
1.0rc1 - 2011-04-30
Added upgrade step to install new javascript from plone.formwidget.autocomplete [davisagli]
Added basic support for making TTW changes to schemas defined in filesystem models and code. (Note: This feature will not actually work until some further changes are completed in plone.dexterity.)
In order to support this change, the event handling to serialize schema changes was revised. We now register a single event handler for the SchemaModifiedEvent raised for the schema context. This allows us to keep track of the FTI that changes need to be serialized to on the schema context. The serializeSchemaOnFieldEvent and serializeSchemaOnSchemaEvent handlers were removed from the serialize module and replaced by serializeSchemaContext. The serializeSchema helper remains but is deprecated. [davisagli]
Add MANIFEST.in. [WouterVH]
Add "export" button to types editor. Exports GS-style zip archive of type info for selected types. [stevem]
Fix old jquery alias in types_listing.pt. This closes http://code.google.com/p/dexterity/issues/detail?id=159 [davisagli]
Make display templates fill content-core on Plone 4. [elro]
Add ids to the group fieldsets on display forms. [elro]
Exclude from navigation behavior should be restricted to IDexterityContent. [elro]
1.0b4 - 2011-03-15
- Add a "Name from file name" behavior. [elro]
- Remove the NameFromTitle behavior factory, it is not necessary. [elro]
- Add "Next previous navigation" and "Next previous navigation toggle" behaviors. [elro]
- Add an "Exclude from navigation" behavior. [lentinj]
- Put the folder listing within a fieldset. [lentinj]
1.0b3 - 2011-02-11
- Add a navigation root behavior. [elro]
- Fix decoding error when an encoded description is stored in the FTI. [davisagli]
- Avoid empty <div class="field"> tag for title and description in item.pt and container.pt. [gaudenzius]
- Add locales structure for translations with cs , de, es, eu, fr, ja, nl, pt_BR [toutpt]
- Update french translation [toutpt]
1.0b2 - 2010-08-05
- Fix several XML errors in templates. Needed for Chameleon compatibility. [wichert]
- cloning a type through the dexterity UI in the control panel did not work if the type had a hyphen in it's name. This fixes http://code.google.com/p/dexterity/issues/detail?id=126 [vangheem]
1.0b1 - 2010-04-20
- Require plone.app.jquerytools for the schema editor UI, and make sure it is installed when upgrading. [davisagli]
- Remove unused schemaeditor.css. [davisagli]
- Omit the metadata fields except on edit and add forms. [davisagli]
- Enable the "Name from title" behavior for new types, by default. [davisagli]
- Include plone.formwidget.namedfile so that File upload and Image fields are available out of the box. You must explicitly include z3c.blobfile in your environment if you want blob-based files. [davisagli]
- Added a DexterityLayer that can be used in tests. [davisagli]
- Fix issue with the BehaviorsForm accidentally polluting the title of the z3c.form EditForm 'Apply' button. [davisagli]
- Add upgrades folder and make sure plone.app.z3cform profile gets installed on upgrades from previous versions of Dexterity. [davisagli]
- Depend on the plone.app.z3cform profile, to make sure the Plone browser layer for z3c.form gets installed. [davisagli]
- Avoid relying on acquisition to get the portal_url for links in the type listing table. [davisagli]
1.0a7 - 2010-01-08
- Make sure the Dublin Core fieldsets appear in the same order as they do in AT content. [davisagli]
- Make sure the current user is loaded as the default creator for the IOwnership schema in an add form. [davisagli]
- Include behavior descriptions on the behavior edit tab. [davisagli]
- IBasic behavior: set missing_value of description-field to u'' . The description should never be None (live_search would not work any more). [jbaumann]
- Fix issue where traversing to a nonexistent type name in the types control panel did not raise NotFound. [davisagli]
- Make it possible to view the fields of non-editable schemata. [davisagli]
- Tweaks to the tabbed_forms template used for the types control panel. [davisagli]
1.0a6 - 2009-10-12
- Add plone.app.textfield as a dependency. We don't use it directly in this package, but users of Dexterity should have it installed and available. [optilude]
- Use some default icons for new types. [davisagli]
- Show type icons in type listing if available. [davisagli]
- Removed 'container' field from the types listing in the control panel (it wasn't working). [davisagli]
- Add message factories to titles and descriptions of metadata schema fields. Fixes http://code.google.com/p/dexterity/issues/detail?id=75. [optilude]
- Patch listActionInfos() instead of listActions() in order to get the folder/add category into the actions list. This avoids a problem with the 'actions.xml' export handler exporting the folder/add category incorrectly. Fixes http://code.google.com/p/dexterity/issues/detail?id=78 [optilude]
1.0a5 - 2009-07-26
- Explicitly include overrides.zcml from plone.app.z3cform. [optilude]
1.0a4 - 2009-07-12
Changed API methods and arguments to mixedCase to be more consistent with the rest of Zope. This is a non-backwards-compatible change. Our profuse apologies, but it's now or never. :-/
If you find that you get import errors or unknown keyword arguments in your code, please change names from foo_bar too fooBar, e.g. serialize_schema() becomes serializeSchema(). [optilude]
1.0a3 - 2009-06-07
- Updated use of <plone:behavior /> directive to match plone.behavior 1.0b4. [optilude]
1.0a2 - 2009-06-01
- Remove superfluous <includeOverrides /> in configure.zcml which would cause a problem when the package is loaded via z3c.autoinclude.plugin [optilude]
1.0a1 - 2009-05-27
- Initial release
1.1. The Zope Component Architecture
A high level overview of the basic concepts of the Zope Component Architecture
The Zope Component Architecture underpins much of the advanced functionality in Zope and Plone. By mastering a few core concepts, you will be able to understand, extend and customise a wide range of Zope technologies. These concepts include:
- Using interfaces formalise a contract for and document a given component
- Implementing the singleton pattern with unnamed utilities
- Using named utilities to build a registry of homogenous objects
- Using adapters to implement generic functionality that can work with heterogeneous objects
- Customising behaviour with the concept of a more-specific adapter or multi-adapter
- Event subscribers and event notification
- Display components, including browser views, viewlets and resource directories
This tutorial will explain these concepts using simple examples, and illustrate how to use convention-over-configuration with the five.grok package to quickly and easily employ adapters, utilities, event subscribers and browser components in your own code.
Conventions used in this manual
The examples in this manual are sometimes shortened for readability, and you may find that certain details of implementation are not shown to keep the examples short and focused.
You will find two kinds of code listings here. A code block illustrating code you may write in your own files is shown verbatim like this:
form five import grok
class SampleAdapter(grok.Adapter):
grok.provides(ISomeInterface)
grok.context(ISomeOtherInterface)
...
Note:
- Code snippets may refer to code defined earlier on the same page. In this case, import statements for this code are not shown.
- An ellipsis is sometimes used to abbreviate code listings.
Sometimes, we will also show how a component or function can be used in client code. Here, "client code" means any code that is making use of the features implemented with the components illustrated. These are shown using Python interpreter (aka doctest) conventions, like this:
>>> from five import grok >>> context = SomeObjects() >>> adapted = ISomeInterface(context) >>> adapted.someValue 123
Lines starting with >>> indicate executable Python code, be that in a test, in the interactive interpreter, or in the body of a function or method somewhere. Return values and output are shown underneath without the three-chevron prefix.
1.2. What is Grok and five.grok?
Heritage and anthropology
This manual is about using five.grok to configure components in the Zope Component Architecture. But what is five.grok?
Grok is a web development framework built on top of Zope 3 (aka Zope Toolkit, or ZTK). One of the main aims of the Grok project is to make it easier to get started with Zope development by employing "convention-over-configuration" techniques. Whereas in a "plain Zope" a developer would typically write a component in Python code and then register it in ZCML (an XML syntax, normally found in a file called configure.zcml), a Grok developer uses base classes and inline code directives to achieve the same thing. The advantage is that the "wiring" of a component is maintained right next to its code, making it easier to understand what registrations are in effect, and reducing the need for context-switching between different files and syntaxes.
It is important to realise that, for the purposes of this manual at least, the Grok concepts are just an alternate way to configure the Zope Component Archiecture. Everything that can be done with Grok configuration can also be done with "plain Zope" and ZCML. The grok syntax is merely a more convenient, compact and opinionated way to acheive the same thing.
Opinionated? That's right. In part, the design of the various Grok directives and base classes aims to steer developers towards good practice, well-organised code and shared standards. That's not to say you can't go your own way if you really need to, but it is usually best to follow the conventions and standards used by everybody else, unless you have good reason to do otherwise.
Example
Let's take a look at an example. Here is a simple adapter registration in vanilla Zope. First, the adapter factory, a Python class:
from zope.interface import implements
from zope.component import adapts
from my.package.interfaces import IMyType
from zope.size.interfaces import ISized
class MyTypeSized(object):
implements(ISized)
adapts(IMyType)
def __init__(self, context):
self.context = context
def sizeForSorting(self):
return 'bytes', 0
def sizeForDisplay(self):
return u'nada'
Then, the registration in configure.zcml:
<configure xmlns="http://namespaces.zope.org/zope" i18n_domain="my.package">
<adapter
for=".interfaces.IMyType"
provides="zope.size.interfaces.ISized"
factory=".size.MytypeSized"
/>
</configure>
(note: in this case we could omit the for and provides lines, but this is the full syntax)
With Grok convention-over-configuration, you can do it all in one file, like this:
from five import grok
from my.package.interfaces import IMyType
from zope.size.interfaces import ISized
class MyTypeSized(grok.Adapter):
grok.provides(ISized)
grok.context(IMyType)
def sizeForSorting(self):
return 'bytes', 0
def sizeForDisplay(self):
return u'nada'
For this to work, the package needs to be "grokked". This is done with a single statement in configure.zcml, which then grokks all modules in the package:
<configure xmlns="http://namespaces.zope.org/zope" i18n_domain="my.package"
xmlns:grok="http://namespaces.zope.org/grok">
<include package="five.grok" />
<grok:grok package="." />
</configure>
The <include /> statement ensures that the grok directive is available. Once these two lines are in configure.zcml, we should not need to add any more registrations to this file, no matter how many grokked components we added to modules inside this package.
When the configuration is loaded (at "grok time"), various "grokkers" will analyse the code in the package, typically looking for special base classes (like grok.Adapter above), directives (like the grok.provides() and grok.implements() lines above), module-level function calls, directories and files (e.g. page templates), and configure components based on these conventions.
Grok vs. five.grok vs. grokcore
Grok started life as a monolithic framework, but the nice cavemen of the Grok project decided to factor out the various grokkers into multiple smaller packages. Thus, we have packages like martian, the toolkit used to write grokkers, grokcore.component, which contains grokkers for basic component architecture primitives such as adapters and utilities, grokcore.security, which provides for permissions and security declarations, grokcore.view, which provides support for browser views, grokcore.viewlet, which provides support for viewlets, and so on.
five.grok is an integration package for Zope 2 which brings these directives to Zope 2 applications such as Plone. In most Grok documentation, you will see a line like this:
import grok
This is using the standalone Grok framework. The five.grok equivalent is:
from five import grok
As far as possible, the five.grok project aims to make the conventions and syntax used in standalone Grok work identically in Zope 2. If you come across a piece of Grok documentation, chances are you can get it to work in Zope 2 by switching the "import grok" line to "from five import grok", although there are situations where this is not the case. In particular, we tend to use Plone content types instead of Grok "models" and standard add/edit forms instead of the formlib-based forms from Grok.
1.3. Adding five.grok as a dependency
How to install the five.grok package safely
Assuming you already have a suitable package and a buildout, using five.grok should be as simple as depending on it in your setup.py file:
install_requires = [
...
'five.grok',
]
As shown on the previous page, you probably also want this as a minimum in your configure.zcml:
<configure xmlns="http://namespaces.zope.org/zope"
xmlns:grok="http://namespaces.zope.org/grok"
i18n_domain="my.package">
<include package="five.grok" />
<grok:grok package="." />
</configure>
However, if you are using Zope 2.10, you may also need to pin certain eggs in your buildout.cfg. If you are using Dexterity, there are already part of the "known good set" of packages. Otherwise, see the five.grok installation instructions for details.
Naturally, you will need to re-run buildout after editing setup.py and/or buildout.cfg.
2. Core components
Using five.grok to configure adapters, utilities and event subscribers
Table of Contents
Introduction
Dexterity wants to make some things really easy. These are:
- Create a "real" content type entirely through-the-web without having to know programming.
- As a business user, create a schema using visual or through-the-web tools, and augment it with adapters, event handlers, and other Python code written on the filesystem by a Python programmer.
- Create content types in filesystem code quickly and easily, without losing the ability to customise any aspect of the type and its operation later if required.
- Support general "behaviours" that can be enabled on a custom type in a declarative fashion. Behaviours can be things like title-to-id naming, support for locking or versioning, or sets of standard metadata with associated UI elements.
- Easily package up and distribute content types defined through-the-web, on the filesystem, or using a combination of the two.
Philosophy
Dexterity is designed with a specific philosophy in mind. This can be summarised as follows:
- Reuse over reinvention
- As far as possible, Dexterity should reuse components and technologies that already exist. More importantly, however, Dexterity should reuse concepts that exist elsewhere. It should be easy to learn Dexterity by analogy, and to work with Dexterity types using familiar APIs and techniques.
- Small over big
- Mega-frameworks be damned. Dexterity consists of a number of specialised packages, each of which is independently tested and reusable. Furthermore, packages should has as few dependencies as possible, and should declare their dependencies explicitly. This helps keep the design clean and the code manageable.
- Natural interaction over excessive generality
- The Dexterity design was driven by several use cases that express the way in which we want people to work with Dexterity. The end goal is to make it easy to get started, but also easy to progress from an initial prototype to a complex set of types and associated behaviours through step-wise learning and natural interaction patterns. Dexterity aims to consider its users - be they business analysts, light integrators or Python developers, and be they new or experienced - and cater to them explicitly with obvious, well-documented, natural interaction patterns.
- Real code over generated code
- Generated code is difficult to understand and difficult to debug when it doesn't work as expected. There is rarely, if ever, any reason to scribble methods or 'exec' strings of Python code.
- Zope 3 over Zope 2
- Although Dexterity does not pretend to work with non-CMF systems, as many components as possible should work with plain Zope 3, and even where there are dependencies on Zope 2, CMF or Plone, they should - as far as is practical - follow Zope 3 techniques and best practices. Many operations (e.g. managing objects in a folder, creating new objects or manipulating objects through a defined schema) are better designed in Zope 3 than they were in Zope 2.
- Zope concepts over new paradigms
- We want Dexterity to be "Zope-ish". Zope is a mature, well-designed (well, mostly) and battle tested platform. We do not want to invent brand new paradigms and techniques if we can help it.
- Automated testing over wishful thinking
- "Everything" should be covered by automated tests. Dexterity necessarily has a lot of moving parts. Untested moving parts tend to come loose and fall on people's heads. Nobody likes that.
Getting started
Please read the installation guide to get Dexterity up and running.
Then log in to Plone, go to Site Setup, and go to the Dexterity Types control panel to get started creating content types through the web.
Or read the Dexterity developer manual to get started developing Dexterity content types on the filesystem.
This release of Dexterity is compatible with Plone 3, 4, and 4.1.
Upgrading
If you are upgrading from a previous release of Dexterity, you need to:
- Update your buildout with the new versions (or extend the updated KGS), and re-run it.
- Restart Zope.
- Go to the Add-ons control panel in Plone Site Setup, and run the upgrade steps for "Dexterity Content Types" if there are any available.
Documentation
Various documentation is available:
The following documents are not Dexterity-specific, but will likely be useful to users of Dexterity:
Mailing list
The dexterity-development group provides a place to discuss development and use of Dexterity.
Issue tracker
Please report issues in our Google Code issue tracker.
Contributed Packages
The Dexterity known good set (KGS) of version pins includes a number of contributed packages that are not installed by default:
- plone.app.referenceablebehavior
- Adds support for the Archetypes reference engine to Dexterity content, so that Dexterity items can be referenced from Archetypes items. Requires Plone 4.1.
- plone.app.stagingbehavior
- Adds support for staging Dexterity content, based on plone.app.iterate. Requires Plone 4.1.
- plone.app.versioningbehavior
- Adds support for storing historic versions of Dexterity content, based on Products.CMFEditions. Requires Plone 4.0 or greater.
- collective.z3cform.datagridfield
- A z3c.form widget for editing lists of subobjects via a tabular UI.
Contributing
Most Dexterity code is owned by the Plone Foundation and maintained in the Plone svn repository. We're happy to share commit access so that you can share code with us, but first you must sign the Plone contributor agreement.
Dexterity wouldn't be possible without the hard work of a lot of people, including:
- Martin Aspeli
- Jian Aijun
- Wichert Akkerman
- Jonas Baumann
- JC Brand
- David Brenneman
- Thomas Buchberger
- Joel Burton
- Roche Compaan
- Vincent Fretin
- Rok Garbas
- Anthony Gerrard
- Nathan van Gheem
- David Glick
- Craig Haynal
- Wouter Vanden Hove
- Jean-Michel Francois
- Jim Fulton
- Jamie Lentin
- Alex Limi
- Marco Martinez
- Steve McMahon
- Jason Mehring
- Alec Mitchell
- Daniel Nouri
- Ross Patterson
- Franco Pellegrini
- Martijn Pieters
- Maurits van Rees
- Johannes Raggam
- Lennart Regebro
- Laurence Rowe
- Israel Saeta Perez
- Hanno Schlichting
- Christian Schneider
- Carsten Senger
- Jon Stahl
- Carsten Senger
- Eric Steele
- Gaudenz Steinlin
- Dorneles Tremea
- Sean Upton
- Hector Velarde
- Sylvain Viollon
- Matthew Wilkes
- Matt Yoder
- Andi Zeidler
(Please add your name if we have neglected to.)
Release Notes
This release is compatible with Plone 3, 4, 4.1, and 4.2, as long as you use the correct set of version pins from good-py (see the installation guide).
Changes
The following packages have been updated since Dexterity 1.1:
plone.app.dexterity 1.2 & 1.2.1
- Add missing upgrade description to restore Plone 3 compatibility. [davisagli]
- Give a more explicit warning before deleting content types that have existing instances. [davisagli]
- Add validation to prevent giving a type the same name as an existing type. [davisagli]
- Make sure the title and description of new FTIs are stored encoded, and with a default i18n domain of 'plone'. [davisagli]
- Install the profile from collective.z3cform.datetimewidget to enable the Jquery Tools date picker for date/time fields. [davisagli]
- Bugfix: Make sure type short names are validated. [davisagli]
- Bugfix: Fix display of type descriptions in the types control panel. [davisagli]
- Add intro message to Dexterity control panel. [jonstahl, davisagli]
plone.dexterity 1.1.2
- Fix UnicodeDecodeError when getting an FTI title or description with non-ASCII characters. [davisagli]
- When deleting items from a container using manage_delObjects, check for the "DeleteObjects" permission on each item being deleted. This fixes http://code.google.com/p/dexterity/issues/detail?id=252 [davisagli]
plone.schemaeditor 1.2.0
- Display fields from behaviors in the schema preview too. [davisagli]
- Prevent the user from creating fields with names that are reserved for Dublin Core metadata. title and description can still be used as long as the fields are of the correct type. [davisagli]
- Remove unhelpful help text for min_length and max_length fields. [davisagli]
- The schema listing preview now respects autoform hints (such as custom widgets). [davisagli]
- Make new boolean fields use the radio widget by default. The field now appears as "Yes/No" in the list of field types. [davisagli]
- Hide the 'read only' setting for fields. [davisagli]
- Edit field defaults from the schema listing instead of in the field overlays. This simplifies making sure that the default can't be set to invalid values. [davisagli]
- Limit the height of text areas in the schema listing to avoid extra scrolling. [davisagli]
- Fall back to normal traversal if a field isn't found when traversing the schema context. This fixes inline validation for forms on the schema context. [davisagli]
- Make it possible to make the schemaeditor not be the default view of the schema context, by specifying the schemaEditorView attribute on the schema context. [davisagli]
- Added Spanish translation. [hvelarde]
plone.autoform 1.1
- Added the AutoObjectSubForm class to support form hints for object widget subforms. [jcbrand]
plone.app.textfield 1.1
- Provide a version of the RichText field schema for use with plone.schemaeditor. Only the default_mime_type field is exposed for editing through-the-web, with a vocabulary of mimetypes derived from the AllowedContentTypes vocabulary in plone.app.vocabularies (which can be adjusted via Plone's markup control panel). [davisagli]
- Log original exception when a TransformError is raised. [rochecompaan]
plone.app.versioningbehavior 1.1
- Added French translations. [jone]
- Fixed SkipRelations modifier to also work with behaviors which are storing relations in attributes. [buchi]
- Added Spanish translation. [hvelarde]
plone.app.intid 1.0
- Remove includeOverride for five.intid. [thet]
- Fix import step intid-register-content to register content in all Languages if LinguaPlone is installed. [csenger]
plone.formwidget.autocomplete 1.2.3
- Fix <input /> element generation for Internet Explorer; in most cases, the generated element would be lacking the name attribute. [mj]
plone.formwidget.contenttree 1.0.5
- Added Spanish translation [hvelarde]
collective.z3cform.datagridfield 0.10
- Fix bug with moving the last row up. [m-martinez]
z3c.formwidget.query 0.8
- If one of the values to be displayed provides IRoleManager, then check for permission first. [frapell]
Changelog for plone.app.dexterity
1.2.1 - 2012-03-04
- Add missing upgrade description to restore Plone 3 compatibility. [davisagli]
1.2 - 2011-12-20
- Give a more explicit warning before deleting content types that have existing instances. [davisagli]
- Add validation to prevent giving a type the same name as an existing type. [davisagli]
- Make sure the title and description of new FTIs are stored encoded, and with a default i18n domain of 'plone'. [davisagli]
- Install the profile from collective.z3cform.datetimewidget to enable the Jquery Tools date picker for date/time fields. [davisagli]
- Bugfix: Make sure type short names are validated. [davisagli]
- Bugfix: Fix display of type descriptions in the types control panel. [davisagli]
- Add intro message to Dexterity control panel. [jonstahl, davisagli]
1.1 - 2011-11-26
- Added Italian translation. [giacomos]
- Make sure that the intids utility doesn't get destroyed if a product depending on Dexterity is reinstalled. This closes http://code.google.com/p/dexterity/issues/detail?id=239 [davisagli]
- Add upgrade step to add missing UUIDs to Dexterity content items if plone.uuid is present. [davisagli]
- Added Spanish translation. [hvelarde]
1.0.3 - 2011-09-24
- Fix bad release of 1.0.2. [davisagli]
1.0.2 - 2011-09-24
- Bugfix: Fix the deprecated IRelatedItems to still add a form field. [davisagli]
- Bugfix: Make sure subject can still be retrieved as unicode for the categorization behavior now that the Subject accessor returns a bytestring. [davisagli]
1.0.1 - 2011-07-02
- Deprecated the IRelatedItems behavior in this package. It moved to plone.app.relationfield. [davisagli]
- Add no-op "grok" and "relations" extras for forward-compatibility with Dexterity 2.0. [davisagli]
1.0 - 2011-05-20
- Fix publishing dates DateTime/datetime conversions so as not to drift by the timezone delta every save. [elro]
- Make sure cloned types get a new factory. [davisagli]
- Don't override overlay CSS in Plone 4. [davisagli]
- Fixed cloning of types with a period (.) in their short name. [davisagli]
- Allow specifying a type's short name when adding a type. [davisagli]
- Make sure the Basic metadata adapter accesses the content's title attribute directly so it doesn't get encoded. Also make sure encoded data can't be set via this adapter. [davisagli]
1.0rc1 - 2011-04-30
Added upgrade step to install new javascript from plone.formwidget.autocomplete [davisagli]
Added basic support for making TTW changes to schemas defined in filesystem models and code. (Note: This feature will not actually work until some further changes are completed in plone.dexterity.)
In order to support this change, the event handling to serialize schema changes was revised. We now register a single event handler for the SchemaModifiedEvent raised for the schema context. This allows us to keep track of the FTI that changes need to be serialized to on the schema context. The serializeSchemaOnFieldEvent and serializeSchemaOnSchemaEvent handlers were removed from the serialize module and replaced by serializeSchemaContext. The serializeSchema helper remains but is deprecated. [davisagli]
Add MANIFEST.in. [WouterVH]
Add "export" button to types editor. Exports GS-style zip archive of type info for selected types. [stevem]
Fix old jquery alias in types_listing.pt. This closes http://code.google.com/p/dexterity/issues/detail?id=159 [davisagli]
Make display templates fill content-core on Plone 4. [elro]
Add ids to the group fieldsets on display forms. [elro]
Exclude from navigation behavior should be restricted to IDexterityContent. [elro]
1.0b4 - 2011-03-15
- Add a "Name from file name" behavior. [elro]
- Remove the NameFromTitle behavior factory, it is not necessary. [elro]
- Add "Next previous navigation" and "Next previous navigation toggle" behaviors. [elro]
- Add an "Exclude from navigation" behavior. [lentinj]
- Put the folder listing within a fieldset. [lentinj]
1.0b3 - 2011-02-11
- Add a navigation root behavior. [elro]
- Fix decoding error when an encoded description is stored in the FTI. [davisagli]
- Avoid empty <div class="field"> tag for title and description in item.pt and container.pt. [gaudenzius]
- Add locales structure for translations with cs , de, es, eu, fr, ja, nl, pt_BR [toutpt]
- Update french translation [toutpt]
1.0b2 - 2010-08-05
- Fix several XML errors in templates. Needed for Chameleon compatibility. [wichert]
- cloning a type through the dexterity UI in the control panel did not work if the type had a hyphen in it's name. This fixes http://code.google.com/p/dexterity/issues/detail?id=126 [vangheem]
1.0b1 - 2010-04-20
- Require plone.app.jquerytools for the schema editor UI, and make sure it is installed when upgrading. [davisagli]
- Remove unused schemaeditor.css. [davisagli]
- Omit the metadata fields except on edit and add forms. [davisagli]
- Enable the "Name from title" behavior for new types, by default. [davisagli]
- Include plone.formwidget.namedfile so that File upload and Image fields are available out of the box. You must explicitly include z3c.blobfile in your environment if you want blob-based files. [davisagli]
- Added a DexterityLayer that can be used in tests. [davisagli]
- Fix issue with the BehaviorsForm accidentally polluting the title of the z3c.form EditForm 'Apply' button. [davisagli]
- Add upgrades folder and make sure plone.app.z3cform profile gets installed on upgrades from previous versions of Dexterity. [davisagli]
- Depend on the plone.app.z3cform profile, to make sure the Plone browser layer for z3c.form gets installed. [davisagli]
- Avoid relying on acquisition to get the portal_url for links in the type listing table. [davisagli]
1.0a7 - 2010-01-08
- Make sure the Dublin Core fieldsets appear in the same order as they do in AT content. [davisagli]
- Make sure the current user is loaded as the default creator for the IOwnership schema in an add form. [davisagli]
- Include behavior descriptions on the behavior edit tab. [davisagli]
- IBasic behavior: set missing_value of description-field to u'' . The description should never be None (live_search would not work any more). [jbaumann]
- Fix issue where traversing to a nonexistent type name in the types control panel did not raise NotFound. [davisagli]
- Make it possible to view the fields of non-editable schemata. [davisagli]
- Tweaks to the tabbed_forms template used for the types control panel. [davisagli]
1.0a6 - 2009-10-12
- Add plone.app.textfield as a dependency. We don't use it directly in this package, but users of Dexterity should have it installed and available. [optilude]
- Use some default icons for new types. [davisagli]
- Show type icons in type listing if available. [davisagli]
- Removed 'container' field from the types listing in the control panel (it wasn't working). [davisagli]
- Add message factories to titles and descriptions of metadata schema fields. Fixes http://code.google.com/p/dexterity/issues/detail?id=75. [optilude]
- Patch listActionInfos() instead of listActions() in order to get the folder/add category into the actions list. This avoids a problem with the 'actions.xml' export handler exporting the folder/add category incorrectly. Fixes http://code.google.com/p/dexterity/issues/detail?id=78 [optilude]
1.0a5 - 2009-07-26
- Explicitly include overrides.zcml from plone.app.z3cform. [optilude]
1.0a4 - 2009-07-12
Changed API methods and arguments to mixedCase to be more consistent with the rest of Zope. This is a non-backwards-compatible change. Our profuse apologies, but it's now or never. :-/
If you find that you get import errors or unknown keyword arguments in your code, please change names from foo_bar too fooBar, e.g. serialize_schema() becomes serializeSchema(). [optilude]
1.0a3 - 2009-06-07
- Updated use of <plone:behavior /> directive to match plone.behavior 1.0b4. [optilude]
1.0a2 - 2009-06-01
- Remove superfluous <includeOverrides /> in configure.zcml which would cause a problem when the package is loaded via z3c.autoinclude.plugin [optilude]
1.0a1 - 2009-05-27
- Initial release
2.1. Interfaces
Describing functionality with interfaces
There is nothing Grok-specific about interfaces, but they are important because they used in various directives for describing or registering components.
Zope interfaces are implemented in the zope.interface package. In addition, zope.schema contains various classes that can be used to describe the type of attributes on an interface (the Dexterity developer manual contains a reference).
Interfaces are typically found in an interfaces.py module, although you will sometimes see schema interfaces kept in the same module as other code (e.g. content classes, event handlers) related to the content type described by that schema.
The simplest interface is a marker interface. This is used as a flag which can either be applied or not to a particular object. A marker interface may look like this:
from zope.interface import Interface
class IImportant(Interface):
""Marker interface used for important objects
""
Notice how we have a docstring on the interface. Interfaces are useful as documentation, and you should endeavour to describe their purpose and behaviour in docstrings on the interface and on any attributes or methods (see below).
Also note that although an interface is created using the class keyword, they are in fact instances of type InterfaceClass. For the most part, you don't need to worry about this.
Interfaces are said to be implemented by classes, in which case instances of that class is said to provide the interface.
from five import grok
class ImportantStuff(object):
grok.implements(IImportant)
...
Note: The grok.implements() directive is just a convenience alias for the implements() directive from zope.interface.
Adherence to a given interface can be tested like this:
>>> IImportant.implementedBy(ImportantStuff) True >>> stuff = ImportantStuff() >>> IIimportant.providedBy(stuff) True
Again, note that we perform an "implements" check against the class and a "provides" check against an instance.
It is also possible to apply an interface directly to an instance. This is mostly relevant to marker interfaces, since other interfaces promise attributes and methods that you usually cannot guarantee that the object will provide.
>>> from zope.interface import alsoProvides >>> alsoProvides(someObject, IImportant)
Let's now take a look at a non-marker interface. This one promises several attributes and methods. Typing and constraints for attributes are described by zope.schema fields.
from zope.interface import Interface
from zope import schema
class IMessage(Interface):
"""An email-like message
"""
subject = schema.TextLine(title=u"Subject")
recipients = schema.Tuple(title=u"Recipients",
description=u"A list of email addresses",
value_type=schema.TextLine())
body = schema.Text(title=u"Body", required=False)
def format():
"""Return a formatted string representing the message
"""
Again notice the use of docstrings for methods and titles and descriptions for fields. Also notice how the method does not take the self parameter. If the implementation is a class (as it is likely to be), its methods will of course take the self parameter, but as far as the user of the interface is concerned, this is an implementation detail, and so does not belong in the interface.
Here is a class implementing this interface:
class Message(object):
implements(IMessage)
subject = u""
recipients = ()
body = u""
def format(self):
return "Subject: %s\nTo: %s\n%s" % (self.subject, ', '.join(self.recipients), self.body,)
Like classes, interfaces may inherit one another. The derived interface inherits all the attributes and methods of the parent interface. Objects providing the derived interface must provide all attributes and methods of both interfaces.
class ITestContent(Interface):
"""Base interface for content types
"""
title = schema.TextLine(title=u"Title")
class IDocumentContent(ITestContent):
"""Document content
"""
text = schema.Text(title=u"Body")
class IFileContent(ITestContent):
"""File content
"""
data = schema.Bytes(title=u"Octet stream")
A class may implement more than one interface. In addition, a class automatically implements all interfaces from its base classes (unless you use the implementsOnly() directive from zope.interface).
class ImportantMessageDocument(Message):
grok.implements(IDocumentContent, IImportant)
title = u"Title"
def _getText(self):
return self.body
def _setText(self, value):
self.body = value
text = property(_getText, _setText)
Here, we have implemented text as a property delegating to the body field from the IMessage interface. We inherited the implementation of body from the Message base class, from which we have also indicated the implements() specification for the IMessage interface:
>>> doc = ImportantMessageDocument() >>> IImportant.providedBy(doc) True >>> IMessage.providedBy(doc) True >>> ITestContent.providedBy(doc) True >>> IDocumentContent.providedBy(doc) True >>> IFileContent.providedBy(doc) False
There are a few other things you can do with interfaces, such as specifying interfaces provided by modules (used to specify an API for that module) or classes (e.g. in the case of class objects acting as factories), looping through the interfaces provided by an instance, or adding or removing marker interfaces. None of these is terribly common. See the documentation for zope.interface (including its interfaces) for details.
2.2. Utilities
Singletons and registries with utilities
Utilities are simple components which may be looked up by interface and optionally name. They are used for two purposes:
- To implement a "singleton" - an object which is created in memory once and shared by all clients.
- To implement a registry of objects all providing the same interface. In this case, each object is a named utility.
As with all components, utilities can be local or global. A local utility is installed as a persistent object in a "local component site" (such as a Plone site). Sometimes, we use local utilities as singletons storing persistent objects, although there are better solutions (such as plone.app.registry / plone.registry) for that. More commonly, a local utility is simply used to override a global utility with of same interface (and optionally name).
In Plone, local components can be installed using the componentregistry.xml GenericSetup import step. See the GenericSetup documentation for more details. The techniques mentioned in this manual pertain to global utilities only.
Global utilities can be registered in one of two ways using five.grok:
- By writing a class deriving from GlobalUtility. The class will be used as a utility factory. It will be instantiated once (its constructor must be callable with no arguments), on startup, and registered according to the directives used on the class.
- By calling the global_utility() function on an already-instantiated object. This allows you to configure the instance in code before registering it.
To illustate both of these techniques, we will create two interfaces:
from zope.interface import Interface
from zope import schema
class ILanguage(Interface):
"""A language.
Each language is registered as a named utility providing this interface.
The utility name should be a locale name, e.g. 'en_GB' or 'de'.
"""
title = schema.TextLine(title=u"Name of the language (in English)")
class ILanguagePreference(Interface):
"""Singleton used to look up a preferred language
"""
preferredLanguage = schema.Object(title=u"User's preferred language", schema=ILanguage, readonly=True)
def switch(newPreferredLanguage):
"""Switch preferred languages.
Takes a local name as a parameter)
"""
Before we implement these utilities, let's consider how we may use these two interfaces from client code which does not care about their implementation.
To look up the currently preferred language, we could do:
>>> from zope.component import getUtility
>>> preference = getUtility(ILanguagePreference)
>>> preference.preferredLanguage
<Language object at ...>
>>> preference.switch('en_GB')
Languages are looked up as named utilities. Thus, we could get a language like this:
>>> from zope.component import queryUtility >>> en_GB = queryUtility(ILanguage, name=u"en_GB")
Notes:
- getUtility() will return the default utility for the given interface if no name is passed (in fact, the default utility has a name of u"", i.e. an empty string).
- If no utility can be found, a ComponentLookupError will be raised.
- We can use queryUtility() instead to fall back on another value (None, by default) instead of raising an error if the utility is not found.
- Each time we call getUtility() with the same arguments, we get the same object back. This may lead to thread-safety issues in multi-threaded environments (such as Zope), so be careful if your utility can be modified after startup.
Let's now show how these utilities could be registered. First, we will create a class to encapsulate languages, instantiate a objects of this class, and register each as a named utility providing the ILanguage interface:
from five import grok
class Language(object):
grok.implements(ILanguage)
def __init__(title):
self.title = title
en_GB = Language(u"English (British)")
en_US = Language(u"English (US)")
de = Language(u"German")
grok.global_utility(en_GB, provides=ILanguage, name="en_GB", direct=True)
grok.global_utility(en_US, provides=ILanguage, name="en_US", direct=True)
grok.global_utility(de, provides=ILanguage, name="en_de", direct=True)
Notes:
- The provides argument can be omitted if (as is the case here) the object provides exactly one interface. Otherwise, it is required.
- Name name parameter defaults to u"" and so can be omitted if you are registered an unnamed utility.
- The direct=True argument indicates that the utility instance is being passed as the first argument. The argument should be False if a class or factory is being passed.
Next, we will define the preferred language utility. This time, we will create a utility class and ask five.grok to register an instance of it for us.
from five import grok
import os
class EnvironmentLanguagePreference(grok.GlobalUtility):
"""Language preference taken from the PREFERRED_LANGUAGE environment variable
"""
grok.provides(ILanguagePreference)
@property
def preferredLanguage(self):
envKey = os.environ.get('PREFERRED_LANGUAGE', 'en_US')
return getUtility(ILanguage, envKey)
def switch(self, newPreferredLanguage):
os.environ['PREFERRED_LANGUAGE'] = newPreferredLanguage
Notes:
- The class is recognised as a factory for a global utility from its base class.
- The class does not have a constructor. If it did, it would need to be callable with no arguments.
- The utility's interface is given with the grok.provides() directive. We could also have used grok.implements(), but bear in mind that the class can implement multiple interfaces whilst a utility can provide only one. grok.provides() can only be used once per class and can only be passed a single interface.
- Here, we are registering an unnamed utility. We could have used the grok.name() directive to give the utility a name.
Provided the package is grokked, this is all it takes to register one unnamed and three named global utilities with five.grok.
2.3. Adapters
Adapting from one interface to another with simple adapters and multi-adapters
Adapters are one of the most powerful concepts of the Zope Component Architecture. They underpin a huge amount of the functionality - and "pluggability" - of Zope and Plone. You will often see documentation (or interfaces) that describe how a customisation can be applied by writing an adapter.
As the name suggests, adapters implement the "adapter" software design pattern. In the simplest terms, an adapter is a component that knows how to use an object providing one interface to implement another interface.
The usual analogy is that of international power plugs: a European plug has two prongs and a UK plug has three, but for the most part they do the same job and use the same (or nearly the same) voltage. If you have a UK appliance and you are in a country that has European sockets, you can (re-)use your appliance (and avoid buying a new one) by employing an adapter that makes the UK plug fit into the European socket. (If you've ever lived in the UK, you'll understand why "European" is not a superset of "UK" in the preceding paragraph).
If you prefer duck metaphors, there is an awesome talk by Brandon Craig Rhodes about the concept of an adapter for your viewing pleasure here. It even has sound effects.
In software terms, it is much the same. Let's say that we were writing a Twitter-to-email gateway (because Gmail has lots of storage space and it's important to know when some man in Kuala Lumpur got out of bed). Suppose we have a function that can send an email, expecting an IMessage object. Unfortunately, our input is a tweet, conforming to the ITweet interface:
from zope.interface import Interface
from zope import schema
class IMessage(Interface):
"""An email-like message
"""
subject = schema.TextLine(title=u"Subject")
recipients = schema.Tuple(title=u"Recipients",
description=u"A list of email addresses",
value_type=schema.TextLine())
body = schema.Text(title=u"Body", required=False)
def format():
"""Return a formatted string representing the message
"""
class ITweet(Interface):
"""A microblogging message
"""
from_ = schema.TextLine(title=u"Subject")
message = schema.Text(title=u"The message")
What we need is a way to adapt our ITweet object to an IMessage.
The basic approach is to write a class that implements the attributes and methods of IMessage by delegating to an adapted context object that provides the ITweet interface. And a simple adapter would be just that: a class that we instantiated directly, being passing a tweet in its constructor, and then behaving like an IMessage.
Doing this, however, would tie us down to a specific implementation of the adapter. The ZCA gives us a more powerful alternative by maintaining a registry of adapters. We simply ask for an adapter from the object we have, to the interface we want, and the ZCA searches the registry for the most appropriate one. In code, it looks like this:
def sendAsEmail(context):
"""Convert the object passed in to an email message and send it.
"""
message = IMessage(context)
sendEmail(message)
We could call this function with a tweet like this:
>>> tweet = getLatestTweet() # implementaiton not shown - returns an object providing ITweet >>> sendAsEmail(tweet)
Here, we have assumed that the incoming object is an ITweet, but in reality it could be anything that was adaptable to IMessage. Suddenly, our sendAsEmail() function can be used for Identi.ca messages and those really irritating Facebook status updates as well. All we have to do is register an appropriate adapter from the source type to IMessage, and we're done.
As you may imagine, this technique is very useful in a content management scenario where you may have any number of different content types and you want to implement some common functionality that works across a number of objects.
The syntax we used here is to "call" the interface. This is the most convenient way to look up a simple (unnamed, non-multi) adapter. If no adapter can be found, the ZCA will raise a TypeError. That normally indicates a programming error, so we don't mind, but if you are unsure whether the adapter has been registered, you can fall back to a default value (e.g. None) like this:
>>> message = IMessage(tweet, None)
Registering classes as adapter factories
Now that we have seen what adapters do and how to look them up, let's actually write and register one.
When we register adapters, we are actually registering an adapter factory. This is a callable that takes as an argument the object being adapted, and returns an object providing the desired interface. Each time an adapter is looked up, the ZCA calls the adapter factory to obtain a new adapter instance. (Compare this to utilities, which are instantiated once and shared.)
In most cases, adapter factories are classes implementing a given interface and taking a single parameter in their constructor. (A Python class object is a callable which takes the arguments of the class' __init__() method and returns an instance of the class). The grok.Adapter base class relies on this convention. It even defines the constructor (although you can override it) and stores the adapted object in the variable self.context, as is the convention.
An adapter factory for adapting ITweets to IMessages could thus look like this:
class TweetMessage(grok.Adapter):
"""Adapts an ITweet to an IMessage.
This adapter is readonly. Thus we are strictly speaking only implementing
a subset of the IMessage interface.
"""
grok.provides(IMessage)
grok.context(ITweet)
@property
def subject(self):
return u"New tweet from %s!" % self.context.form_
@property
def recipients(self):
return ('tweetgateway@example.org',)
@property
def body(self):
return self.context.message
def format(self):
return "%s\n%s" % (self.subject, self.body,)
Assuming the package is being grokked, this is all it takes to register an adapter with five.grok.
Notes:
- The adapted object is available as self.context when using the default constructor.
- If you are writing your own constructor, use a signature like "def __init__(self, context):" and store the context argument as self.context. This is not strictly necessary, but it is an almost universal convention. Since the constructor is called when the adapter is looked up, it is best to avoid any resource-intensive operations there. If an error is raised in the constructor, the adaptation will fail with a "could not adapt" TypeError.
- The grok.provides() directive indicates the interface that will be provided by the adapter. If the adapter factory implements a single interface (via grok.implements() or inherited from a base class), this is optional.
- The grok.context() directive indicates what is being adapted. This is usually an interface, but it can also be a class, in which case the adapter is specific to instances of that class (or a subclass). This directive can sometimes be omitted if there is an unambiguous module-level context. This is an instance providing the IContext interface from grokcore.component.interfaces, and will typically be something like a content object. If in doubt, it is always safest to provide an explicit context using the grok.context() directive.
Modelling aspects as adapters
In the example above, we used an adapter to make an object providing one interface conform to another. In this case, the objects were similar in purpose, they just happened to have incompatible interfaces. However, adapters are also frequently used in situations where we are trying to model different aspects of an object as independent components, without having to support every possible feature in a single class.
Consider our message interface again. Let's say that we wanted to be able to email any content object as a message. Is our content object a message? Not at all, but we can provide an adapter to the IMessage interface which models the "messaging" aspect of the content object.
Such an adapter may look something like this (IDocument and implementation not shown, but assume it supports the properties title and text):
from five import grok
class DocumentMessage(grok.Adapter):
grok.provides(IMessage)
grok.context(IDocument)
@property
def subject(self):
return self.context.title
@property
def recipients(self):
return ('intray@example.org',)
@property
def body(self):
return self.context.text
def format(self):
return "%s\n%s" % (self.subject, self.body,)
This is relatively straightforward, and we could imagine having a number of such adapters to represent any number of different content types as messages.
Now consider the alternatives:
- we could write a bespoke email-sending function for each type of content and use if-statements or lookup tables to find the correct one; or
- we could demand that every content type that supported being sent as an email inherited from a mix-in class that provided the required properties.
The latter is the usual approach in the world of object oriented programming, and is fine for small, closed systems. In an open-ended system such as Plone, however, we don't always have the option of mixing new functionality into old code, and classes with "fat" interfaces can become unweildly and difficult to work with.
Customisation with more-specific adapters
So far, we have limited ourselves to having only one adapter per type of context. The ZCA allows us to have multiple possible adapters, leaving it to pick the most appropriate one. Here, "most appropriate" really means "most specific", according to the following rules:
- An adapter registered for a class is more specific than an adapter registered for an interface
- An adapter registered for an interface directly provided by an instance is more specific than an adapter registered for an interface implemented by that object's class
- An adapter registered for an interface listed earlier in the implements() directive is more specific than an adapter registered for an interface listed later
- An adapter registered for an interface implemented by a given class is more specific than an adapter registered for an interface implemented by a base class
- An adapter registered for a given interface provided by an object is more specific than an adapter registered for a base-interface of that interface
- In the case of a multi-adapter (see below), the specificity of the adapter to the first adapted object is more important than the specificity to subsequent adapted objects
These rules are known as "interface resolution order" and are basically equivalent to the "method resolution order" used to determine which method takes precedence in the case of multiple inheritance. Most of the time, this works as you would expect, so you do not need to worry too much about the detail of the rules.
The power of the "more-specific adapter" concept is that we can create a generic adapter for a generic interface, and then provide an override for a more specific interface. Let's say that we had the following hierarchy of interfaces, implemented by different types of content objects (not shown):
from zope.interface import Interface
from zope import schema
class IContent(Interface):
"""A content object
"""
title = schema.TextLine(title=u"Title")
class IDocument(IContent):
"""A document content item
"""
text = schema.TextLine(title=u"Body text")
class IFile(IContent):
"""A file content item
"""
contents = schema.Bytes(title=u"Raw data")
We could now register a generic adapter from IContent to IMessage, which would be usable for any content item providing this interface, including file content, or some future type of content we haven't even thought of yet. Then, we could register a more specific adapter for IDocument, like the one we saw above, to provide more specific behaviour for the document type.
But why stop there? Perhaps we want to be able to mark certain documents as important and have the message subject change? One way to do that is with a marker interface on the instance:
class IImportantDocument(Interface):
"""Marker interface for important documents
"""
We would apply this selectively to instances using alsoProvides() (perhaps in an event handler):
>>> from zope.interface import alsoProvides >>> alsoProvides(urgentDocument, IImportantDocument)
We could then register an adapter for this. We can even re-use our previous adapter factory by subclassing it and overriding only the properties or methods we care about:
class ImportantDocumentMessage(DocumentMessage):
grok.provides(IMessage)
grok.context(IImpotrantDocument)
@property
def subject(self):
return u"URGENT! " + self.context.title
Note: This factory class is grokked as an adapter because it derives from DocumentMessage which in turn derives from grok.Adapter.
If you have a class that derives from one of the special Grok base classes (like grok.Adapter or grok.GlobalUtility), but you do not want it to be grokked, e.g. because it is used only as a base class for other classes, you can use the grok.baseclass() directive with no arguments to disable grokking.
Using a function as an adapter factory
Remember we said that an adapter factory is a "callable" that returns an object providing the appropriate interface? Classes are one type of callable, but the most common callable, of course, is a function. It can be useful to register a function as an adapter factory in situations when you are not actually (or always) instantiating a class to provide the adapter.
As an example, let's say that we wanted to keep a cache of the adapter instances, perhaps because they are resource intensive. In this case, we may either create a new adapter object, or re-use an existing one (in general, we wouldn't do this of course, due to thread safety and state management issues, but it's a useful example). We can't do that in the __init__() method of a class, because that is not called until after the class has been instantiated. Instead, we could use a function as the adapter factory:
from five import grok
@grok.implementer(IMessage)
@grok.adapter(ITweet)
def messageFromTweetAdapter(context):
cached = messageCache.get(context) # dict-like interface; not shown
if cached is not None:
return cached
else: # create a new object
return TweetMessage(context)
Notes:
- The @implementer decorator specifies the interface(s) which will be provided by the returned objects. In the case of an adapter factory, you should pass a single interface, although the decorator can take multiple arguments.
- The @adapter decorator actually registers the adapter. For a single adapter, pass a single interface. For a multi-adapter (see below), you can pass multiple arguments. For a named adapter (see below) you can pass a name=u"name" keyword argument.
Using an instance as an adapter factory
In addition to classes and functions, an instance of a class that has a __call__() method may be used as an adapter factory callable. To register such an object as an adapter factory, we can't use the grok.Adapter base class (since that would register the class as the adapter factory and we want to register the object) or the @adapter decorator. Instead, we use the global_adapter() function.
This is much less common, but can be useful in certain circumstances. Here is an example from the z3c.form library:
from five import grok
from zope.interface import Interface
from zope import schema
import z3c.form.widget import StaticWidgetAttribute
class ISchema(Interface):
"""This schema will be used to power a z3c.form form"""
field = schema.TextLine(title=u"Sample field")
labelOverride = StaticWidgetAttribute(u"Override label", field=ISchema['field'])
grok.global_adapter(labelOverride, name=u"label")
The StaticWidgetAttribute() function returns a callable object that is intended to be registered as an adapter factory. The global_adapter() function takes care of this for us at "grok time". In this case, we have passed the instance and a name (see named adapters, below) because the object provides a single interface and has an "adapts" declaration. If this was not the case, we could use the full syntax:
grok.global_adapter(adapterFactoryInstance, (IAdapted,), IProvided, name=u"name")
Note: The adapted interfaces are passed as a tuple to support multi-adapters (see below).
Named adapters
As we have seen above, adapters - like utilities - can be registered with a name:
- By using the grok.name() directive in the class body of an adapter factory deriving from grok.Adapter.
- By using the name keyword argument to the @adapter function decorator
- By using the name argument to the global_adapter() function
Named adapters are a lot less common than named utilities, but can be useful in a few circumstances:
- You want the user to choose among a number of different implementations at runtime. In this case, you could translate the user's input (or some other external runtime state) to the name of an adapter.
- You want to allow third-party packages to plug in any number of adapters, which you will iterate over and use as appropriate, but you also want to allow an individual named adapter to be overridden for a more specific interface (see also subscription adapters below).
- Most browser components (views, viewlets, resource directories) are in fact implemented as named (multi-)adapters. For a view, the name is the path segment that appears in the URL.
If you want to get a simple (non-multi) adapter by name, use the getAdapter() function:
>>> from zope.component import getAdapter >>> adapted = getAdapter(context, IMessage, name=u"adapter-name")
This will raise a ComponentLookupError if no adapter can be found. There is also a queryAdapter() function which returns None as a fallback instead.
If you want to iterate over all the named adapters that provide a given interface, you can do:
>>> from zope.component import getAdapters >>> for name, adapter in getAdapters((context,), IMessage): ... print "Name gave us", adapter.format()
Note that this function takes a tuple of objects as the context, because it is also used for multi-adapters.
Multi-adapters
So far, our adapters have all adapted a single context. A multi-adapter is an adapter that adapts more than one thing. There are a few reasons to want to do this:
- If you have written an adapter and you find that you need to pass an object to (almost) every one of its methods, you could use a multi-adapter to allow the adapter to be initialised with more than one object.
- The rules of "more specific" adapters applies to each adapted context of multi-adapters. Thus, if you want to allow a component to be swapped out (customised) along multiple dimensions, you could look it up as a multi-adapter.
Multi-adapters are frequently used in browser components (such as views and viewlets), which adapt a context object and the request. This allows multiple views to be registered with the same name, with different implementations based on the type of context (i.e. the "index" view for an IDocument is different to the view of an IFile) or the type of request (i.e. an HTTP request results in a different response to an XML-RPC request). Furthermore, the request may be marked with marker interfaces (known as "browser layers") upon traversal, allowing you to register a different view depending on which layer is in effect.
Browser components are registered using specific grokkers which also take care of things like security and template binding. We will cover those later. For a simple example, however, consider the following:
from zope.interface import Interface
from zope import schema
class IBloggingService(Interface):
"""A blogging service
"""
title = schema.TextLine(title=u"Name of service")
url = schema.URI(title=u"API URL")
class IMicroBloggingService(IBloggingService):
"""A micro-blogging service
"""
maxMessageLength = schema.Int(title=u"Max message length allowed")
class IMessageBroadcaster(Interface):
"""Multi-adapt a context and a blogging service to this interface
"""
def send():
"""Send the context as a message using the given service
"""
We could imagine looking up a multi-adapter like this:
>>> from zope.component import getMultiAdapter() >>> context = Document() # an object providing IDocument >>> service = TwitterService() # an object providing IMicroBloggingService >>> broadcaster = getMultiAdapter((context, service,), IMessageBroadcaster)
This will raise a ComponentLookupError if no suitable adapter can be found. There is also queryMultiAdapter(), which will return None as a fallback.
Like other adapters, a multi-adapter is registered with a callable that acts as the adapter factory. The callable must take one argument for each adapted object (two, in this case). We can register multi-adapters with the @adapter function decorator or the grok.global_adapter() function, as indicated above. More commonly, however, we will use the grok.MultiAdapter base class, like this:
class BloggingBroadcaster(grok.MultiAdapter):
grok.provides(IMessageBroadcaster)
grok.adapts(IContent, IBloggingService)
def __init__(self, context, service):
self.context = context
self.service = service
def send(self):
message = IMessage(self.context)
text = message.format()
print text
class MicroBloggingBroadcaster(grok.MultiAdapter):
grok.provides(IMessageBroadcaster)
grok.adapts(IContent, IMicroBloggingService)
def __init__(self, context, service):
self.context = context
self.service = service
def send(self):
message = IMessage(self.context)
text = message.format()
print text[:self.service.maxMessageLength]
Here, we have registered two multi-adapters, the second more specific than the first. Notice how we have to define a constructor: the base class can't do it for us, since it doesn't know how many things we will adapt or what we may want to call the variables where they are stored.
Subscription adapters
There is one final type of adapter known as a subscription adapter. These can be used when you always want to look up and iterate over all available adapters to a specific interface, as opposed to finding the most specific one. However, it is not possible to override or disable a subscription adapter without removing its registration directly, so most people prefer to use named adapters instead, which allow an adapter with a given name to be overridden for a more specific interface. Like event handlers (which are in fact implemented using subscription adapters), subscription adapters are registered with the <subscriber /> ZCML directive. There is currently no way to register one using Grok conventions.
2.4. Annotations
Using the zope.annotation package
Annotations are a convenient feature used throughout Zope and Plone. They also serve as a useful real-world example of adapters.
The zope.annotation package defines an interface IAnnotations. By adapting an object (often a content item) to this interface, you can get and set values using a dict-like API. These values are then persisted against the context object.
Remember that we often use adapters to model different aspects of an object. A content object may have a basic schema - its content data model - but there may be any number of adapters providing functionality which in turn may need to persist other information. The annotations API provides a simple and consistent storage abstraction for such information.
The basic syntax for using an annotation is:
>>> from zope.annotation.interfaces import IAnnotations >>> annotations = IAnnotations(context) >>> annotations['my.package.key'] = 1234
Notes:
- Since annotations can be used by any number of packages, we tend to use dotted names as keys, to reduce the risk of colissions when two packages are writing annotations onto the same object.
- The values stored in annotation must be persitable. Primitives are fine, as are objects deriving from persistence.Persistent.
- The IAnnotations interface promises all the usual methods of a Python dictionary. For example, you can use setdefault() to set a default value if you are not sure that it has been created yet.
But where are annotations stored? As users of the IAnnotations interface we don't care: that logic is implemented by the IAnnotations adapter. We could implement our own such adapter, but we normally don't: the zope.annotation package comes with an adapter providing IAnnotations and adapting a marker interface IAttributeAnnotations. This stores the values in an efficient, persistent BTree structure. (That BTree happens to be stored as an attribute of the content object called __annotations__, although we don't ever access that directly.)
Most content objects and the request object in Zope and Plone provide this marker interface, making them "annotatable". If you are working on a simpler object, you can declare support for IAttributeAnnotations with a directive like:
from persistence import Persistent
from five import grok
from zope.annotation.interfaces import IAttributeAnnotations
class SomeType(Persistent):
grok.implements(IAttributeAnnotations)
...
Let's now show a more complete example of using annotations. Recall the following interface, which we used to implement a multi-adapter in the previous section:
class IMessageBroadcaster(Interface):
"""Multi-adapt a context and a blogging service to this interface
"""
def send():
"""Send the context as a message using the given service
"""
Let's say that we wanted to count the number of messages sent against each each content object. Here is an implementation of the multi-adapter that uses annotations to do that:
from five import grok
from zope.annotation.interfaces import IAnnotations
class BloggingBroadcaster(grok.MultiAdapter):
grok.provides(IMessageBroadcaster)
grok.adapts(IContent, IBloggingService)
COUNTER_KEY = 'example.messaging.counter'
def __init__(self, context, service):
self.context = context
self.service = service
def send(self):
message = IMessage(self.context)
text = message.format()
annotations = IAnnotations(self.context, None)
if annotations is not None:
messageCount = annotations.get(COUNTER_KEY, 0)
messageCount += 1
annotations[COUNTER_KEY] = messageCount
print "This is message number", messageCount
print text
This code is defensive in that we gracefully handle the case where the context is not annotatable. When it is, we get the current counter (if set), increment it, and then save it back again, before printing the value.
2.5. Events
Registering event handlers and sending events
Zope provides an events system. Various components (e.g the standard add and edit forms) notify any number of event subscribers (also known as event handlers) of a particular event. The subscribers are then executed.
Note that:
- Event subscribers are executed in arbitrary order
- Events are executed synchronously: The code which notifies of the event will block until all event handlers have returned
Each type of event is described by an interface. The implementation of this interface will typically carry some information about the event, which may be useful to event subscribers.
Some events are known as object events. These have an object attribute, giving access to the (content) object that the event relates to. Object events allow event handlers to be registered for a specific type of object as well as a specific type of event.
Some of the most commonly used event types in Plone are shown below. They are all object events (i.e. they derive from zope.component.interfaces.IObjectEvent).
- zope.lifecycleevent.interfaces.IObjectCreatedEvent, fired by the standard add form just after an object has been created, but before it has been added on the container. Note that it is often easier to write a handler for IObjectAddedEvent (see below), because at this point the object has a proper acquisition context. This makes it possible to look up tools using getToolByName(), for example.
- zope.lifecycleevent.interfaces.IObjectModifiedEvent, fired by the standard edit form when an object has been modified
- zope.app.container.interfaces.IObjectAddedEvent, fired when an object has been added to its container. The container is available as the newParent attribute, and the name the new item holds in the container is available as newName.
- zope.app.container.interfaces.IObjectRemovedEvent, fired when an object has been removed from its container. The container is available as the oldParent attribute, and the name the item held in the container is available as oldName.
- zope.app.container.interfaces.IObjectMovedEvent, fired when an object is added to, removed from, renamed in, or moved between containers. This event is a super-type of IObjectAddedEvent andIObjectRemovedEvent, shown above, so an event handler registered for this interface will be invoked for the 'added' and 'removed' cases as well. When an object is moved or renamed, all of oldParent, newParent, oldName and newName will be set.
- Products.CMFCore.interfaces.IActionSucceededEvent, fired when a workflow event has completed. The workflow attribute holds the workflow instance involved, and the action attribute holds the action (transition) invoked.
Of course, you can create your own event types as well. However, for standard CRUD type operations (create, read, update, delete), it is best to use the standard event types with a custom object type rather than creating an object-specific event type.
Registering an event subscriber
Event subscribers can be registered using the subscribe() decorator. This takes at least one argument: the type (interface) of event to subscribe to. For object events, it can take two parameters: the type of object, and the type of event. This allows us to limit an event handler to a particular type of context object.
Here is an example, printing a message every time a CMF content object is added to a folder:
from five import grok
from zope.app.container.interfaces import IObjectAddedEvent
from Products.CMFCore.interfaces import IContentish
@grok.subscribe(IContentish, IObjectAddedEvent)
def printMessage(obj, event):
print "Received event for", obj, "added to", event.newParent
Provided the module is grokked, this is all we have to do to register a new event subscriber. Although this example is trivial, there is no limit to what you can do within an event handler.
Notes:
- The two arguments to the function correspond to the two arguments to the subscribe() decorator. For object events, the first is the object that the event relates to (which will be the same as event.object in most cases). The second is the event instance.
- Obviously, we could use a more specific content type interface if we wanted to be more specific.
- Unlike adapters, you cannot override an event subscriber by using a more specific interface. Each and every applicable event subscriber will be executed when an event is fired.
Creating a custom event type
Creating a new type of event is not much more difficult. Here is an example that involves the sample message broadcasting service we saw in the previous sections:
First, we define an object event type. This would typically be in an interfaces.py module:
from zope.component.interfaces import IObjectEvent
from zope import schema
class IMessageSentEvent(IObjectEvent)
message = schema.Object(title=u"Message", schema=IMessage)
messageCount = schema.Int(title=u"Number of messages so far")
The event implementation itself is simple too. The object attribute is mandated by the IObjectEvent interface.
from five import grok
from zope.component.interfaces import ObjectEvent
class MessageSentEvent(ObjectEvent):
grok.implements(IMessageSentEvent)
def __init__(self, object, message, messageCount):
self.object = object
self.message = message
self.messageCount = messageCount
Here is another implementation of the messaging service, this time broadcasting an event:
from five import grok
from zope.annotation.interfaces import IAnnotations
from zope.event import notify
class BloggingBroadcaster(grok.MultiAdapter):
grok.provides(IMessageBroadcaster)
grok.adapts(IContent, IBloggingService)
COUNTER_KEY = 'example.messaging.counter'
def __init__(self, context, service):
self.context = context
self.service = service
def send(self):
message = IMessage(self.context)
text = message.format()
annotations = IAnnotations(self.context, None)
messageCount = -1
if annotations is not None:
messageCount = annotations.get(COUNTER_KEY, 0)
messageCount += 1
annotations[COUNTER_KEY] = messageCount
print "This is message number", messageCount
notify(MessageSentEvent(self.context, message, messageCount))
print text
Notes:
- We use the notify() function from the zope.event package to broadcast the event.
- The call to notify() will not return until every event subscriber has been executed.
As before, we could now register an event subscriber for this event. Since it is an object event, we can use the two-argument version of the subscribe decorator as shown above. However, we could also have a more general event handler that executes for any type of object. Here is one that simply logs that a message has been sent:
from five import grok
import logging
auditLog = logging.getLogger('auditlog')
@grok.subscriber(IMessageSentEvent)
def log(event):
auditLog.info("Message number %s sent for %s" % (event.messageCount, event.object,))2.6. Further details
Where to find more information about the core components
In this section, we have described how to use five.grok to configure standard Zope Component Architecture components. In fact, the functionality for this can be found in the grokcore.component package, and does not strictly require the use of five.grok: you could use grokcore.component directly. This may be useful if you are trying to create a package that should work with other frameworks using the Zope Toolkit / Zope 3, such as Grok itself.
grokcore.component contains a few other grokkers and helper functions which we have not described here. You are encouraged to read its documentation for details.
If you need to introspect the interfaces of your components, you should also take a look at the zope.interface API, which for example provides functions for enumerating the interfaces implemented by a class or provided by an object.
If you need to introspect the component architecture itself, look up the zope.component API, where you will find methods for enumerating, querying, registering and removing adapters, utilities and event subscribers.
Both of these packages have interfaces containing copious internal documentation.
3. Browser components
Using five.grok to create views, viewlets and resource directories
Table of Contents
Introduction
Dexterity wants to make some things really easy. These are:
- Create a "real" content type entirely through-the-web without having to know programming.
- As a business user, create a schema using visual or through-the-web tools, and augment it with adapters, event handlers, and other Python code written on the filesystem by a Python programmer.
- Create content types in filesystem code quickly and easily, without losing the ability to customise any aspect of the type and its operation later if required.
- Support general "behaviours" that can be enabled on a custom type in a declarative fashion. Behaviours can be things like title-to-id naming, support for locking or versioning, or sets of standard metadata with associated UI elements.
- Easily package up and distribute content types defined through-the-web, on the filesystem, or using a combination of the two.
Philosophy
Dexterity is designed with a specific philosophy in mind. This can be summarised as follows:
- Reuse over reinvention
- As far as possible, Dexterity should reuse components and technologies that already exist. More importantly, however, Dexterity should reuse concepts that exist elsewhere. It should be easy to learn Dexterity by analogy, and to work with Dexterity types using familiar APIs and techniques.
- Small over big
- Mega-frameworks be damned. Dexterity consists of a number of specialised packages, each of which is independently tested and reusable. Furthermore, packages should has as few dependencies as possible, and should declare their dependencies explicitly. This helps keep the design clean and the code manageable.
- Natural interaction over excessive generality
- The Dexterity design was driven by several use cases that express the way in which we want people to work with Dexterity. The end goal is to make it easy to get started, but also easy to progress from an initial prototype to a complex set of types and associated behaviours through step-wise learning and natural interaction patterns. Dexterity aims to consider its users - be they business analysts, light integrators or Python developers, and be they new or experienced - and cater to them explicitly with obvious, well-documented, natural interaction patterns.
- Real code over generated code
- Generated code is difficult to understand and difficult to debug when it doesn't work as expected. There is rarely, if ever, any reason to scribble methods or 'exec' strings of Python code.
- Zope 3 over Zope 2
- Although Dexterity does not pretend to work with non-CMF systems, as many components as possible should work with plain Zope 3, and even where there are dependencies on Zope 2, CMF or Plone, they should - as far as is practical - follow Zope 3 techniques and best practices. Many operations (e.g. managing objects in a folder, creating new objects or manipulating objects through a defined schema) are better designed in Zope 3 than they were in Zope 2.
- Zope concepts over new paradigms
- We want Dexterity to be "Zope-ish". Zope is a mature, well-designed (well, mostly) and battle tested platform. We do not want to invent brand new paradigms and techniques if we can help it.
- Automated testing over wishful thinking
- "Everything" should be covered by automated tests. Dexterity necessarily has a lot of moving parts. Untested moving parts tend to come loose and fall on people's heads. Nobody likes that.
Getting started
Please read the installation guide to get Dexterity up and running.
Then log in to Plone, go to Site Setup, and go to the Dexterity Types control panel to get started creating content types through the web.
Or read the Dexterity developer manual to get started developing Dexterity content types on the filesystem.
This release of Dexterity is compatible with Plone 3, 4, and 4.1.
Upgrading
If you are upgrading from a previous release of Dexterity, you need to:
- Update your buildout with the new versions (or extend the updated KGS), and re-run it.
- Restart Zope.
- Go to the Add-ons control panel in Plone Site Setup, and run the upgrade steps for "Dexterity Content Types" if there are any available.
Documentation
Various documentation is available:
The following documents are not Dexterity-specific, but will likely be useful to users of Dexterity:
Mailing list
The dexterity-development group provides a place to discuss development and use of Dexterity.
Issue tracker
Please report issues in our Google Code issue tracker.
Contributed Packages
The Dexterity known good set (KGS) of version pins includes a number of contributed packages that are not installed by default:
- plone.app.referenceablebehavior
- Adds support for the Archetypes reference engine to Dexterity content, so that Dexterity items can be referenced from Archetypes items. Requires Plone 4.1.
- plone.app.stagingbehavior
- Adds support for staging Dexterity content, based on plone.app.iterate. Requires Plone 4.1.
- plone.app.versioningbehavior
- Adds support for storing historic versions of Dexterity content, based on Products.CMFEditions. Requires Plone 4.0 or greater.
- collective.z3cform.datagridfield
- A z3c.form widget for editing lists of subobjects via a tabular UI.
Contributing
Most Dexterity code is owned by the Plone Foundation and maintained in the Plone svn repository. We're happy to share commit access so that you can share code with us, but first you must sign the Plone contributor agreement.
Dexterity wouldn't be possible without the hard work of a lot of people, including:
- Martin Aspeli
- Jian Aijun
- Wichert Akkerman
- Jonas Baumann
- JC Brand
- David Brenneman
- Thomas Buchberger
- Joel Burton
- Roche Compaan
- Vincent Fretin
- Rok Garbas
- Anthony Gerrard
- Nathan van Gheem
- David Glick
- Craig Haynal
- Wouter Vanden Hove
- Jean-Michel Francois
- Jim Fulton
- Jamie Lentin
- Alex Limi
- Marco Martinez
- Steve McMahon
- Jason Mehring
- Alec Mitchell
- Daniel Nouri
- Ross Patterson
- Franco Pellegrini
- Martijn Pieters
- Maurits van Rees
- Johannes Raggam
- Lennart Regebro
- Laurence Rowe
- Israel Saeta Perez
- Hanno Schlichting
- Christian Schneider
- Carsten Senger
- Jon Stahl
- Carsten Senger
- Eric Steele
- Gaudenz Steinlin
- Dorneles Tremea
- Sean Upton
- Hector Velarde
- Sylvain Viollon
- Matthew Wilkes
- Matt Yoder
- Andi Zeidler
(Please add your name if we have neglected to.)
Release Notes
This release is compatible with Plone 3, 4, 4.1, and 4.2, as long as you use the correct set of version pins from good-py (see the installation guide).
Changes
The following packages have been updated since Dexterity 1.1:
plone.app.dexterity 1.2 & 1.2.1
- Add missing upgrade description to restore Plone 3 compatibility. [davisagli]
- Give a more explicit warning before deleting content types that have existing instances. [davisagli]
- Add validation to prevent giving a type the same name as an existing type. [davisagli]
- Make sure the title and description of new FTIs are stored encoded, and with a default i18n domain of 'plone'. [davisagli]
- Install the profile from collective.z3cform.datetimewidget to enable the Jquery Tools date picker for date/time fields. [davisagli]
- Bugfix: Make sure type short names are validated. [davisagli]
- Bugfix: Fix display of type descriptions in the types control panel. [davisagli]
- Add intro message to Dexterity control panel. [jonstahl, davisagli]
plone.dexterity 1.1.2
- Fix UnicodeDecodeError when getting an FTI title or description with non-ASCII characters. [davisagli]
- When deleting items from a container using manage_delObjects, check for the "DeleteObjects" permission on each item being deleted. This fixes http://code.google.com/p/dexterity/issues/detail?id=252 [davisagli]
plone.schemaeditor 1.2.0
- Display fields from behaviors in the schema preview too. [davisagli]
- Prevent the user from creating fields with names that are reserved for Dublin Core metadata. title and description can still be used as long as the fields are of the correct type. [davisagli]
- Remove unhelpful help text for min_length and max_length fields. [davisagli]
- The schema listing preview now respects autoform hints (such as custom widgets). [davisagli]
- Make new boolean fields use the radio widget by default. The field now appears as "Yes/No" in the list of field types. [davisagli]
- Hide the 'read only' setting for fields. [davisagli]
- Edit field defaults from the schema listing instead of in the field overlays. This simplifies making sure that the default can't be set to invalid values. [davisagli]
- Limit the height of text areas in the schema listing to avoid extra scrolling. [davisagli]
- Fall back to normal traversal if a field isn't found when traversing the schema context. This fixes inline validation for forms on the schema context. [davisagli]
- Make it possible to make the schemaeditor not be the default view of the schema context, by specifying the schemaEditorView attribute on the schema context. [davisagli]
- Added Spanish translation. [hvelarde]
plone.autoform 1.1
- Added the AutoObjectSubForm class to support form hints for object widget subforms. [jcbrand]
plone.app.textfield 1.1
- Provide a version of the RichText field schema for use with plone.schemaeditor. Only the default_mime_type field is exposed for editing through-the-web, with a vocabulary of mimetypes derived from the AllowedContentTypes vocabulary in plone.app.vocabularies (which can be adjusted via Plone's markup control panel). [davisagli]
- Log original exception when a TransformError is raised. [rochecompaan]
plone.app.versioningbehavior 1.1
- Added French translations. [jone]
- Fixed SkipRelations modifier to also work with behaviors which are storing relations in attributes. [buchi]
- Added Spanish translation. [hvelarde]
plone.app.intid 1.0
- Remove includeOverride for five.intid. [thet]
- Fix import step intid-register-content to register content in all Languages if LinguaPlone is installed. [csenger]
plone.formwidget.autocomplete 1.2.3
- Fix <input /> element generation for Internet Explorer; in most cases, the generated element would be lacking the name attribute. [mj]
plone.formwidget.contenttree 1.0.5
- Added Spanish translation [hvelarde]
collective.z3cform.datagridfield 0.10
- Fix bug with moving the last row up. [m-martinez]
z3c.formwidget.query 0.8
- If one of the values to be displayed provides IRoleManager, then check for permission first. [frapell]
Changelog for plone.app.dexterity
1.2.1 - 2012-03-04
- Add missing upgrade description to restore Plone 3 compatibility. [davisagli]
1.2 - 2011-12-20
- Give a more explicit warning before deleting content types that have existing instances. [davisagli]
- Add validation to prevent giving a type the same name as an existing type. [davisagli]
- Make sure the title and description of new FTIs are stored encoded, and with a default i18n domain of 'plone'. [davisagli]
- Install the profile from collective.z3cform.datetimewidget to enable the Jquery Tools date picker for date/time fields. [davisagli]
- Bugfix: Make sure type short names are validated. [davisagli]
- Bugfix: Fix display of type descriptions in the types control panel. [davisagli]
- Add intro message to Dexterity control panel. [jonstahl, davisagli]
1.1 - 2011-11-26
- Added Italian translation. [giacomos]
- Make sure that the intids utility doesn't get destroyed if a product depending on Dexterity is reinstalled. This closes http://code.google.com/p/dexterity/issues/detail?id=239 [davisagli]
- Add upgrade step to add missing UUIDs to Dexterity content items if plone.uuid is present. [davisagli]
- Added Spanish translation. [hvelarde]
1.0.3 - 2011-09-24
- Fix bad release of 1.0.2. [davisagli]
1.0.2 - 2011-09-24
- Bugfix: Fix the deprecated IRelatedItems to still add a form field. [davisagli]
- Bugfix: Make sure subject can still be retrieved as unicode for the categorization behavior now that the Subject accessor returns a bytestring. [davisagli]
1.0.1 - 2011-07-02
- Deprecated the IRelatedItems behavior in this package. It moved to plone.app.relationfield. [davisagli]
- Add no-op "grok" and "relations" extras for forward-compatibility with Dexterity 2.0. [davisagli]
1.0 - 2011-05-20
- Fix publishing dates DateTime/datetime conversions so as not to drift by the timezone delta every save. [elro]
- Make sure cloned types get a new factory. [davisagli]
- Don't override overlay CSS in Plone 4. [davisagli]
- Fixed cloning of types with a period (.) in their short name. [davisagli]
- Allow specifying a type's short name when adding a type. [davisagli]
- Make sure the Basic metadata adapter accesses the content's title attribute directly so it doesn't get encoded. Also make sure encoded data can't be set via this adapter. [davisagli]
1.0rc1 - 2011-04-30
Added upgrade step to install new javascript from plone.formwidget.autocomplete [davisagli]
Added basic support for making TTW changes to schemas defined in filesystem models and code. (Note: This feature will not actually work until some further changes are completed in plone.dexterity.)
In order to support this change, the event handling to serialize schema changes was revised. We now register a single event handler for the SchemaModifiedEvent raised for the schema context. This allows us to keep track of the FTI that changes need to be serialized to on the schema context. The serializeSchemaOnFieldEvent and serializeSchemaOnSchemaEvent handlers were removed from the serialize module and replaced by serializeSchemaContext. The serializeSchema helper remains but is deprecated. [davisagli]
Add MANIFEST.in. [WouterVH]
Add "export" button to types editor. Exports GS-style zip archive of type info for selected types. [stevem]
Fix old jquery alias in types_listing.pt. This closes http://code.google.com/p/dexterity/issues/detail?id=159 [davisagli]
Make display templates fill content-core on Plone 4. [elro]
Add ids to the group fieldsets on display forms. [elro]
Exclude from navigation behavior should be restricted to IDexterityContent. [elro]
1.0b4 - 2011-03-15
- Add a "Name from file name" behavior. [elro]
- Remove the NameFromTitle behavior factory, it is not necessary. [elro]
- Add "Next previous navigation" and "Next previous navigation toggle" behaviors. [elro]
- Add an "Exclude from navigation" behavior. [lentinj]
- Put the folder listing within a fieldset. [lentinj]
1.0b3 - 2011-02-11
- Add a navigation root behavior. [elro]
- Fix decoding error when an encoded description is stored in the FTI. [davisagli]
- Avoid empty <div class="field"> tag for title and description in item.pt and container.pt. [gaudenzius]
- Add locales structure for translations with cs , de, es, eu, fr, ja, nl, pt_BR [toutpt]
- Update french translation [toutpt]
1.0b2 - 2010-08-05
- Fix several XML errors in templates. Needed for Chameleon compatibility. [wichert]
- cloning a type through the dexterity UI in the control panel did not work if the type had a hyphen in it's name. This fixes http://code.google.com/p/dexterity/issues/detail?id=126 [vangheem]
1.0b1 - 2010-04-20
- Require plone.app.jquerytools for the schema editor UI, and make sure it is installed when upgrading. [davisagli]
- Remove unused schemaeditor.css. [davisagli]
- Omit the metadata fields except on edit and add forms. [davisagli]
- Enable the "Name from title" behavior for new types, by default. [davisagli]
- Include plone.formwidget.namedfile so that File upload and Image fields are available out of the box. You must explicitly include z3c.blobfile in your environment if you want blob-based files. [davisagli]
- Added a DexterityLayer that can be used in tests. [davisagli]
- Fix issue with the BehaviorsForm accidentally polluting the title of the z3c.form EditForm 'Apply' button. [davisagli]
- Add upgrades folder and make sure plone.app.z3cform profile gets installed on upgrades from previous versions of Dexterity. [davisagli]
- Depend on the plone.app.z3cform profile, to make sure the Plone browser layer for z3c.form gets installed. [davisagli]
- Avoid relying on acquisition to get the portal_url for links in the type listing table. [davisagli]
1.0a7 - 2010-01-08
- Make sure the Dublin Core fieldsets appear in the same order as they do in AT content. [davisagli]
- Make sure the current user is loaded as the default creator for the IOwnership schema in an add form. [davisagli]
- Include behavior descriptions on the behavior edit tab. [davisagli]
- IBasic behavior: set missing_value of description-field to u'' . The description should never be None (live_search would not work any more). [jbaumann]
- Fix issue where traversing to a nonexistent type name in the types control panel did not raise NotFound. [davisagli]
- Make it possible to view the fields of non-editable schemata. [davisagli]
- Tweaks to the tabbed_forms template used for the types control panel. [davisagli]
1.0a6 - 2009-10-12
- Add plone.app.textfield as a dependency. We don't use it directly in this package, but users of Dexterity should have it installed and available. [optilude]
- Use some default icons for new types. [davisagli]
- Show type icons in type listing if available. [davisagli]
- Removed 'container' field from the types listing in the control panel (it wasn't working). [davisagli]
- Add message factories to titles and descriptions of metadata schema fields. Fixes http://code.google.com/p/dexterity/issues/detail?id=75. [optilude]
- Patch listActionInfos() instead of listActions() in order to get the folder/add category into the actions list. This avoids a problem with the 'actions.xml' export handler exporting the folder/add category incorrectly. Fixes http://code.google.com/p/dexterity/issues/detail?id=78 [optilude]
1.0a5 - 2009-07-26
- Explicitly include overrides.zcml from plone.app.z3cform. [optilude]
1.0a4 - 2009-07-12
Changed API methods and arguments to mixedCase to be more consistent with the rest of Zope. This is a non-backwards-compatible change. Our profuse apologies, but it's now or never. :-/
If you find that you get import errors or unknown keyword arguments in your code, please change names from foo_bar too fooBar, e.g. serialize_schema() becomes serializeSchema(). [optilude]
1.0a3 - 2009-06-07
- Updated use of <plone:behavior /> directive to match plone.behavior 1.0b4. [optilude]
1.0a2 - 2009-06-01
- Remove superfluous <includeOverrides /> in configure.zcml which would cause a problem when the package is loaded via z3c.autoinclude.plugin [optilude]
1.0a1 - 2009-05-27
- Initial release
3.1. Views
Browser views with and without templates
Browser views (or just "views") are the most common form of display technology in Zope. When you view a web page in Plone, chances are it was rendered by a view[1].
At the most basic level, a view is a component (in fact, a named multi-adapter) that is looked up during traversal (i.e. when Zope interpreters a URL) and then called by the Zope publisher to obtain a string of HTML to return to the browser. That normally involves a page template, although it is possible to construct the response in code as well. Sometimes, the view may not return anything. One reason may be that it results in a redirect. Furthermore, some views are not designed to be invoked from URL traversal, instead containing utility methods which are looked up from other views or components.
Views with templates
The most common type of view involves a Python class and an associated page template. The Python class is used to register the view. An instance of the class is also available in the template, under the name view. This provides a natural home for "display logic" - calculations or preparation of data intended only for the view.
As a rule of thumb, try to keep the page template free from complex expressions. Python code is much easier to debug and test.
Here is an example of a view class which registers a view and provides some helper methods and attributes. It also prepares some variables for the view in the update() method, which is called just before the view is rendered. Obviously, we could have omitted these things if they were not necessary, in which case the Python class would serve only as a place to hang the view's registration.
This class could go in any Python module. For generic views, browser.py is a good choice.
from five import grok
from Acquisition import aq_inner
class AsMessage(grok.View):
"""Render a document as a message
"""
grok.context(IDocument)
grok.requires('zope2.View')
grok.name('as-message')
def update(self):
context = aq_inner(self.context)
self.message = IMessage(context)
def truncatedBody(self, maxLength=1000):
return self.message.body[:maxLength
The automatically associated template is shown below. If the Python module was browser.py, this would be found in a directory browser_templates/asmessage.pt in the same package. The directory name is taken from the module name (with _templates appended); the filename is taken from the class name (in all lowercase, with a .pt extension).
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
lang="en"
metal:use-macro="context/main_template/macros/master"
i18n:domain="example.conference">
<body>
<metal:main fill-slot="main">
<tal:main-macro metal:define-macro="main">
<div tal:replace="structure provider:plone.abovecontenttitle" />
<h1 class="documentFirstHeading">Message view</h1>
<div tal:replace="structure provider:plone.belowcontenttitle" />
<p class="documentDescription">This is the message view of the content object</p>
<div tal:replace="structure provider:plone.abovecontentbody" />
<div>
<label>Subject:</label> <span tal:content="view/message/subject" />
</div>
<div>
<label>Body:</label> <span tal:content="view/truncatedBody" />
</div>
<div tal:replace="structure provider:plone.belowcontentbody" />
</tal:main-macro>
</metal:main>
</body>
</html>
If we now had a content object providing IDocument reachable at http://example.org/my-document, we would be able to invoke this view using a URL like http://example.org/my-document/@@as-message. See the Dexterity Developer Manual for more information about how to register default and alternative views for content items.
Notes:
- The class will grokked as a view because it derives from grok.View. This in turn defines a constructor which saves the context content object as self.context and the request as self.request.
- We register the view for a specific type of content object using grok.context(), which we have already seen in the context of adapters. Here, we have used the IDocument interface from earlier in this manual. If there is a module-level context, this can be omitted.
- We give the view a name using grok.name(). This corresponds to the path segment in the URL. This directive is optional. The default view name is the name of the class in all lowercase, e.g. "asmessage" in this case.
- We specify a permission required to access the view using grok.require(). This directive is required. You can pass "zope2.Public" to indicate that the view does not require any permissions at all. Other common permissions include zope2.View, cmf.ModifyPortalContent and cmf.ManagePortal. See the Dexterity Developer Manual for more information about permissions and workflow.
- We override the update() method, which is called by the base class before the view is rendered. This is a good place to pre-calculate values used in the template and process any request variables (see the section on forms below). Since views are transient objects instantiated on the fly, we can safely store values on the view object itself. Here, we have looked up an IMessage adapter (from the adapter examples earlier in this manual) and stored it in self.message. This is available in the template as view/message.
- In the update() method, we use the aq_inner() function on self.context to avoid possible problems with the view being part of the acquisition chain of self.context. If that didn't make any sense, better not to worry about why this is necessary. Nine times of out ten, you won't have a problem if you just use self.context directly, but since the tenth time is quite hard to debug, it's a good habit to get into.
- We have also defined a custom method, which we use in the template via a TAL expression.
- In the template, we use the master macro of Plone's main_template to get the standard Plone look-and-feel, and include a number of standard viewlet managers (see the section on viewlets later in this manual) to provide standard UI elements.
- We use a number of TAL expressions to render information from the context (the IDocument object) and the view instance (in particular, the view.message object we set in the update() method). See the ZPT tutorial for more details on the TAL syntax.
Views without templates
Sometimes, we do not need a template. In this case, we can override the render() method of the grok.View base class to return a string, which is then returned to the browser as the response body.
Below is an example that builds a CSV file of the recipients of the message representation of the context. By setting appropriate response headers, this view ensures that the browser will attempt to download that generated file, rather than display a plain text response.
from StringIO import StringIO
import csv
from five import grok
from Acquisition import aq_inner
class MessageRecipients(grok.View):
"""Return a CSV file with message recipients
"""
grok.context(IDocument)
grok.requires('zope2.View')
grok.name('message-recipients')
def update(self):
context = aq_inner(self.context)
self.message = IMessage(context)
def render(self):
out = StringIO()
context = aq_inner(self.context)
writer = csv.writer(out)
# Write header
writer.writerow(('Email address', 'Subject'))
subject = self.message.subject
# Write body
for recipient in self.message.recipients:
writer.writerow((recipient, subject,))
# Prepare response
filename = "Recipients for %s.csv" % context.title
self.request.response.setHeader('Content-Type', 'text/csv')
self.request.response.setHeader('Content-Disposition', 'attachment; filename="%s"' % filename)
return out.getvalue()
Notes:
- We use the Python csv module to build the output string.
- We return a string, which represents the response body.
- We set the Content-Type repsonse header to indicate to the browser that the return value should be opened as a spreadsheet.
- We set the Content-Disposition response header to indicate that the return value should be treated as a separate file rather than opened in the browser, and suggest a filename for the download.
Implementing simple forms
Dexterity uses the powerful z3c.form library to provide forms based on schemata defined in Python or through-the-web, including validation and standardised widgets. Sometimes, though, we just want a simple HTML form and a bit of logic to process request parameters. One common way to implement this is with a view that defines a form, which submits back to itself. The form is processed in the update() method of the view class.
The example below shows a simple form which allows users to subscribe to a content object with an email address. The list of subscribers is stored in an annotation (as described earlier in this manual).
from five import grok
from Acquisition import aq_inner
from BTrees.OOBTree import OOSet
from zope.annotation.interfaces import IAnnotatablel, IAnnotations
class Subscribe(grok.View):
"""Allow users to subscribe to an item
"""
grok.context(IAnnotatable)
grok.requires('zope2.View')
def update(self):
context = aq_inner(self.context)
# A dictionary of items submitted in a POST request
form = self.request.form
self.errors = {}
if 'form.button.Subscribe' in self.request:
email = self.request.get('email', None)
if email is None:
self.errors['email'] = "Email address is required"
else:
annotations = IAnnotations(context)
addresses = annotations.setdefault('example.grok.subscriptions', OOSet())
if email in addresses:
self.errors['email'] = "Email address already subscribed"
else:
addresses.add(email)
self.request.response.redirect(self.context.absolute_url() + "/view")
Here is the form template. Assuming the view was put in a module subscription.py, the template would be in subscription_templates/subscribe.pt.
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
lang="en"
metal:use-macro="context/main_template/macros/master"
i18n:domain="example.conference">
<body>
<metal:main fill-slot="main">
<tal:main-macro metal:define-macro="main">
<div tal:replace="structure provider:plone.abovecontenttitle" />
<h1 class="documentFirstHeading">Subscribe</h1>
<div tal:replace="structure provider:plone.belowcontenttitle" />
<div tal:replace="structure provider:plone.abovecontentbody" />
<form tal:attributes="action request/URL" method="post">
<div class="field">
<div class="error"
tal:condition="view/errors/email|nothing"
tal:content="view/errors/email|nothing" />
<label for="email">Email address:</label>
<input type="text" id="email" name="email" />
</div>
</form>
<div tal:replace="structure provider:plone.belowcontentbody" />
</tal:main-macro>
</metal:main>
</body>
</html>
To make the example more realistic, we would obviously also need to write some code to help manage the list of subscribers, allowing users to un-subscribe and so on, as well as some functionality to actually use the list. These could potentially be created as other views in the same module. Their templates would then also go in the subscription_templates directory.
Notes:
- We've omitted the grok.name() directive, so the view name will be @@subscribe.
- We register the form for a generic interface so that it can be used on any annotatable context.
- We use a redirect if the form is successfully submitted. The grok.View base class is smart enough to avoid invoking any associated template or overridden render() method if a redirect takes place.
- We use self.request.form to inspect the submitted form. This dictionary contains form values submitted via a POST request. For a GET request, use self.request.get() to obtain parameters.
- We use an OOSet as an efficient persistent storage of subscription email addresses.
Utility views
Not all views are meant to be rendered. Sometimes, a view provides utility methods that may be used from other views. Plone has a few such views in the plone.app.layout.globals package:
- plone_portal_state, which gives access to site-wide information, such as the URL of the navigation root.
- plone_context_state, which gives access to context-specific information, such as an item's URL or title.
- plone_tools, which gives access to common tools, such as portal_membership or portal_catalog.
See the interfaces.py module in plone.app.layout.globals for details. In a template, we would look up these with a TAL expression like:
<div tal:define="context_state nocall:context/@@plone_context_state;
viewUrl context_state/view_url;">
<a tal:attributes="href viewUrl">View URL</a>
</div>
In code, we could perform the same lookup like so (note that we need a context object and the request; in a view, we'd normally get these from self.context and self.request):
>>> from zope.component import getMultiAdapter >>> context_state = getMultiAdapter((context, request,), name=u"plone_context_state") >>> viewUrl = context_state.view_url()
A utility view is registered like any other view. If you are using grok.View to register one, you should return an empty string from the render() method. You also should not use update(), since it may not be called for you. Instead, define methods and attributes that can be accessed independently. Here is an example:
from five import grok
from Acquisition import aq_inner
from plone.memoize import view
from Products.CMFCore.interfaces import IContentish
class MessageInfo(grok.View):
"""Utility view to quickly access message aspects of
an object.
"""
grok.context(IContentish)
grok.requires('zope2.View')
grok.name('message-info')
def render(self):
"""No-op to keep grok.View happy
"""
return ''
@view.memoize
def recipients(self):
message = self._message()
if message is None:
return None
return message.recipients
...
@view.memoize
def _message(self):
"""Get the message representation of the context
"""
context = aq_inner(self.context)
return IMessage(context, None)
Notes:
- We have implemented an empty render() method to satisfy grok.View.
- We have used plone.memoize to lazily cache variables. The @view.memoize decorator will cache each value for the duration of the request. See plone.memoize's interfaces.py for more details.
- We're being defensive and returning None in the cases where the IMessage adapter cannot be looked up.
Overriding views
Recall that views are implemented behind the scenes as named multi-adapters. One consequence if this is that it is possible to override a view with a given name by using the more-specific adapter concept. You can:
- Register a view with the same name as an existing view, specifying a more specific context interface with grok.context()
- Register a view with the same name as an existing view, specifying a more specific type of request with grok.layer().
The term "layer" here relates to the concept of a "browser layer". Upon traversal, the request may be marked with one or more marker interfaces. In Plone, this normally happens in one of two ways:
- A browser layer can be automatically associated with the currently active Plone theme. This magic is performed using the plone.theme package.
- One or more browser layers can be activated when a particular product is installed in a Plone site. The plone.browserlayer package supports this via the browserlayer.xml GenericSetup syntax. See the Dexterity Developer Manual for more information about creating a GenericSetup profile.
For example, the following class (view implementation and template not shown) could be used to override a view for a specific layer:
from five import grok
...
class AsMessage(grok.View):
"""Render a document as a message
"""
grok.context(IDocument)
grok.layer(IMessageOverrides)
grok.requires('zope2.View')
grok.name('as-message')
...
Notes:
- The grok.layer() directive takes an interface as its only argument. This should be a layer marker interface. In this case, we have assumed that we have an IMessageOverrides layer.
- We've used the same name and context as the default implementation of the view.
- We've also used the same permission. It is possible to change the permission, but in most cases this would just be confusing.
- We will also sometimes use layers not to override an existing view, but to ensure that the view is not available until a package has been installed into a Plone site (in the case of a layer registered with browserlayer.xml) or a given theme is active (in the case of a theme-specific layer).
You can use five.grok to override any browser view, not just those registered with five.grok. For a simpler way to override templates (but not Python logic), you may also want to look into z3c.jbot.
For more details on this topic, see the Customization for Developers tutorial.
[1] As of Plone 3, that's not entirely true: an older technology known as skin layer templates are used for many of the standard pages, but the principles behind them are the same.
3.2. Viewlets
Dynamic snippets with viewlets
Viewlets, as their name suggests, are "little views" - snippets of HTML that are rendered at defined location in a view. Behind the scenes, a viewlet is a named browser component that is registered for a context and a request (like a view), as well as for a view and a viewlet manager.
In a page template, you may see something this:
<div tal:replace="structure provider:plone.belowtitle" />
This tells Zope to look up and render the content provider with the name plone.belowtitle. The most common type of content provider is a viewlet manager. When the viewlet manager is rendered, it will look up any viewlets which are registered for that viewlet manager, and which are applicable to the current context (content object), request (browser layer), and view (the same provider expression can be used in multiple templates, but sometimes we only want a viewlet to show up for a particular view). These are then rendered into the page template.
Plone comes with a number of standard viewlet managers, covering various areas of the page which you may want to plug viewlets into. The standard viewlet managers are all defined in the package plone.app.layout.viewlets. In its configure.zcml, you will see a number of sections like this:
<browser:viewletManager
name="plone.htmlhead"
provides=".interfaces.IHtmlHead"
permission="zope2.View"
class="plone.app.viewletmanager.manager.BaseOrderedViewletManager"
/>
This shows that we have a viewlet manager with the name plone.htmlhead, identifiable via the interface plone.app.layout.viewlets.interfaces.IHtmlHead.
Another way to find viewlet managers is to use the @@manage-viewlets view: simply append /@@manage-viewlets to the end of the URL of a content item in Plone, and you will see the viewlet managers and viewlets that make up various parts of the page. You can find the various viewlet manager names and interfaces on this screen as well.
Registering a viewlet
With five.grok, we can register a viewlet using the grok.Viewlet base class:
from five import grok
from plone.app.layout.viewlets.interfaces import IAboveContent
...
class MessageViewlet(grok.Viewlet):
"""Display the message subject
"""
grok.name('example.messaging.MessageViewlet")
grok.context(IDocument)
grok.require('zope2.View')
grok.viewletmanager(IAboveContent)
def update(self):
self.message = IMessage(self.context)
Notes:
- We use grok.name() to give the viewlet a name. If this were omitted, the name would be taken from the class name, in all lowercase. The name is primarily useful for overriding the viewlet for a more specific context or layer later, but it must be unique, so it is a good idea to use a dotted name based on the package name.
- We use grok.context() to limit this viewlet to a particular content type, described by the IDocument interface from earlier examples (not shown). We could omit this, in which case the viewlet would be shown for any type of context where the viewlet manager is rendered.
- As with a view, we have to specify a permission required to see the viewlet, using grok.require(). If the user does not have the required permission, the viewlet will simply be omitted.
- We override the update() method to prepare some data for the template, much like we did for the view in the previous section. We could also define additional properties or methods on the viewlet class.
- We specify the viewlet manager using grok.viewletmanager().
- As with a view, the context is available as self.context and the request as self.request. In addition, there is self.view, the current view, and self.viewletmanager, the viewlet manager.
To render the viewlet, we could either override the render() method and return a string, or use a page template. A page template will be automatically associated using the rules that apply views. Thus, if the viewlet was defined in browser.py, the template would be found in browser_templates/messageviewlet.pt. In the template, the variable view refers to the current view, and the variable viewlet refers to an instance of the viewlet class. For example:
<div class="messageViewlet"> <span>The message subject for this document would be </span> <span tal:content="viewlet/message/subject" /> </div>
Notes:
- Viewlet templates tend to be short, and never include the full <html /> wrapper.
- For the page template to be valid, there must be exactly one root node, a <div /> in this case.
- It is a good idea to apply a CSS class to the outer element of the viewlet, so that it can be styled easily.
- The viewlet variable refers to an instance of the viewlet class. There is also view, the current view; context, the context content object; and request, the current request.
Viewlet ordering
By default, the order of viewlets in a viewlet manager is arbitrary. Plone's viewlet managers, however, add ordering support, as well as the ability to temporarily hide particular viewlets. You can control the order through-the-web using the @@manage-viewlets view described above.
A more robust and repeatable option, however, is to configure ordering at product installation time using Generic Setup, by adding a viewlets.xml to your profiles/default directory.
For example, to ensure that our new viewlet appeared first in the plone.abovecontent manager, we could use a viewlets.xml file like this:
<?xml version="1.0"?>
<object>
<order manager="plone.abovecontent" skinname="*">
<viewlet name="example.messaging.MessageViewlet" insert-before="*"/>
</order>
</object>
See this tutorial for more detail about the syntax of this file.
Overriding an existing viewlet
Just like a view, a viewlet with a particular name can be overridden based on the type of context, using the grok.context() directive, or a browser layer, using the grok.layer() directive.
Here is an example using a more-specific context override:
from five import grok
from plone.app.layout.interfaces import IAboveContent
...
class SilentMessageViewlet(grok.Viewlet):
"""Don't get in the way of important documents
"""
grok.name('example.messaging.MessageViewlet")
grok.context(IImportantDocument)
grok.require('zope2.View')
grok.viewletmanager(IAboveContent)
def update(self):
self.message = IMessage(self.context)
def render(self):
return ''
Notes:
- The viewlet name and manager are the same as those used in the original registration, allowing this viewlet to act as an override for the one defined previously.
- Here, the viewlet is registered for a more-specific context, using grok.context().
- In this case, there is no page template. Instead, we return an empty string from render(). This has the effect of hiding the viewlet for documents providing IImpotantDocument (from the examples earlier in the manual, this is a marker interface that can be applied to IDocument instances). We could of course have used a template as well, as shown above.
Restricting a viewlet to the canonical view
A viewlet may be registered to appear only when a particular type of view is being rendered, using the grok.view() directive. You can pass either the view class itself, or an interface it implements, to this directive. One common example of this is the IViewView marker interface, which Plone applies to the canonical view (i.e. the one you get when clicking the View tab) of a content object.
Here is a refined version of our original viewlet, applied to the canonical view only (template not shown again):
from five import grok
from plone.app.layout.interfaces import IAboveContent
from plone.app.layout.globals.interfaces import IViewView
...
class MessageViewlet(grok.Viewlet):
"""Display the message subject
"""
grok.name('example.messaging.MessageViewlet")
grok.context(IDocument)
grok.require('zope2.View')
grok.viewletmanager(IAboveContent)
gokr.view(IViewView)
def update(self):
self.message = IMessage(self.context)
Notes:
- We restrict the viewlet to views providing the IViewView marker interface, using the grok.view() directive.
- The grok.view() directive can also be used to override an existing viewlet for a more-specific view, much like we showed with a more-specific context above.
Creating and using a new viewlet manager
Plone comes with viewlet managers to cover most of the areas of the page where you may want to inject things, including in the HTML <head /> for things like JavaScript or CSS resources. If you are writing a generic view or customising Plone's main_template, however, you may also want to create your own viewlet manager.
Note that if you simply want to split a complex view up into multiple page templates, ZPT macros are probably a better choice, as viewlets by design involve another layer of indirection.
A viewlet manager can be created by deriving from grok.ViewletManager:
from five import grok
from plone.app.viewletmanager.manager import OrderedViewletManager
class MessageAreaViewletManager(OrderedViewletManager, grok.ViewletManager):
grok.name('example.messaging.MessageArea')
Notes:
- We mix in Plone's viewlet manager base class first, to get Plone ordering support.
- By default, the viewlet manager renders each of its viewlets in order. It is possible to define a custom template instead, by setting the template class variable to a ViewPageTemplate instance, or overriding the render() method.
- If the grok.name() directive were omitted, the name would be taken from the class name, in all lowercase.
- We could limit the viewlet manager to being available only on a particular type of context, using grok.context().
- We could limit the viewlet manager ot being available only on a particular type of view, using grok.view().
- We could also limit the viewlet manager to a particular browser layer using grok.layer(), although it is uncommon to do so.
To use this in a page template, we would do:
<div tal:replace="structure provider:example.messaging.MessageArea" />
Note that this will cause an error if the viewlet manager is not available for the current context and view.
We need to register some viewlets before this would actually display anything. Previously, we used an interface provided by the viewlet manager to register a viewlet for that manager. We could define such an interface and use grok.implements() on the viewlet manager class to associate it with the manager class. However, we can also use the viewlet manager class directly:
class DummyViewlet(grok.Viewlet):
grok.name('example.messaging.DummyViewlet')
grok.require('zope2.View')
grok.viewletmanager(MessageAreaViewletManager)
def render(self):
return "<p>Dummy</p>"
It would of course be better to use a page template, but this would be enough for a quick test.
3.3. Resource directories
Exposing static resources such as CSS, JavaScript and image files.
So far, we have seen how to create views and viewlets. Using views with a custom render() method that sets the Content-Type header, we were able to create files on the fly. We could even use this for binary data.
In may cases, however, we simply want to expose some static files, such as CSS and JavaScript files, or images and use them in our dynamic views. Luckily, five.grok makes this easy.
When a package is grokked, a grokker will look for a directory inside the package with the name static. This is then available under the prefix ++resource++<packagename>, where <packagename> is the dotted name to the package in which the static directory is located.
For example, let's say we had a package called example.messaging. The static directory would then be found in example/messaging/static, alongside the Python modules and sub-packages in this package. If this directory in turn contained a file called messaging.css, it would be accessible on a URL like http://example.org/site/++resource++example.messaging/messaging.css.
If you need to register additional directories, you can do so using the <browser:resourceDirectory /> ZCML directive in configure.zcml. This requires two attributes: name is the name that appears after the ++resource++ namespace; directory is a relative path to the directory containing resources.
Importing CSS and JavaScript files in templates
One common use of static resources is to add a static CSS or JavaScript file to a specific template. We can do this by filling the style_slot or javascript_slot in Plone's main_template in our own view template and using an appropriate resource link.
For example, we could add the following in a view using main_template. Note that this would go outside the block filling the master macro.
<html>
...
<head>
<metal:block fill-slot="style_slot">
<link rel="stylesheet" type="text/css"
tal:define="navroot context/@@plone_portal_state/navigation_root_url"
tal:attributes="href string:${navroot}/++resource++example.messaging/messaging.css"
/>
</metal:block>
</head>
...
</html>
Always create the resource URL relative to the navigation root as shown here, so that the URL is the same for all content objects using this view. This allows for efficient resource caching.
Of course, we could use the same technique anywhere else in any other page template, but the head slots are a good place for CSS and JavaScript resources.
Registering resources with Plone's resource registries
Sometimes it is more appropriate to register a stylesheet with Plone's portal_css registry (or a JavaScript file with portal_javascripts), rather than add the registration on a per-template basis. This ensures that the resource is available site-wide.
It may seem wasteful to include a resource that is not be used on all pages in the global registry. Remember, however, that portal_css and portal_javascripts will merge and compress resources, and set caching headers such that browsers and caching proxies can cache resources well. It is often more effective to have one slightly larger file that caches well, than to have a variable number of files that may need to be loaded at different times.
To add a static resource file, you can use the GenericSetup cssregistry.xml or jsregistry.xml import steps in the profiles/default directory. For example, an import step to add the conference.css file site-wide may involve a cssregistry.xml file that looks like this:
<?xml version="1.0"?>
<object name="portal_css">
<stylesheet id="++resource++example.conference/conference.css"
title="" cacheable="True" compression="safe" cookable="True"
enabled="1" expression="" media="screen" rel="stylesheet" rendering="import"
/>
</object>
Similarly, a JavaScript resource could be imported with a jsregistry.xml like:
<?xml version="1.0"?>
<object name="portal_javascripts">
<javascript cacheable="True" compression="none" cookable="True"
enabled="False" expression=""
id="++resource++example.conference/conference.js" inline="False"/>
</object>
Image resources
Images can be added to resource directories just like any other type of resource. To use the image in a view, you can construct an <img /> tag like this:
<img style="float: left; margin-right: 2px; margin-top: 2px"
tal:define="navroot context/@@plone_portal_state/navigation_root_url"
tal:attributes="src string:${navroot}/++resource++example.conference/program.gif"
/>4. Other five.grok functionality
What we haven't covered
Grok and five.grok provide some functionality we deliberately haven't discussed in this manual. This includes:
- Annotation factories. Annotations are very useful, but the pattern of using a persistent object as the adapter implementation instead of just using the IAnnotations from zope.annotation and its dict-like API to store primitives can lead to problems when code is moved or uninstalled. See grokcore.annotation for an example of this functionality.
- Defining permissions with grok.Permission. We prefer to define permissions in configuration files, rather than code. See grokcore.security if you hate XML so much that you don't mind using Python as a configuration language. See the Dexterity developer manual for more details on creating custom permissions.
- Defining resource directories (other than the implicit static directory) using grok.DirectoryResource instead of the <browser:resourceDirectory /> directive, for the same reasons. See grokcore.view.
- Defining local component sites and local utilities using grokcore.site. In Plone, we use the componentregistry.xml GenericSetup import step for this purpose.
- Creating browser layers with the grok.skin() directive. In Plone, we use the browserlayer.xml GenericSetup import step and/or the plone.theme package for this purpose.
- Forms using grokcore.formlib. For Dexterity development, we use z3c.form instead.
- Model objects using grok.Model. We use Dexterity content objects instead.
- The grok.order() directive, used to order viewlets based on an integer weighting. We use the base class for plone.app.viewletmanager instead, which supports explicit ordering as part of a theme. See grokcore.viewlet for details on how grok.order() works.
- The view/static variable. This allows access to static resources in the static directory using TAL expressions like tal:attributes="href view/static/stylesheet.css". Unfortunately, the link this results in will always be relative to the context, rather than relative to the site navigation root, which means that it will not cache well. Therefore, we construct the URL manually instead. See grokcore.view for more details.
Some of this reflects the Dexterity developers' preferences and views. You are allowed to disagree.
