Developer Manual
Plone Developer Manual is a comprehensive guide to Plone programming.
1. Managing projects with Buildout
Learn about eggs, setuptools and dependency management, and how to use Buildout to set up a development environment.
1.1. Introduction
Or: "What's wrong with a plain old Zope instance"?
This tutorial shows how to install Plone 3 into a buildout, and how to use that buildout when working on a software project that extends Plone. A buildout is a self-contained environment where you can manage the dependencies (including Zope and Plone and any third-party products or libraries you need) and custom code for your project. Even if you are not planning on writing any custom code, the buildout approach is an easy way to install Plone in a robust, well-tested manner. As of Plone 3.2, all of the installers are now buildout based.
Prior to Plone 3.0, most developers and users who did not use a GUI installer, would set up a Zope instance, drop in a few products into the Products folder, and be done with it. Unfortunately, this approach has a few problems:
- Plain old Zope instances are not very well equipped to deal with packages distributed as python eggs or using setuptools namespace packages. Many new packages in Plone 3 are made in this way, and more and more third party modules will be as well.
- Without access to the metadata that is held in eggs, developers may find it too time-consuming or confusing to factor their work into multiple packages that are more re-usable, preferring monolithic products that are impossible to re-use outside Zope.
- Without any further tools, it is cumbersome to repeat a setup across different environments.
As eggs become more important, developers should look to employ more appropriate tools for managing their code. zc.buildout, hereafter referred to only as "buildout" is one such tool. This tutorial shows how to use buildout for day-to-day development as well as deployment.
More buildout documentation and background
Buildout was created by Jim Fulton of Zope Corporation, and is documented in depth at: http://buildout.org/
1.2. Packages, products and eggs
Looking at the core concepts in more detail
This page has been removed due to its old, inaccurate, and misleading information.
For usage of paster templates please refer to this document.
-Mikko
1.3. Prerequisites
A few things you need before we can get started
This page has been removed as it contained old, inaccurate and misleading information.
For usage of paster templates to create Plone add-on products please refer to this document.
-Mikko
1.4. Creating a buildout for your project
How to create a new buildout for a project, adding Plone and other third party products as dependencies
Now, enter the newly created myproject directory, and run the buildout bootstrap script. NOTE: Python 2.4 is currently required to Plone 4.x:
$ cd myproject $ python2.6 bootstrap.py
This will create a number of directories and scripts and dowload the latest version of the zc.buildout egg. This step should be needed only once.
To get started straight away, run:
$ ./bin/buildout
This reads the generated buildout.cfg file and executes its various "parts", setting up Zope, creating a Zope instance, downloading and installing Plone. We will explain this file in more detail shortly.
You will need to run ./bin/buildout again each time you change buildout.cfg. If you do not want buildout to go online and look for updated versions of eggs or download other archives, you can run it in non-updating, offline mode, with;
$ ./bin/buildout -No
To start Zope in foreground and debug mode, run:
$ ./bin/instance fg
The instance script is analogous to zopectl as found in a standard Zope instance. You can use ./bin/instance start to run Zope in daemon mode. It can also be used to run tests:
$ ./bin/instance test -s plone.portlets
Running:
bin/instance console
is equivalent to bin/instance fg, but does not implicitly turn on debug mode but respects the debug-mode setting in buildout.cfg. This can be useful to run Zope in non-development mode with daemon-control programs like supervisord.
Once your buildout installation is up and running, you will still need to install a Plone site. Log in to the Zope Management Interface (ZMI) and from "select type to add..." choose Plone Site. Fill in the required details and submit. Now you have a Plone site at the ID that you specified.
Directories in the buildout
Before we dive into buildout.cfg, let us take a quick look at the directories that buildout has created for us:
- bin/
- Contains various executables, including the buildout command, and the instance Zope control script.
- eggs/
- Contains eggs that buildout has downloaded. These will be explicitly activated by the control scripts in the bin/ directory.
- downloads/
- Contains non-egg downloads, such as the Zope source code archive.
- var/
- Contains the log files (in var/log/) and the file storage ZODB data (in var/filestorage/Data.fs). Buildout will never overwrite these.
If you want to import a .zexp file, place it in the var/instance/imports folder.Previously one had to put that file into parts/instance/import, but this folder gets wiped and regenerated when running bin/buildout, so the import location was changed.
- src/
- Initially empty. You can place your own development eggs here and reference them in buildout.cfg. More on that later.
- products/
- This is analogous to a Zope instance's Products/ directory (note the difference in capitalisation). If you are developing any old-style Zope 2 products, place them here. We will see how buildout can automatically download and manage archives of products, but if you want to extract a product dependency manually, or check one out from Subversion, this is the place to do so.
- parts/
- Contains code and data managed by buildout. In our case, it will include the local Zope installation, a buildout-managed Zope instance, and Plone's source code. In general, you should not modify anything in this directory, as buildout may overwrite your changes.
You can check in a buildout directory to a source code repository to share it among developers. In this case, you should ignore the directories bin/, eggs/, downloads/, var/, and parts/. Each developer can run bootstrap.py to get these back, and will normally need local copies anyway. All your configuration should be in the buildout.cfg file, and all custom code in src/ or products/.
1.5. Understanding buildout.cfg
How to manage the main buildout configuration file
Important note: This document applies to Plone 3.2 onwards. In Plone versions prior to 3.2 the vanilla buildout.cfg file was significatively different because Plone wasn't fully eggified.
buildout.cfg is the most important file in your new buildout environment. Here is how it looks:
[buildout]
parts =
zope2
productdistros
instance
zopepy
# Change the number here, and in find-links below, to change the version of
# Plone being used
extends = http://dist.plone.org/release/3.3.5/versions.cfg
versions = versions
# Add additional egg download sources here. dist.plone.org contains archives
# of Plone packages.
find-links =
http://dist.plone.org/release/3.3.5
http://dist.plone.org/thirdparty
# Add additional eggs here
eggs =
# Reference any eggs you are developing here, one per line
# e.g.: develop = src/my.package
develop =
[zope2]
recipe = plone.recipe.zope2install
url = ${versions:zope2-url}
# Use this section to download additional old-style products.
# List any number of URLs for product tarballs under URLs (separate
# with whitespace, or break over several lines, with subsequent lines
# indented). If any archives contain several products inside a top-level
# directory, list the archive file name (i.e. the last part of the URL,
# normally with a .tar.gz suffix or similar) under 'nested-packages'.
# If any archives extract to a product directory with a version suffix, list
# the archive name under 'version-suffix-packages'.
[productdistros]
recipe = plone.recipe.distros
urls =
nested-packages =
version-suffix-packages =
[instance]
recipe = plone.recipe.zope2instance
zope2-location = ${zope2:location}
user = admin:admin
http-address = 8080
# comment the following two options in production sites
debug-mode = on
verbose-security = on
# If you want Zope to know about any additional eggs, list them here.
# This should include any development eggs you listed in develop-eggs above,
# e.g. eggs = Plone my.package
eggs =
Plone
${buildout:eggs}
# If you want to register ZCML slugs for any packages, list them here.
# e.g. zcml = my.package my.other.package
zcml =
products =
${buildout:directory}/products
${productdistros:location}
[zopepy]
recipe = zc.recipe.egg
eggs = ${instance:eggs}
interpreter = zopepy
extra-paths = ${zope2:location}/lib/python
scripts = zopepy
Let us walk through this file step-by-step:
The main [buildout] section
The [buildout] section is the starting point for the file. It lists a number of "parts", which are configured in separate sections later in the file. Each part has an associated recipe, which is the name of an egg that knows how to perform a particular task, e.g. build Zope or create a Zope instance. A recipe typically takes a few configuration options.
Our global settings are as follows:
[buildout]
parts =
zope2
productdistros
instance
zopepy
find-links =
http://dist.plone.org/release/3.3.5
http://dist.plone.org/thirdparty
eggs =
develop =
This specifies that the parts zope2, productdistros, instance and zopepy will be run, in that order. Then, we tell buildout that it can search one of a number of URLs when it is looking for eggs to download. In addition, it will always search the Cheese Shop.
Note that configuration entries are commonly split into multiple lines. For this to work, all lines after the first must begin with at least 4 spaces.
Next, we can list any eggs that buildout should download and install for us. This may include version specifications. For example, if you want sqlalchemy 0.3, but not 0.4, you could list;
eggs =
sqlalchemy>=0.3,<0.4dev
Please note that you will need the Python Imaging Library (PIL) for Plone to work. This example assumes that you have this library already installed and available from your Python interpreter, but otherwise you can install a slightly modified (to workaround some common problems) version from the "thirdparty" Plone repository in your buildout adding its name to the eggs list:
eggs =
PILwoTk
And the full path to the package in the find-links, e.g.:
find-links = http://dist.plone.org/thirdparty/PILwoTk-1.1.6.4.tar.gz
Finally, we can list development eggs, by specifying a directory where the egg is extracted in source format. For example:
eggs =
my.package
develop =
src/my.package
This presumes that there is an egg called my.package in the src/ directory. We will learn how to create such eggs a little later in this tutorial. Notice how we must also list my.package as an actual egg dependency: development eggs are not automatically added to the "working set" of eggs that are installed for Zope.
The extends and versions lines
This part was introduced with Plone 3.2. It references a remote file where the version of each needed package is specified. Check that remote file to see yourself how these dependencies are specified.
# Change the number here, and in find-links below, to change the version of # Plone being used extends = http://dist.plone.org/release/3.3.5/versions.cfg versions = versions
If you want to use a local file instead of a remote one to be able to work offline, download it to your buildout directory and reference it like this:
extends = versions.cfg
The [zope2] section
This part builds Zope 2, using plone.recipe.zope2install. If you specified an existing Zope installation, you will not have this part. Otherwise, it looks like this:
[zope2]
recipe = plone.recipe.zope2install
url = ${versions:zope2-url}
Here, we reference the download location for Zope as present in the versions file. This ensures that we always get the recommended version of Zope. You could specify a download URL manually instead, if you wanted to use a different version of Zope.
When the recipe is run, Zope 2 is installed in parts/zope2. The Zope software home becomes parts/zope2/lib/python.
The [productdistros] section
This uses the plone.recipe.distros recipe, which is able to download distributions (archives) of Zope 2 style products and make them available to Zope. It is empty to begin with:
[productdistros] recipe = plone.recipe.distros urls = nested-packages = version-suffix-packages =
However, you can list any number of downloads. The recipe is also able to deal with archives that contain a single top-level directory that contains a bundle of actual product directories (nested-packages), or packages that have a version number in the directory name and thus need to be renamed to get the actual product directory (version-suffix-packages).
Consider the following distributions:
# A typical distribution
ExampleProduct-1.0.tgz
|
|- ExampleProduct
|
|- __init__.py
|- (product code)
# A version suffix distribution
AnotherExampleProduct-2.0.tgz
|
|- AnotherExampleProduct-2.0
|
|- __init__.py
|- (product code)
# A nested package distribution
ExampleProductBundle-1.0.tgz
|
|- ExampleProductBundle
|
|- ProductOne
| |- __init__.py
| |- (product code)
|
|- ProductTwo
|- __init__.py
|- (product code)
Here is what the part would look like if we try to install the three distributions above:
[productdistros]
recipe = plone.recipe.distros
urls =
http://example.com/dist/ExampleProduct-1.0.tgz
http://example.com/dist/AnotherExampleProduct-2.0.tgz
http://example.com/dist/ExampleProductBundle-1.0.tgz
nested-packages = ExampleProductBundle-1.0.tgz
version-suffix-packages = AnotherExampleProduct-2.0.tgz
You can specify multiple downloads on separate lines. When the recipe is run, the product directories for downloaded products are found in parts/productdistros.
The [instance] section
The instance section pulls it all together: It configures a Zope instance using the plone.recipe.zope2instance script. Here is how it looks:
[instance]
recipe = plone.recipe.zope2instance
zope2-location = ${zope2:location}
user = admin:admin
http-address = 8080
# comment the following two options in production sites
debug-mode = on
verbose-security = on
eggs =
Plone
${buildout:eggs}
zcml =
products =
${buildout:directory}/products
${productdistros:location}
Here, we reference the Zope 2 installation from the [zope2] part - if you specified a location yourself when creating the buildout, you would see that one here. Then, we specify the initial admin user and password used only when creating the initial database, and the port that Zope will be bound to. We also turn on debug mode and verbose security. They are useful for development, but remember to turn them off in production sites since they can compromise the security of your site. These options are used to generate an appropriate zope.conf file for this instance. See the recipe page in the Cheese Shop for more details on the options available.
Next, we specify which eggs that will be made available to Zope. This references the "global" eggs from the [buildout] section, as well as Plone itself. You could add additional eggs here, though it is generally easier to specify these at the top of the file, so that they get included in the ${buildout:eggs} working set.
Zope 3 configure.zcml files are not automatically loaded for eggs or packages that lack z3c.autoinclude support and are not in the Products namespace. To load ZCML files for a regular package, we can make buildout create a ZCML slug by listing the package under the zcml option:
zcml =
my.package
my.package-overrides
This assumes that my.package was previously referenced in the buildout. This would load both the main configure.zcml and the overrides.zcml file from this package. Over time, the need for these entries should diminish, as z3c.autoinclude support becomes widespread.
Finally, we list the various directories that contain Zope 2 style products - akin to the Products/ directory in a traditional instance. Notice how the products/ directory in the main buildout directory comes first, followed by the products downloaded with the [productdistros] part.
When the recipe is run, the Zope instance home will be parts/instance, and a control script is created in ./bin/instance.
The [zopepy] section
This final section creates a Python interpreter that has all the eggs and packages (but not Zope 2 style products) that Zope would have during startup. This can be useful for testing purposes.
[zopepy]
recipe = zc.recipe.egg
eggs = ${instance:eggs}
interpreter = zopepy
extra-paths = ${zope2:location}/lib/python
scripts = zopepy
Here, we copy the eggs from the [instance] section, and include in the pythonpath the Zope instance home.
When the recipe is run, the script will be created in ./bin/zopepy.
1.6. Creating a buildout defaults file
This makes it possible to share configuration across multiple buildouts, and save some time and disk space.
To set "global" options affecting all buildouts, create a directory .buildout (note leading dot) in your home directory, and add a file there called default.cfg. Any option set here will be applied to the corresponding section in any buildout.cfg that you run, unless it is overridden by a more specific option in the buildout.cfg file itself.
The most common options are:
- executable
- Specify a python interpreter other than the system default. This is useful if you have Python 2.5 installed, say, but you want your buildouts to use another installation of Python 2.4.
- eggs-directory
- Specify a directory where eggs will be downloaded. This allows multiple buildouts to share the same eggs, saving disk space and download time. Note that only those eggs explicitly required by a particular buildout will be activated. The eggs directory may contain many more eggs (or many different versions of the same package) than what is used at any one time.
- download-cache
- Specify a shared directory for downloaded archives. Again, this can save disk space and download time. NOTE: before zc.buildout 1.0, this was called download-directory
- extends-cache
- Specify a shared directory for extended buildout configurations that are downloaded from a URL. As of Plone 3.2 this is how Plone pins the versions of its eggs. This option was added in zc.buildout 1.4.1, prior to that the offline mode in combination with a extends URL would not work.
Here is an example ~/.buildout/default.cfg setting all three:
[buildout] executable = /opt/python24/bin/python eggs-directory = /home/username/.buildout/eggs download-cache = /home/username/.buildout/downloads extends-cache = /home/username/.buildout/extends
This assumes Python 2.4 is installed in /opt/python2.4. For the last two options to work, you would need to create the directories eggs and downloads inside the ~/.buildout directory.
1.7. Installing a third party product
How to install a new package using these tools
How to install a new third-party products will depend on whether it is packaged as an egg, or a traditional Zope 2 product.
Installing eggs
So long as an egg has a release in the PyPi or elsewhere, buildout can download and install it, including any explicitly specified dependencies. Simply list the egg, and optionally a version (otherwise, you get the latest available), in the eggs option.
[buildout]
...
eggs =
elementtree
borg.project>=1.0b1,<2.0dev
If you want buildout to search an index other than PyPi's, you can add a URL to find-links that contains download links for the eggs. In fact, we have already seen an example of this: elementtree is found at http://effbot.org/downloads, not in PyPi directly. Thus, we have:
[buildout]
...
find-links =
http://dist.plone.org
http://download.zope.org/ppix/
http://download.zope.org/distribution/
http://effbot.org/downloads
eggs =
elementtree
We have also listed some of the download locations for Zope and Plone eggs.
Again - re-run buildout for the changes to take effect:
$ ./bin/buildout
Development eggs
If there is not a release for your egg, or you want to track an egg in Subversion, check it out to the src/ directory. Make sure you get the full egg, including the top-level setup.py file. For example, to get the plone.portlets trunk development, egg do:
$ cd src $ svn co https://svn.plone.org/svn/plone/plone.portlets/trunk plone.portlets
Then, add the following to buildout.cfg:
[buildout]
...
eggs =
...
plone.portlets
develop =
src/plone.portlets
Note that:
- The develop option contains a relative path to where the source egg is installed. Buildout will expect to find a suitable setup.py in this directory.
- Development eggs always take precedence over regular eggs.
- You still need to list the egg name in the eggs option for it to be installed.
- If you are overriding an egg that ships with Plone, you may need to list it in the eggs section of the [plone] part instead:
[buildout]
...
develop =
src/plone.portlets
...
[plone]
recipe = plone.recipe.plone
eggs =
plone.portlets
This is because plone.recipe.plone is very expilcit about which versions of its various eggs to use, to ensure Plone keeps running as it was released.
Buildout recipes (such as plone.recipe.plone) are distributed as eggs. You can use a development egg of a recipe by listing it under the develop option. There is no need to explicitly list it under the eggs option, since it is referenced by the recipe option of the relevant part.
Installing a traditional Zope 2 product
The easiest way to try out a traditional Zope 2 product is to extract it into the products/ folder inside the buildout. If you see documentation referring to the Products/ folder in a Zope instance, this is the same thing.
However, this approach makes it harder to redistribute your project and share it with other developers. It is often more predictable to let buildout download and install the package for you. You can do this with the [productdistros] section of buildout.cfg. For example, here is how you might install a product named ExampleProduct and a set of products named ExampleProductBundle:
[productdistros]
recipe = plone.recipe.distros
urls =
http://example.com/dist/ExampleProduct-1.0.tgz
http://example.com/dist/ExampleProductBundle-1.0.tgz
nested-packages =
ExampleProductBundle-1.0.tgz
version-suffix-packages =
Note that our fictional ExampleProductBundle is distributed as a single directory containing a number of products in sub-directories, so we list it under nested-packages.
As always, if you change buildout.cfg, you must re-run buildout:
$ ./bin/buildout
Managing ZCML files
It is important to realize that Zope will not load configure.zcml files automatically for packages that are not in the Products.* namespace and lack support for z3c.autoinclude (see next page for more on using z3c.autoinclude). Instead, you must explicitly reference the package. Buildout can create such a reference (known as a ZCML slug) with the zcml option under the [instance] part. Here is how to ensure that borg.project is available to Zope:
[buildout]
...
eggs =
elementtree
borg.project
...
[instance]
...
zcml =
borg.project
Should you need to load an overrides.zcml or a meta.zcml, you can use a syntax like:
zcml =
some.package
some.package-overrides
some.package-meta
Policy products
Many developers prefer to create a single "policy product" (also known as a "deployment product") that orchestrates various dependencies. If you have such a product, you may want to include various dependencies directly from the policy product's configure.zcml file, with lines such as:
<configure xmlns="http://namespace.zope.org/zope">
<include package="borg.project" />
</configure>
In this case, you may still need one slug (using the zcml option as above) for the policy product.
1.8. Creating a new package
Adding a new custom package is not much different from installing a third-party one.
Creating a traditional Zope 2 product
To create a traditional Zope 2 product, put it in the top-level products/ directory and re-start Zope. Nothing more should be required. As explained previously, products placed here will be found automatically at start-up, and their configure.zcml files will be executed automatically.
Creating an egg
Of course, if you are using products, you cannot benefit from the additional features of eggs, including automatic dependency
management, distribution via the Cheese Shop and nested namespaces.
A part of this page has been removed as it contained old, inaccurate and misleading information.
For usage of paster templates to create Plone add-on products please refer to this document.
-Mikko
Â
You will now have:
- A setup.py which contains the metadata you entered
- A package in myorg.mypackage/myorg/mypackage. Your source code goes here.
- A skeleton configure.zcml, tests.py and a few other useful starting points.
- Some generic documentation in myorg.mypackage/docs.
Of course, you must also add this package to the buildout. In buildout.cfg, you might have:
[buildout]
...
eggs =
...
myorg.mypackage
develop =
src/myorg.mypackage
Unless you plan to include this package from another one (or use automatic ZCML loading, explained below), you probably also need a ZCML slug:
[instance]
...
zcml =
myorg.mypackage
Do not forget to re-run buildout after making the change:
$ ./bin/buildout
Automate ZCML loading for your package
If you're not including your package from another one, you can still avoid having to include a ZCML slug in buildout.cfg for it. This is particulary useful to avoid unneccessary repetition of package names in buildout.cfg, which begginer integrators might easily overlook. From Plone 3.3 on, you can make your packages signal that their ZCML should be included by adding:
setup(...
entry_points="""
...
[z3c.autoinclude.plugin]
target = plone
...)
"""
to their setup.py file. For further information, see the setuptools documentation about dynamic discovery of services and plugins.
Specifying dependencies
If your new package has explicit dependencies, you can list them in setup.py. That way, buildout will be able to download and install these as well. Dependencies are listed in the install_requires argument to the setup() method, By default, setuptools is listed here, since we need this to support namespace packages. To add sqlalchemy 0.3 (but not 0.4), and the MySQL-Python driver, you could amend this to read:
install_requires=[
'setuptools',
'sqlalchemy>=0.3,<0.4dev',
'MySQL-Python',
],
Uploading your egg to the Cheese Shop
If you want to share your packge with the rest of the Python community and make it easy to install using tools like buildout and easy_install, you can upload the package to the Cheese Shop.
Before doing so, you should:
- Commit your latest changes and tag the release in Subversion, if applicable.
- Remove (temporarily) the setup.cfg file: this makes your package a development release.
- Make sure the version number in setup.py is correct. This should use common conventions such as "1.0b2" for the second beta of version 1.0, or "2.1.3rc1" for the first release candidate of version 2.1.3.
- If you are using Mac OS X, run export COPY_EXTENDED_ATTRIBUTES_DISABLE=true on the shell first - otherwise, the egg will contain Mac OS X resource forks which cause problems if your egg is used on Windows.
When you are ready, run the following command from your package's directory (e.g. src/myorg.mypackage):
$ python setup.py egg_info -RDb "" sdist register upload
This will ask you to create a Cheese Shop account if you do not have one already. You can run this command as often as you'd like to release a new version (probably with a new version number).
1.9. A deployment configuration
How to use buildout for deployment configuration
Finally, let's take a look at a more advanced configuration, better suited for deployment. Save this file as deployment.cfg, at the root of the buildout next to the main buildout.cfg file:
[buildout]
extends =
buildout.cfg
parts +=
debug-instance
zeoserver
varnish-build
varnish-instance
[zeoserver]
recipe = plone.recipe.zope2zeoserver
zope2-location = ${instance:zope2-location}
zeo-address = ${instance:zeo-address}
[instance]
recipe = plone.recipe.zope2instance
zope2-location = ${zope2:location}
zeo-client = true
zeo-address = 8100
zodb-cache-size = 5000
zeo-client-cache-size = 300MB
debug-mode = off
verbose-security = off
eggs += Products.CacheSetup
[debug-instance]
recipe = collective.recipe.zope2cluster
instance-clone = instance
http-address = 8081
debug-mode = on
verbose-security = on
[varnish-build]
recipe = zc.recipe.cmmi
url = http://downloads.sourceforge.net/varnish/varnish-2.0.2.tar.gz
[varnish-instance]
recipe = plone.recipe.varnish
daemon = ${buildout:parts-directory}/varnish-build/sbin/varnishd
bind = 127.0.0.1:8082
backends = 127.0.0.1:8080
cache-size = 1G
Here, we are:
- Referencing the main buildout.cfg file, extending and overriding it with configuration more appropriate for deployment.
- Setting up a ZEO server with two client instances, instance and debug-instance (see plone.recipe.zope2zeoserver and plone.recipe.zope2instance for more details)
- Compiling the Varnish cache server (see plone.recipe.varnish for more details).
By combining buildout configuration files like this, you can create tailor-made configurations for different deployment scenarios. To learn more about the advanced features of buildout, see its documentation.
To build this environment, you must explicitly specify a configuration file:
$ ./bin/buildout -c deployment.cfg
To start Zope and Plone, you will need to start the ZEO server, the instance and the Varnish server:
$ ./bin/zeoserver start $ ./bin/instance start $ ./bin/varnish-instance
If you need to bring up an instance for debugging then you can start up the debug-instance in foreground mode.
$ ./bin/debug-instance fg
The recipes will also create scripts to back up the ZODB filestorage (in ./bin/repozo) and to pack the database (in ./bin/zeopack).
Further options
zc.buildout is a very flexible system. It is relatively easy to create new recipes, and you can combine existing recipes in powerful ways. Search the Cheese Shop for "buildout" to find more recipes, or take a look at the source code for some of Plone's own recipes to understand how recipes are created.
1.10. Useful buildout recipes
A list of the most common and useful buildout recipes used when working with Plone.
The list is more or less sorted by topic. Check all available recipes at PyPI.
- zc.recipe.egg - Installs eggs into a buildout eggs directory. It also generates scripts in a buildout bin directory with egg paths baked into them.
- infrae.subversion - This zc.buildout recipe will check out a number of URLs into its parts directory. It won't remove its parts directory if there are any changes in the checkout, so it's safe to work with that checkout for development.
- plone.recipe.zope2install - Installs Zope 2, i.e. its Python libraries and scripts, but doesn't create any instance.
- plone.recipe.zope2instance - Creates and configures a Zope 2 instance in parts. It also installs a control script, which is like zopectl, in the bin/ directory.
- plone.recipe.zope2zeoserver - This recipe creates and configures a Zope 2 ZEO server in parts. It also installs a control script, which is like zeoctl, in the bin/ directory.
- plone.recipe.distros - Installs distributions, i.e. Zope products not packaged as eggs.
- plone.recipe.apache - Builds and configures the Apache web server.
- gocept.nginx - zc.buildout recipe for configuring an nginx server
- plone.recipe.varnish - Installs the Varnish reverse-cache proxy. It works for non-Zope sites as well.
- plone.recipe.squid - Installs the Squid proxy. It works for non-Zope sites as well.
- collective.recipe.omelette - Creates a unified directory structure of all namespace packages, symlinking to the actual contents, in order to ease navigation.
- collective.recipe.i18noverrides - Creates an i18n directory within one or more Zope 2 instances in your buildout. It copies some .po files to those directories. The translations in those .po files will override any other translations.
- zc.recipe.cmmi - The Configure-Make-Make-Install recipe automates installation of configure-based source distribution into buildouts.
- plone.recipe.command - Execute arbitrary commands in buildout through os.system.
1.11. Installing products from Subversion
Sometimes Plone products are not eggified, but available only in Subversion version control repository. This how to tells how such product can be automatically installed in buildout installations.
2. Content-types with Archetypes
How to create new content-types in Plone using the Archetypes framework.
2.1. Introduction
2.1.1. What is Archetypes?
Brief presentation of Archetypes.
Archetypes is a framework for developing new content types for a Plone project. Most content management projects involve introducing new types of content, which in the non-trivial case requires an informed understanding of how Zope and the CMF work. Archetypes provides a simple, extensible framework that can ease both the development and maintenance costs of CMF content types while reducing the learning curve for the simpler cases.
Compared to building content types using the stock CMF (through subclassing), Archetypes gives you the following advantages:
- automatically generates forms and views;
- provides a library of stock field types, form widgets, and field validators;
- allows defining custom fields, widgets, and validators;
- automates transformations of rich content;
- a built-in reference engine that gives the ability to link two objects together with a relation; such a "link" from a given object to another one is a Python object called a reference.
Since Plone 2.1, Archetypes has become the de-facto way of developing new content types, and a majority of third party products that are released these days use Archetypes.
2.1.2. Archetypes schemas
Introducing Archetypes-based schemas and fields.
Archetypes provides a robust framework for storing data attributes on content objects. This framework consist of a number of Fields stored in a container called a Schema. Fields are simply specialized Python classes that allow you to store and retrieve data associated with an Archetypes object.
Fields provide a few functionalities. First, there are specialized field types for strings, lists of strings, integers, floating-point numbers, etc., that allow special handling of fields based on the type of data stored.
Some definitions
Before we go diving in, let's define some often-used terms:
- Field: An Archetypes Field. This refers to an instance of a Field class defined in a Schema.
- Schema: The "container" that Archetypes uses to store fields.
- Schemata: A named grouping of fields. One Schema can have many schematas.
- AT: Abbreviation for Archetypes.
Fields, Classes, and Objects
Archetypes Fields are Python objects contained within the Schema. A Field is defined once for an Archetypes content class. This single Field instance is used for every instance of that class. Therefore, the relationship between Field instances and content classes is described as such: "A field instance belongs to exactly one class." A class, however, can have many different Field instances. Furthermore, every instance of an AT class uses the same set of Fields. AT objects themselves do not contain unique Fields.
When Zope starts up, during product initialization, Archetypes reads the schema of the registered classes and "automagically" generates methods to read (the accessor) and change (the mutator) each of the fields defined.
Stock schemas
Archetypes includes three stock schemas:
- BaseSchema: defines a normal content type,
- BaseFolderSchema: defines a folderish content type (object can contain other objects),
- BaseBTreeFolderSchema: for folders which need to handle hundreds or thousands of objects (even up to millions).
All three include two
fields, id and title, as well as the
standard Dublin Core metadata fields.
Modifying the fields of an existing schema
Modifying an existing schema field is possible using the syntax schema['<field_name>'].attribute = value. For example, to change the label of the description field widget (already available in BaseSchema), you can write (in your defined schema definition that reuses BaseSchema):
schema['description'].widget.label = u'Summary'
The fields in the schema are ordered, and normally first fields come
first in "add" and "edit" forms. To rearrange a field within the schema
use the moveField method:
- Place it before a specific field:
schema.moveField('<field_to_move>', before='<field_to_place_it_before>') - Place it after a specific field:
schema.moveField('<field_to_move>', after='<field_to_place_it_after>') - Place it at the top of the schema:
schema.moveField('<field_to_move>', pos='top') - Place it at the bottom:
schema.moveField('<field_to_move>', pos='bottom') - Place it in a specific position:
schema.moveField('<field_to_move>', pos=0)
2.1.3. What is ATContentTypes?
ATContentTypes is the Plone core product that provides the default content types (since Plone 2.1).
One of the major changes introduced in Plone 2.1 was that the core content types (Page, Image, etc) were changed from being based on stock CMF types, to using Archetypes. The new core types are housed in the ATContentTypes product.
ATContentTypes introduces a number of base classes and tools that provide common "Plone-ish" behaviour. This includes things like support for the "display" menu and the "more..." menu and restrictions for the "add item" menu.
You can use ATContentTypes' base classes and tools in your own products. The RichDocument tutorial covers the core techniques, and is probably a good place to go when you have finished this reference.
2.2. A Simple AT Product
A semi-realistic example Archetypes-based content type product.
2.2.1. Introduction
Introducing a sample AT Product and the contents of the tutorial.
In this part of the manual, we discuss a sample AT Product to explain CMF/Archetypes practices. We will be building a product called example.archetype, which will implement a content type (InstantMessage) that members with specific rights can use to add messages readable by other members. However, as you may have guessed, this is more a learning example than a usable product for a real website application.
What is a Product ? A product - a Zope product to be precise - is a third party add-on that can be integrated to provide additional functionality. It is a code package written using the Python language and conventions.
In order to understand this section you will need to have some prior knowledge of working on the file system and programming protocols common to Python and Zope.
The example.archetype product features the following CMF and Archetypes capabilities:
- basic fields and widgets;
- defining and using a vocabulary for a field with a selection widget;
- defining specific "Add" permissions for the contents.
The code of the product can be downloaded here: http://plone.org/products/example.archetype/
2.2.2. Product package layout
Conventions and techniques for organizing the package for an AT product.
Following Zope, Plone and AT's conventions, the content of our example product pakage will look like this:
- __init__.py
- configure.zcml
- config.py
- interfaces.py
- content
- __init__.py
- message.py
- profiles
- default
- browser
- __init__.py
- configure.zcml
- instantmessage.pt
- tests
- __init__.py
- base.py
- test_setup.py
What is the purpose of these files and directories?
- __init__.py: The usual "Python package" initialization module;
- configure.zcml: Using Zope's new Configuration Markup Language (ZCML), this file configures the services or behaviour the Zope server needs to load at startup;
- config.py: Provides configuration variables for the product;
- interfaces.py: Where you define interfaces describing what the packages' classes will do;
- content: Contains the modules providing the implementation of the content types.
In this case, it contains the message.py file where the
'InstantMessage' class should be defined;
- profiles/default:
Contains a set of XML files that are needed to provide the settings that will be used by Plone's Quick-Installer tool when installing the product within Plone; this is what we call an Extension Profile, an artifact of Zope CMF's GenericSetup technology. Note that this replaces the old way of doing based on the Extensions/Install. More precisely, since Plone 3.0, you do not need that old-style technique;
- browser: The sub-package where the developer can add specific presentation code such as browser views and templates; the contained configure.zcml is used to provide these components registration.
- tests: Contains the unit tests code for the product.
If you have ZopeSkel installed, you can use the following command to create a similar structure:
paster create -t archetype example.archetype
Now we will go through the files one by one and add what we need to produce our application.
2.2.3. The interfaces module
The module where you define interfaces describing what your content class(es) will do.
Why do you need interfaces?
Interfaces are useful to describe what a class will do. They are a kind of contract between a class and the components that class interact with. Starting a content management functionality package with writing interfaces is recommended practice as it helps document your code. In addition to that, Zope Component Architecture (ZCA) allows us to use interfaces as components for adapting a class (which is useful as new user requirements appear) and thus specializing its behaviour.
The interface for the Instant Message class
This is done by convention in the interfaces.py file, that you need to add at the root of the package.
First, we need an import from Zope's zope.interface module, which is included into Zope 2's distribution since version 2.8:
from zope.interface import Interface
Following ZCA naming conventions (interface names start with an I), we define the IInstantMessage interface we need for the InstantMessage class that we will define later:
class IInstantMessage(Interface):
"""
Interface for the InstantMessage class.
"""
That's it!
We could add attribute definitions to it using the zope.interface.Attribute class, but this is not mandatory. When an interface is defined as above, without any function nor attribute, we call it a "marker interface" meaning that it will be used simply to "mark" the instances of the class that implements it.
More information about interfaces in the context of Archetypes can be found in the b-org tutorial - Interfaces section. For a detailed presentation of interfaces and their usage patterns, read the doctests document available from Zope's documentation site.
2.2.4. The configuration module
The configuration details for your content type, in config.py.
First, we have to import a class from Archetypes:
from Products.Archetypes.atapi import DisplayList
Displaylist is a data container we use when displaying
pulldowns/radiobuttons/checkmarks with different choices. Let's say we wanted
priorities on our instant messages, and we wanted those to be High, Normal
and Low. We will specify these later in the file.
The next two lines set the project (Product in Zope) name, and point to the
skin directory. PROJECTNAME should reference the name of the package: example.archetype.
PROJECTNAME = "example.archetype"
Now, we need to specify our 'Priority' pulldown. It should look like this, using the DisplayList utility class that Archetypes has provided for exactly that purpose:
MESSAGE_PRIORITIES = DisplayList((
('high', 'High Priority'),
('normal', 'Normal Priority'),
('low', 'Low Priority'),
))
Python notes:
-
The reason for double parantheses is that DisplayList is a class that you pass a tuple of tuples to.
We also need to define the "Add" permission(s) for the content type(s):
ADD_CONTENT_PERMISSIONS = {
'InstantMessage': 'example.archetype: Add InstantMessage',
}
We recommend using the standard way of naming permissions: '<ProductName>: <Permission>'. This will group the related permissions together within the ZMI (Security tab), and allow the Administrator to recognize which permissions belong to which Product.
Note that, unless you have an advanced case which needs custom security settings, you don't need to define your own permissions for the "edit" and "view" of the content. In this simple case you will just reuse, in the modules where needed, the generic permissions defined in CMFCore.permissions: "View", "Modify portal content"...2.2.5. The startup module
The initialization module (__init__.py) provides the script that is run when Zope is started.
Before starting the usual Zope product initialization code, we need to define a Message Factory for when this product is internationalized.
from zope.i18nmessageid import MessageFactory
exampleMessageFactory = MessageFactory('example.archetype')
The defined MessageFactory object will be imported with the special name "_" in most modules, and strings like _(u"message") will then be extracted by i18n tools for translation.
Now, we import some useful stuff from the Archetypes API:process_types is useful to get the product's content types,
associated constructors, and Factory Type Information (FTI) data structures, while listTypes can be used to list the types available in the product.
We also need to import the utils module from CMFCore to be able to use its ContentInit class later.
from Products.Archetypes.atapi import process_types from Products.Archetypes.atapi import listTypes from Products.CMFCore import utils
Python notes:
-
Factory Type Information (FTI): Part of a CMF portal's configuration, the FTI for a content type is the data structure that holds the information needed to expose a content type within the portal. From the integrator's perspective, the FTI is the object (Factory-based Type Information object) within the portal_types component that tells CMF and Plone how to create a content from the type and how to display it.
-
How exactly does 'listTypes' work: See those registerType() calls in your content type modules? Notice how we also import those modules (but do nothing with the import) in the 'content' package's __init__.py. The registerType() call tells AT about the type so that listTypes() can find it later.
from content import messageNow, we import the configuration module, in order to have access to the variables it contains, such as the "Add" permission setting:
import config
Now for the real action. You define a function that is required by Zope and CMF internals to initialize our content type(s):
def initialize(context):
The first part of the code of this function generates the content types, the constructors and the Factory-based Type Informations (or FTIs) required to make your types work with the CMF:
content_types, constructors, ftis = process_types(
listTypes(config.PROJECTNAME),
config.PROJECTNAME)
The second part instantiates an object of the class ContentInit (from CMFCore), and registers your types in the CMF:
utils.ContentInit(
"%s Content" % config.PROJECTNAME,
content_types = content_types,
permission = config.ADD_CONTENT_PERMISSIONS['InstantMessage'],
extra_constructors = constructors,
fti = ftis,
).initialize(context)
Handling several content types
There is a better way to write the code that initializes the content type class with its "Add" permission and constructor, so that it still works if you define several content types. This is useful if you plan to later augment your product with additional types.
Here is the improved code:
def initialize(context):
content_types, constructors, ftis = process_types(
listTypes(config.PROJECTNAME),
config.PROJECTNAME)
# We want to register each type with its own permission,
# this will afford us greater control during system
# configuration/deployment (credit : Ben Saller)
allTypes = zip(content_types, constructors)
for atype, constructor in allTypes:
kind = "%s: %s" % (config.PROJECTNAME, atype.portal_type)
utils.ContentInit(kind,
content_types = (atype,),
permission = config.ADD_CONTENT_PERMISSIONS[atype.portal_type],
extra_constructors = (constructor,),
fti = ftis,
).initialize(context)
Python notes:
-
We can use the "ADD_CONTENT_PERMISSIONS[atype.portal_type]" construct because ADD_CONTENT_PERMISSIONS references a dictionary in which the keys are the potential content types names.
-
The zip() function is a Python built-in that pairs up elements of two lists. In this case, "allTypes" will be a list of tuples containing a content type from "content_types" and the corresponding constructor from "constructors".
-
If you have several content types, you should not forget to import each content module, as is done for the message example discussed here !
2.2.6. The content package and its modules
Now we are ready for the core of the product, i.e. the content class definition module (content/message.py).
Since it provides a Python (sub)package, the 'content' directory contains 2 modules:
- the usual __init__ module that initializes the package,
- the message module (message.py) where we will define the 'InstantMessage' class.
The message module
First imports we need
We start the message module by adding the general Zope-related imports we need, such as the implements function from the zope.interface module:
from zope.interface import implements
We need to use a few classes and/or functions provided by
the core of our codebase, i.e. CMF/Archetypes. It is possible to have access to all the classes and helper functions
made
publicly available by Archetypes, by importing its façade or API module (Products.Archetypes.atapi) this way:
from Products.Archetypes import atapi
i18n support
It is always a good idea to have an i18n-enabled application. To start using Zope's i18n support, let's import the MessageFactory object created in the product's startup module:
from example.archetype import exampleMessageFactory as _
The MessageFactory referenced with the _ symbol can now be used to provide i18nized labels, descriptions, and all the miscellaneous text snippets that are injected in the UI, also known as "messages". For a content type implementation, this is useful for UI widgets; for example to define the label of the content title field widget, we could define label = _(u'Title'). (See later for how we make use of this tool/practice.)
ATContentTypes-based schema definition
You can base your implementation directly on these stock Archetypes schemas. But you can add better support for Plone's UI and content management policies (such as the parameters that allow showing/hiding contents in the navigation menu), by basing the implementation on ATContentTypes' base schema, ATContentTypeSchema. To be compatible with that schema, you will also need to inherit from ATContentTypes' ATCTContent base class.
Let's add the import of modules we need for that:
from Products.ATContentTypes.content import base from Products.ATContentTypes.content import schemataThen, we import things internal to our product package, such as our defined interface(s) and the configuration module (for access to things such as
PROJECTNAME and MESSAGE_PRIORITIES):
from example.archetype.interfaces import IInstantMessage from example.archetype import config
Now, we have everything we need to start building the schema, and then the class that will use it. We start out by copying ATContentTypes' ATContentTypeSchema, and we extend it by adding our specific fields and/or overriden field properties.
schema = schemata.ATContentTypeSchema.copy() + atapi.Schema((
atapi.StringField('priority',
vocabulary = config.MESSAGE_PRIORITIES,
default = 'normal',
widget = atapi.SelectionWidget(label = _(u'Priority')),
),
atapi.TextField('body',
searchable = 1,
required = 1,
allowable_content_types = ('text/plain',
'text/structured',
'text/html',),
default_output_type = 'text/x-html-safe',
widget = atapi.RichWidget(label = _(u'Message body')),
),
))
Notes:
-
To instantiate an Archetypes schema object, you pass a tuple of field objects to the 'Schema' class.
We define the body of the InstantMessage object using a RichWidget, so the user can use formatting with a WYSIWYG editor.
The full list of out-of-the-box available Fields and Widgets can be found in the Fields section at the end of the manual. You can find more 3rd party fields and widgets here.
Content-type class definition
The last step is to create the class for the InstantMessage content. It inherits from ATContentTypes' ATCTContent, which itself is based on AT's BaseContent, which automatically gives its 'id' and 'title' attributes, and the entire Dublin Core metadata set (Title, Description, Creator, CreationDate, etc):
class InstantMessage(base.ATCTContent):
"""An Archetype for an InstantMessage application"""
implements(IInstantMessage)
schema = schema
The first information we add for the class definition is saying that it implements the IInstantMessage interface that we have previously defined (in interfaces.py) and imported.
implements(IInstantMessage)
The next thing is assigning the reference of the Archetypes schema, using the schema class attribute.
schema = schema
The content class definition is done. Now, we are ready to activate the content type in Archetypes'
internal types registry. This is done using the helper function called registerType.
atapi.registerType(InstantMessage, config.PROJECTNAME)
Congratulations! You have just created your first Archetype for Plone! It allows you to handle the content of an instant message with Zope-based persistent objects which:
- can be added within your Plone site,
- published by the Zope Publisher, which means you can visit them via their URLs, etc...
- searched since they are automatically indexed,
- etc...
But wait! You have some final packaging work to do to ease installation of the product within your Plone site.
Notes:
-
At the content class level, you could also provide the 'actions' attribute useful for defining the settings of the type's actions (for the portal_actions tool). In Plone 3, this is no more needed, since this is part of the FTI's configuration details, and should be provided using GenericSetup, in the types-related XML files (i.e. 'profiles/default/types/InstantMessage.xml'). Same for the aliases.
The __init__ module
The trick here is to simply import the message module so that all the code of that module gets interpreted as soon as the Python interpreter initializes the package.
import message
2.2.7. Adding a custom view for the content
Providing the custom presentation template for the InsantMessage, using Zope's browser layer mechanism.
The browser layer concept
A browser layer is a concept introduced by Zope Component Architecture (Zope 3), and which can be used in Plone. It is useful for registering views and resources (images, CSS, JS) for the site, in a way that they can override default elements (which are implicitly registered for the default browser layer) or be overriden when needed, even through the ZMI. A browser layer is similar in purpose to a CMF skin layer, but is implemented differently.
To add a browser layer to your product, you need 3 steps:
- Define the marker interface for the browser layer (for example,
example.archetype.interfaces.IInstantMessageSpecific.) - Add an XML file in your extension profile named
browserlayer.xmlproviding the browser layer settings to the site. (This step is covered later as part of the various product setup details.) - Register (using ZCML) your browser views, templates and resources.
Defining the browser layer interface
Add a marker interface for the browser layer (in interfaces.py):
from plone.theme.interfaces import IDefaultPloneLayer class IInstantMessageSpecific(IDefaultPloneLayer): """Marker interface that defines a Zope 3 skin layer for this product. """
Adding and registering the browser template
To provide a custom view template for your content type, you need a page template called
instantmessage.pt in the browser/ directory, and a ZCML declaration in the configure.zcml to associate the template to the IInstantMessageSpecific Zope 3 skin layer.
<configure xmlns="http://namespaces.zope.org/zope" xmlns:browser="http://namespaces.zope.org/browser" i18n_domain="example.archetype" > <browser:page for="example.archetype.interfaces.IInstantMessage" layer="example.archetype.interfaces.IInstantMessageSpecific" name="instantmessage_view" template="instantmessage.pt" permission="zope2.View" /> </configure>
Here is the example template code:
<html metal:use-macro="here/main_template/macros/master"
i18n:domain="plone" >
<body>
<div metal:fill-slot="main"
tal:define="priority here/getPriority;
priority_color python:(priority == 'high' and 'red') or (priority == 'low' and 'green') or ''" >
<h1 tal:content="context/Title"
tal:attributes="style string:color:$priority_color" >
Title
</h1>
<p tal:content="structure here/getBody" />
<div class="documentByLine">
Message by <span tal:content="context/Creator" />
with <strong tal:content="priority" /> priority.
-
<span tal:replace="python:here.toLocalizedTime(context.CreationDate(),long_format=1)" />
</div>
</div>
</body>
</html>
Python notes:
-
The new methods we use on the content object (getPriority, getBody, etc), called the "accessors", are generated by Archetypes as part of its internal mechanisms, based on the field definition in the content schema; so if the field is called 'priority', there is a generated method called 'getPriority' responsible to return the stored value on the object. Note that the code of the method is not available somewhere for modification ; "generated" here means it is available in the server's memory, within Archetypes engine's registries, when the Zope server has started.
After the product installation step, which we still have to discuss (see later), Plone should be able to find this template and use it as the content object's default view when you invoke the content's URL.
2.2.8. Installing the product
Ensuring the product elements (types, browser layers, resources) are correctly installed.
In this part, we will provide the code to be executed when the integrator "adds", i.e. installs, the InstantMessage product to the Plone site. This aspect of the product code is called the "Extension Profile" (or "Setup Profile") and is managed under the hood by a machinery called GenericSetup.
For more about GenericSetup, its possibilities, and how a developer uses it, read the GenericSetup tutorial.The setup profile files (profiles/default)
The setup profile is composed of a set of GenericSetup XML files containing setup declarations.
Type declaration and definition
First, we provide the files needed for adding the types to CMF's types registry (portal_types): types.xml and types/InstantMessage.xml.
In types.xml, within the <object name="portal_types" ... /> element, add the setup code for the type(s) you want to install:
<?xml version="1.0"?>
<object name="portal_types" meta_type="Plone Types Tool">
<property
name="title">Controls the available content types in your portal</property>
<object name="InstantMessage"
meta_type="Factory-based Type Information with dynamic views"/>
</object>
The name property of the <object> node constitutes the called portal type name of the content-type, a CMF concept which supports two things:
- Dynamic typing: objects can change their content
type during their lifetime. To do this use
_setPortalTypeName(<type>). - You can have arbitrarily many different content types
using the same base class (and having therefore the same
meta_type) but differing in their Factory Type Information (FTI) settings.
The portal type name was formerly set in a content-type class attribute called portal_type, which is no longer necessary.
The name of the file inside the profiles/default/types folder must match the portal type name, with spaces converted to underscores whenever necessary. So, in types/InstantMessage.xml, add the code for the InstantMessage FTI object:
<?xml version="1.0"?>
<object name="InstantMessage"
meta_type="Factory-based Type Information with dynamic views"
i18n:domain="example.archetype" xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<property name="title" i18n:translate="">Example AT - InstantMessage</property>
<property name="description"
i18n:translate="">An example type (InstantMessage) discussed in the AT Developer Manual.</property>
In these first lines we give the content-type a title and a description.
The title property indicates the user-friendly name of the content-type. This is what's supposed to be used in the user
interface, and can be accessed using the <fti>.title_or_id() or the Type() methods, which
both return the content-type title if it exists or the content-type id
otherwise. Like portal type, this property was formerly set in a the content-type class attribute called archetype_name, which is no longer neccessary.
<property name="content_meta_type">InstantMessage</property> <property name="content_icon">document_icon.gif</property> <property name="product">example.archetype</property> <property name="factory">addInstantMessage</property>
The meta_type property of the object is a Zope concept to organize object classification or containment.
For historical reasons, it is used in
CMF in some places because first versions of
CMF didn't have today's portal_type. Also note that Archetypes uses the content-type class name as the meta_type value, unless given explicitly.
The content_icon property specifies the icon image file which will be shown in the Plone UI for this content-type. This icon image file must be accessible from the context of the content-type, and
therefore should be placed into a CMF skin layer (the CMF way) or in a browser resource directory (the Zope 3 way).
The factory property indicates the factory function which
will be used to create and initialize new content objects of this type.
This factory is automatically generated by the Archetypes framework,
when the product is initialized (via the code in the startup module), and is always named add<content-meta-type>. The factory is also associated with a certain product by means of the product property.
<property name="immediate_view">atct_edit</property> <property name="global_allow">True</property> <property name="filter_content_types">False</property> <property name="allow_discussion">False</property>
The global_allow property determines if the content-type will be available to be added from anywhere in the site.
The filter_content_types property, paired with allowed_content_types, controls what content-types will be addable inside the current one.
With allow_discussion, we specify whether or not comments will be allowed by default on this content-type.
<property name="default_view">@@instantmessage_view</property>
<property name="view_methods">
<element value="@@instantmessage_view" />
</property>
<alias from="(Default)" to="@@instantmessage_view" />
<alias from="edit" to="atct_edit" />
<alias from="sharing" to="@@sharing" />
<alias from="view" to="@@instantmessage_view" />
Here we define CMF views (templates) and aliases that map content-type methods to views.
<action title="View" action_id="view" category="object" condition_expr=""
url_expr="string:${object_url}/" visible="True">
<permission value="View" />
</action>
<action title="Edit" action_id="edit" category="object" condition_expr=""
url_expr="string:${object_url}/edit" visible="True">
<permission value="Modify portal content" />
</action>
</object>
The <action> elements register type-specific actions for the content-type. The object category makes the render as tabs in the Plone UI.
- The
url_expris a TALES expression that defines the URL from where the action will be triggered and should match one of the method aliases defined above. Hence, theeditaction points tostring:${object_url}/edit, which means that if you are at/path/to/objectand clickedit, you will go to/path/to/object/edit./editthen gets recognized as a method alias, which points to the page templateatct_edit, causing Zope to render/path/to/object/atct_edit. - The
<permission /> element specifies a guard permission for this
action. If the user's role doesn't have this permission, the action
won't be available and the corresponding action tab won't be shown.
- In addition to the former criteria, the
condition_expris a TALES expression which will be evaluated to decide if the action is available or not. - The
visibleattribute indicates wheter the action tab will be visible or hidden. If it's set to False, the tab won't appear even when the action is available, but the exposed page will still be accesible from the associated URL.
- Defining new content-type actions this way, i.e. using GenericSetup, supersedes the old
updateActionsfunction fromATContentTypes.content.base. - Don't worry. You don't have to type all this XML each time you create a new content-type; since most of it is boilerplate (XML is very verbose) you can copy & paste an already working example (like the CMFPlone ones) and modify only the changing bits.
Type factory
We also need the file useful for setting the type against Plone's factory tool (portal_factory): factorytool.xml.
This is needed so that when a user adds a content object and then
clicks Cancel in the edit form, a stale object won't be lying around.)
<?xml version="1.0"?> <object name="portal_factory" meta_type="Plone Factory Tool"> <factorytypes> <type portal_type="InstantMessage"/> </factorytypes> </object>
Roles - Permissions mapping
For our content type(s) to be usable, we need to assign the required "Add" permission to the Plone site's default roles: Contributor, Owner, and Manager. This is done using the rolemap.xml file as follows:
<?xml version="1.0"?>
<rolemap>
<permissions>
<permission name="example.archetype: Add InstantMessage" acquire="True">
<role name="Manager"/>
<role name="Owner"/>
<role name="Contributor"/>
</permission>
</permissions>
</rolemap>
Browser skin layer
In order to install our browser skin layer, we also add a browserlayer.xml file with the following code:
<?xml version="1.0"?>
<layers>
<layer name="example.archetype"
interface="example.archetype.interfaces.IInstantMessageSpecific" />
</layers>
Registering our setup profile
This last step ensures everything can work. We update the package's configure.zcml file with the code snippet that will load the extension profile:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="example.archetype" >
<five:registerPackage package="." initialize=".initialize" />
<include package=".browser" />
<genericsetup:registerProfile
name="default"
title="Example Archetype content - InstantMessage"
directory="profiles/default"
description="Extension profile for Example AT - InstantMessage"
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
</configure>
Restarting Zope
Now that you have a first version of your product ready to be tested, and installed via your buildout, you need to (re)start Zope.
Quick-installing the product
Back in the Plone configuration (or Plone control panel), when you visit the "Add/Remove Products" interface or the portal_quickinstaller tool through the ZMI (at the root of the site), you can see the product show up under the category of "installable products".
Select and click the button to install the product. If everything goes fine, the product should be installed, and you're ready to start using it!
2.2.9. Basic integration tests
No product is complete without tests.
To build high-quality software, you must provide automatic tests - often known as "unit" tests (though tests for Archetypes products tend to be "integration" tests, strictly speaking).
The tutorial on testing and test-driven development is essential reading if you want to write high-quality software (and you don't know the techniques it advocates already). Please refer to it for details.
The example.archetype product contains basic tests that prove that the product is properly installed, that it registers its types, and that an InstantMessage object can actually be instantiated. If it contained more functionality, there would have been more tests, but even simple integration tests like this can be surprisingly useful - if you accidentally broke the content type with some change, you'd notice that it failed to install or instantiate.
The tests are in the "tests" directory. The file "base.py" contains some base classes that are used for tests, to ensure the site is properly set up:
import unittest
from zope.testing import doctestunit
from zope.component import testing
from Testing import ZopeTestCase as ztc
from Products.Five import zcml
from Products.Five import fiveconfigure
from Products.PloneTestCase import PloneTestCase as ptc
from Products.PloneTestCase.layer import PloneSite
from Products.PloneTestCase.layer import onsetup
@onsetup
def setup_product():
"""Set up the package and its dependencies.
The @onsetup decorator causes the execution of this body to be deferred
until the setup of the Plone site testing layer. We could have created our
own layer, but this is the easiest way for Plone integration tests.
"""
fiveconfigure.debug_mode = True
import example.archetype
zcml.load_config('configure.zcml', example.archetype)
fiveconfigure.debug_mode = False
ztc.installPackage('example.archetype')
setup_product()
ptc.setupPloneSite(products=['example.archetype'])
class InstantMessageTestCase(ptc.PloneTestCase):
"""Base class for integration tests.
This may provide specific set-up and tear-down operations, or provide
convenience methods.
"""
The actual tests are in "test_setup.py":
from base import InstantMessageTestCase
from example.archetype.interfaces import IInstantMessage
class TestProductInstall(InstantMessageTestCase):
def afterSetUp(self):
self.types = ('InstantMessage',)
def testTypesInstalled(self):
for t in self.types:
self.failUnless(t in self.portal.portal_types.objectIds(),
'%s content type not installed' % t)
def testPortalFactoryEnabled(self):
for t in self.types:
self.failUnless(t in self.portal.portal_factory.getFactoryTypes().keys(),
'%s content type not installed' % t)
class TestInstantiation(InstantMessageTestCase):
def afterSetUp(self):
# Adding an InstantMessage anywhere - can only be done by a Manager or Portal Owner
self.setRoles(['Manager'])
self.portal.invokeFactory('InstantMessage', 'im1')
def testCreateInstantMessage(self):
self.failUnless('im1' in self.portal.objectIds())
def testInstantMessageInterface(self):
im = self.portal.im1
self.failUnless(IInstantMessage.providedBy(im))
def test_suite():
from unittest import TestSuite, makeSuite
suite = TestSuite()
suite.addTest(makeSuite(TestProductInstall))
suite.addTest(makeSuite(TestInstantiation))
return suite
To run these tests within your buildout environment:
./bin/instance test -s example.archetype
You may see output like:
Ran 4 tests with 0 failures and 0 errors in 0.119 seconds.
If there was an error with one or more of the tests, you'd be told here!
Please refer to the testing tutorial for more about writing tests - and writing good tests - and how to run them.
2.2.10. Troubleshooting
When creating new content types, many factors can silently fail due to human errors in the complex content type setup chain and security limitations. The effect is that you don't see your content type in Add drop down menu. Here are some tips for debugging.
1. Is your product broken due to Python import time errors? Check the *Zope Management Interface (ZMI from now on) â Control Panel â Products*. Turn on Zope debugging mode to trace import errors. To see error messages directly in the console with buildout, use *bin/instance fg*.
2. Have you rerun the quick installer (GenericSetup) after creating/modifying the content type? If not, (re)install the product from the *Plone Control Panel â Add-on Products* or from the *ZMI â portal_quickinstaller*.
3. Do you have a correct Add permission for the product? Check the call of the ``ContentInit()`` method inside the *__init__.py* file. See `The startup module <http://plone.org/documentation/manual/archetypes-developer-manual/a-semi-realistic-example/the-startup-module>`_.
4. Does it show up in the portal factory? Check *ZMI â portal_factory* and *factorytool.xml*.
5. Is it correctly registered as a portal type and implictly addable? Check *ZMI â portal_types*. Check *default/profiles/type/yourtype.xml*.
6. Does it have a correct product name defined? Check *ZMI â portal_types*.
7. Does it have a proper factory method? Check *ZMI â types_tool*. Check Zope logs for ``_queryFactory`` and import errors.
8. Does it register itself with Archetypes? Check *ZMI â archetypes_tool*. Make sure that you have ``ContentInit`` properly run in your *__init__.py*. Make sure that all modules having Archetypes content types defined and ``registerType()`` calls are imported in *__init__py*.
2.3. Fields
Fields for Archetypes
2.3.1. Fields Reference
Attributes of standard Archetypes fields.
Topics |
||
|---|---|---|
|
|
||
Common Field Attributes
These attributes are common to nearly all fields. Field-specific attributes follow, and are listed by field. Particular fields have different defaults, types, and some other specialized attributes.
| Name | Description | Possible Values | Default |
|---|---|---|---|
| accessor | The name of a class method that will return the value of the field. Use this to
change how the field is retrieved. If you don't provide a custom method
name here, a default accessor, named getYourFieldName, is going to be created
that just returns the value of the Field. |
A class method name; for example, specialGetMethod | None |
| default |
The default value for the field. |
Type should be appropriate to the field. |
None |
| default_method |
The name of a class method returning a value for the field. | A class method name; for example, getSpecialDescription. |
None |
| edit_accessor | The name of a class method that returns the raw value of a field. | Any method name (for example, rawGetMethod). |
None |
| enforceVocabulary | Determines whether or not values outside the vocabulary will be accepted.
If True, Archetypes will validate input for the field against the vocabulary.
Only values already in the vocabulary will be accepted. |
True or False. |
False |
| index (Plone < 3 only) |
If you want this field to be placed in its own catalog index, then specify the type of index here as a string. If you append :schema onto the end of the schema, then this will also be added as a metadata column. (The actual index will be on the field accessor, typically "getFieldName".) Ignored in Plone 3+; use GenericSetup profile for similar functionality. |
The name of any index, such as KeywordIndex or KeywordIndex:schema. |
None |
| index_method | May be used to specify the method called when indexing a field. Use '_at_accessor' to use the default accessor, '_at_edit_accessor' to use the edit accessor, or the name of a method returning the value to be indexed. | _at_accessor, _at_edit_accessor, getIndexAccessor and getIndexAccessorName |
_at_accessor |
| languageIndependent |
Flag for Fields that are independent of the language, such as dates. True tells LinguaPlone that no translation is necessary for this field. | True or False | False |
| isMetadata |
Marks metadata fields. This is currently only needed as a convenience for the filterFields method of Schema. Fields marked as metadata are not displayed in the uncustomized base view. | True or False | False |
| mode |
The read/write mode of field, as a string; the default is to be read and write.
Accessors will not be created without the read mode, and Mutators will not be created without the write mode. |
For read only: r, for write only: w, for read and write: rw. |
rw |
| multiValued |
Set this to True if the field can have multiple values. This is the case for fields like multiple-selection lists that allow the selection of multiple values. | True or False. |
False |
| mutator |
The string name of a class method that changes the value of the Field. If you don't provide a special method name here, a default mutator is generated with the name 'setYourFieldName' to simply store the value. | A class method name; for example, specialSetMethod. |
None |
| name | A unique name for this field.
Usually specified as the first item in the field definition.
|
Any string. Strongly recommended: lowercase, no punctuation or spaces,
conforming to standard Python identifier rules.
For example, description, user_name, or coffee_bag_6. |
No default. |
| primary | If True, this will be the field that used for File Transfer Protocol (FTP) and WebDAV requests. There can be only field that does this; if multiple are defined, the first one in the schema will be used. You normally set this for the main body attribute. Only used for TextField and FileField field types. | True or False | False |
| read_permission |
The permission required for the current user to allowed to view or access the field. Only useful if the read mode is activated. This read permission is checked when rendering the widget in read mode. | A permission identifier imported from Products.CMFCore.permissions | View |
| required |
Specifies that some value for this field required. | True or False. | False |
| schemata |
Use named schematas to organize fields into grouped views. | A short string that labels the group. |
default |
| searchable |
Specifies whether or not the field value will be indexed as part of the SearchableText for the content object. SearchableText is what is checked by the portal's main search. | True or False. | False |
| storage |
The storage mechanism for the field. The default is Attribute Storage, which stores the field as an attribute of the object. | Any valid storage object such as AttributeStorage or SQLStorage.
You can find these in the Archetypes Application Programming Interface (API). |
AttributeStorage |
| type |
Provided by the field class.. Should never be changed in a Schema. | None |
None |
| validators |
A list or tuple of strings naming validators that will check field input.
If you only have one validator, you may specify it as a string. Validators may also be instances of a class implementing the IValidator interface from from Products.validation.interfaces.IValidator. Providing a class instance allows you more flexibility as you may set additional parameters. Validators are checked in order specified. |
The names of validators registered via Products.validation; for example, isEmail. | () |
| vocabulary |
Provides the values shown in selection and multi-selection inputs.
This may be specified as a static list or as the name of a class method returning
the choice list. |
A list of strings (in which case keys and values will be the same); a list of 2-tuples of strings [("key1","value 1"),("key 2","value 2"),...]; a Products.Archetypes.utils.DisplayList. Or, the name of a class method returning any of the above. | () |
| vocabulary_factory | Like the vocabulary attribute, in Plone 3 provides the values shown in selection and multi-selection inputs. | A string name of a Zope 3 style vocabulary factory (a named utility providing zope.schema.interfaces.IVocabularyFactory) | None |
| widget | The widget that will be used to render the field for viewing and editing.
See the widget reference for a list of available widgets. |
An instance of a widget; for example, StringWidget(). |
StringWidget() |
| write_permission |
The permission required for the current user to edit the field value. Only interesting if the write mode is activated. The write permission is checked when rendering the widget in write mode. | A permission identifier imported from Products.CMFCore.permissions | ModifyPortalContent |
Standard Fields
BooleanField
Simple storage of True or False for a field.
Standard properties| Name | Type | Default | Description | example values |
|---|---|---|---|---|
| widget | widget | BooleanWidget | Implemented as a check box. | |
| default | boolean | False |
|
|
| type | boolean |
|
|
Note: The required attribute for the boolean field is often confusing. It does not require that the box be checked. Use a validator if you need to require the box be checked.
ComputedField
Read-only field, whose content cannot be edited directly by users, but is
computed instead from a Python expression. For example, it can be the result of
an operation on the contents from some other fields in the same schema,
e.g. calculating the sum of two or more currency amounts, or composing a
full name from first name and surname.
This field is usually not stored in the database, because its content is
calculated on the fly when the object is viewed.
| Name | Type | Default | description | some possible values |
|---|---|---|---|---|
| widget | widget | ComputedWidget | ||
| storage | storage | ReadOnlyStorage | |
|
| type | computed | |
||
| mode | string | r | |
| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| expression |
|
Evaluated on the object to compute a value. | |
CMFObjectField
Used for storing values inside a CMF Object, which can have workflow. Can only be used for BaseFolder-based content objects.
Standard properties| Name | Type | Default | description | some possible values |
|---|---|---|---|---|
| widget | widget | FileWidget | ||
| storage | storage | ObjectManagedStorage | |
|
| type | object | |
| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| portal_type | File |
|
|
|
| workflowable | True |
|
|
|
| default_mime_type | application/octet-stream |
|
|
DateTimeField
Used for storing dates and times.
Standard properties| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| widget | widget | CalendarWidget |
|
|
| default | DateTime |
|
|
|
| type | datetime |
|
|
Note: The default for the DateTimeField needs to be specified as a DateTime object. If you need to set the current date/time as the default, you'll need to use the default_method attribute to specify a class method returning the current date/time as a DateTime object.
Example:
from DateTime.DateTime import DateTime
...
# inside the schema definiton
DateTimeField('dateAdded',
searchable = 1,
required = 0,
default_method = 'getDefaultTime',
widget = CalendarWidget(
label = 'Date Added'
),
),
...
#inside the content class definition
def getDefaultTime(self): # function to return the current date and time
return DateTime()
FileField
Storage for large chunks of data such as plain-text files, office-automation documents, and so on. If you're using Plone 4 or newer, consider using plone.app.blob.field.BlobField instead, that stores the file data outside of the ZODB and accepts the same parameters as atapi.FileField. See this page for info about migration.
Standard properties| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| widget | widget | FileWidget | ||
| default | string |
|
|
|
| type | file |
|
|
| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| primary | False |
|
|
|
| default_content_type | application/octet |
|
|
|
| primary | boolean | False | Set this True to mark the field as primary for FTP or WebDAV. |
Note: File field values are stored as strings. It's a common practice to use streams to read/write the values as if they were files.
FixedPointField
For storing numerical data with fixed points.
Standard properties| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| widget | widget | DecimalWidget | ||
| validators | validators | isDecimal |
|
|
| default | string | 0.00 |
|
|
| type | fixedpoint |
|
|
| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| precision | 2 |
|
|
FloatField
For storing numerical data with floating points.
Standard properties| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| default | string | 0.0 | |
|
| type | float |
|
|
ImageField
Stores an image and allows dynamic resizing of the image. If you're using Plone 4 or newer, consider using plone.app.blob.field.ImageField instead, that stores the image data outside of the ZODB, and accepts the same parameters as atapi.ImageField. See this page for info about migration.
Standard properties| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| widget | widget | ImageWidget | ||
| default | string |
|
|
|
| type | image |
|
|
|
| allowable_content_types | tuple of MIME strings | Specifies the types of images that will be allowed. | ('image/gif','image/jpeg','image/png') | ('image/jpeg','image/png') |
Note: Archetypes Image field values are stored as strings. It's a common practice to use streams to read/write the values as if they were files.
Special properties| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| original_size | tuple (w,h) | None | The size to which the original image will be scaled. If it's None, then no scaling will take place; the original size will be retained. Caution: the aspect ratio of the image may be changed. | (640,480) |
| max_size | tuple (w,h) | None | If specified then the image is scaled to be no bigger than either of the given values of width or height. Aspect ratio is preserved. Useful to prevent storage of megabytes of unnecessary image data. | (1024,768) |
| sizes | dict | {'thumb':(80,80)} |
A dictionary specifying any additional scales in which the image will be available. Dictionary entries
should be of the form 'scaleName':(width,height). The scaled versions will be accessible as object/<imagename>_<scalename>, e.g. object/image_mini. |
{ 'mini' : (80,80), 'normal' : (200,200), 'big' : (300,300), 'maxi' : (500,500)} |
| pil_quality | integer | 88 | A JPEG quality setting (range 0 to 100). Lower numbers yield high compression and low image quality. High numbers yield low compression and better quality. | 50 (a medium quality) |
Using Image Scales
To display the original image (possibly rescaled if you used original_size or max_size attributes), you may use a URL like "http://url_of_content_object/imageFieldName" as the SRC attribute of an IMG tag where url_of_content_object is the URL of the content object and imageFieldName is the name of the image field.
To display one of the scales, use a URL like "http://url_of_content_object/imageFieldName_scale",
where scale is one of the keys of the sizes dictionary.
Attention: The direct attribute access as shown above works only together with AttributeStorage, which will be used by default. To avoid heavy memory consumption on sites with many images it is recommended to use AnnotationStorage for the ImageField.
You may also generate a ready-to-insert IMG tag with the python code:
obj.getField('image').tag(obj, scale='mini')
if obj is your content object, image the name of your image field, and mini the name of your scale.
You may rescale to other sizes than those in the sizes field attribute with code like:
obj.getField('image').tag(obj, height=480, width=640, alt='alt text',
css_class='css_class_selector', title='html title attribute')
From Plone 4 on, the plone.app.imaging package introduces a new way to control image scales, factoring this functionality out of Archetypes for reutilization. For example:
<img tal:define="scales context/@@images;
thumbnail python: scales.scale('image', width=64, height=64);"
tal:condition="thumbnail"
tal:attributes="src thumbnail/url;
width thumbnail/width;
height thumbnail/height" />
Would create an up to 64 by 64 pixel scaled down version of the image stored in the "image" field of the context. For further info, check the plone.app.imaging README file.
IntegerField
For storing numerical data as integers.
Standard properties| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| widget | widget | IntegerWidget |
|
|
| default | integer | 0 |
|
|
| type | integer |
|
|
| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| size | 10 | Sets the size of the input field. | |
LinesField
Used for storing text as a list, for example a list of data such as keywords.
Standard properties| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| widget | widget | LinesWidget | ||
| default | string | () |
|
|
| type | lines |
|
|
ReferenceField
Used for storing references to other Archetypes Objects.
Standard properties| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| widget | widget | ReferenceWidget | ||
| index_method | _at_edit_accessor |
|
|
|
| type | reference |
|
|
|
| multiValued | boolean | False | Set multiValued True to allow multiple references (one-to-many), or False to allow only a single reference (one-to-one). | |
| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| relationship |
|
Specifes an identifier for the type of relationships associated with the field. This should be unique within your content type, but has no larger meaning. A ReferenceField allows you to edit the set of references with a particular relationship identifier from the current content object to other objects. | 'KnowsAbout', 'Owns', 'WorksWith' | |
| allowed_types | tuple of portal types | () | Determines all the portal types that will be searched to find objects that the user can make a reference to. It also specifies the Types that should be allowed to be added directly from the reference widget. This is only activated if the addable property is set. An empty list or tuple will allow references to all portal types. | ('Document', 'File') |
| allowed_types_method | string | None | A string containing the name of a class method that will return a list of portal types to which references are allowed. | |
| vocabulary_display_path_bound | integer | 5 | Sets a limit for presentation of reference items. Up to this limit, only titles are displayed. Above the limit, the path to the referenced object is also displayed. The idea is that if there are a large number of referenced items, the user will need help to differentiate them. | |
| vocabulary_custom_label | string | None | A string containing a python expression that will be evaluated to get the displayed text for a referenced item. Your expression may use the variable "b" which will be a reference to the catalog brain returned by the reference lookup. | "b.getObject().title_or_id()" |
More about References
Archetypes References work with any object providing the IReferenceable interface. They are mantained in the uid_catalog and reference_catalog catalogs. You can find both at the root of your Plone site. Check them to see their indexes and metadata.
Althought you could use the ZCatalogs API to manage Archetypes references, these catalogs are rarely used directly. A ReferenceField and its API is used instead.
To set a reference, you can use the setter method with either a
list of UIDs or one UID string, or one object or a list of objects (in
the case the ReferenceField is multi-valued) to which you want to add a
reference to. Note that None and [] are equal.
For example, to set a reference from the myct1 object to the areferenceableobject object using the MyReferenceField field:
>>> myct1.setMyReferenceField(areferenceableobject)
To get the referenced object(s), just use the getter method. Note that what you get are
the objects themselves, not their catalog brains.
>>> myct1.getMyReferenceField()
StringField
A field for plain-text, unformatted strings.
Standard properties| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| default | string |
|
|
|
| type | string |
|
|
|
| widget | widget | StringWidget |
|
| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| default_content_type | string MIME type | text/plain |
|
Rarely changed. |
TextField
A string field typically used for longer, multi-line strings. The string may also be transformed into alternative formats.
Standard properties| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| default | string |
|
|
|
| type | text |
|
|
|
| widget | widget | StringWidget |
|
| Name | Type | Default | Description | some possible values |
|---|---|---|---|---|
| primary | boolean | False |
Set this True to mark the field as primary for FTP or WebDAV.
|
|
| default_content_type | string MIME type | text/plain | A string designating MIME the default input type for the field. | text/plain, text/html |
| allowable_content_types | tuple of MIME-type strings | ('text/plain',) | Used in the TextArea and Rich widgets to let the user choose between different text formats in which the content is entered. | ('text/plain', 'text/html',) |
| default_output_type | string MIME type | text/plain | This is used by the accessor (get) method to and decides which MIME-Type the content should be transformed into if no special MIME-Type is demanded. | 'text/html', 'text/x-html-safe' |
2.3.2. Widgets Reference
This page is a syntax reference and general guide for defining and using Widgets.
Widget Attribute Topics
|
||
|---|---|---|
Common Widget Attributes
The table below describes attributes common to nearly all widgets. Illustrations and special attributes listings for each of the standard widgets follows.
| Name | Description | Possible Values |
|---|---|---|
| condition |
A string containing a TALES expression to determine whether or not a field/widget is
included on a view or edit page.
This does not distinguish between view and edit mode. |
Your TALES expression may referenc the current context as 'object' and the Plone site root as 'portal' |
| description |
Help or explanatory text for the field. Usually shown on the edit form under the label and above the input field. | |
| description_msgid | The i18n identifier for the description message. Used to translate the message. Should be unique within your product's i18n domain. | 'help_type_field' |
| label | The label that will appear in the field. | Any string, for example, Start Date for a field start_date. Also label_msgid (takes string message ids for i18n.) |
| label_msgid | The i18n identifier for the label message. Should be unique within your product's i18n domain. | 'label_type_field' |
| i18n_domain | The i18n domain specifier for your product. This should be unique for your product, and will be used to find the translation catalogs for your product. | 'productname' |
| modes | The modes that this widget will be shown in; by default there are two modes: view and edit. | A list of modes as strings; by default ("view", "edit"). |
| populate | If this is enabled, the view and edit fields will be populated. Usually this is enabled, but for fields such as a password field, this shouldn't be the case. Usually this is true by default. | True or False |
| postback | If this is enabled, then when an error is raised, the field is repopulated; for fields such as a password field, this shouldn't be the case. Usually this is True by default. | True or False |
| visible | Determines whether or not the field is visible view and edit mode. This is a dictionary mapping the view mode to a string describing the visibility. Choices are visible, hidden (rendered in an HTML hidden form value), invisible (not rendered at all). | For example, {'view': 'visible', 'edit': 'hidden' } means that the view will show, but the edit page will hide the value. |
Standard Widgets
BooleanWidget
Renders an HTML checkbox, from which users can choose between two values such as on/off, true/false, yes/no.
CalendarWidget
Renders a HTML input box with a helper popup box for choosing dates.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| format | string |
|
Defines the date/time format using strftime, e.g. '%d.%m.%Y', for the view.
(See the strftime section of the Python time documentation.
If this is not specified, the long form of the portal's local time format is used. |
| future_years | integer | 5 | Specifies the number of future years offered by the year drop-down portion of the date widget. Do not use both future_year and end_year. (Plone 2.5+) |
| starting_year | integer | 1999 | The first year offered by the year drop-down. (Plone 2.5+) |
| ending_year | integer | None | The final year offered by the year drop-down. Do not use both future_years and end_year. (Plone 2.5+) |
| show_hm | boolean | True | Should the widget ask for a time as well as a date? (Plone 2.5+) |
ComputedWidget
Generally used for ComputedField field type, it renders the computed value. Note that if your field has a vocabulary, and the field value is a key in that vocabulary, the widget will lookup the key in the vocabulary and show the result.
Standard Properties
| Name | Type | Default | Description |
|---|---|---|---|
| modes | tuple | ('view', 'edit') | As ComputedField is a read-only field, this property can be used to prevent the widget from appearing in edit templates, by setting it to just ('view',). |
DecimalWidget
In edit mode, renders an HTML text input box which accepts a fixed point value.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| thousands_commas | boolean | False | In view mode, formats the value to shows commas for thousands. For example, when thousands_commas is True, "7632654849635.02" is displayed as "7,632,654,849,635.02". (Note: this feature is not localized; it uses commas independent of locale. |
| whole_dollars | boolean | False | Shows whole dollars in view, leaving out the cents. Enter "1.123", and "$1" is shown. (Note: this feature is not localized; it uses the dollar sign independent of locale.) |
| maxlength | 255 | Maximum input size; sets the HTML input tag's maxlength attribute. | |
| dollars_and_cents | boolean | False | In view mode, shows dollars and cents. Enter "123.123" and "$123.12" is shown. (Note: this feature is not localized; it always uses the dollar sign, period, and two digits precision.) |
| size | 5 | Size of the input field; sets the HTML input tag's size attribute. |
FileWidget
Renders an HTML widget so a user can upload a file.
ImageWidget
Renders an HTML widget that can be used to upload, display, delete, and replace images. You can provide a display_threshold that allows you to set the size of an image; if it's below this size, the image will display in the Web page.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| display_threshold | integer | 102400 | Only display the image inline if img.getSize() <= display_threshold |
InAndOutWidget
In edit mode, renders a widget for moving items from one list to another. Items are removed from the source list. This can be used to choose multiple values from a list. This provides a good alternative to the MultiSelectionWidget when the vocabulary is too long for checkboxes.
Special Properties
IntegerWidget
A simple HTML input box for a string.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| size | 6 | Size of the select widget; sets the HTML select tag's size attribute. |
| Name | Type | Default | Description |
|---|---|---|---|
| maxlength | 255 | Maximum input size; sets the HTML input tag's maxlength attribute | |
| size | 5 | Size of the input field; sets the HTML input tag's size attribute. |
KeywordWidget
This widget allows the user to select keywords or categories from a list. It is
used for the Categories field in the Categorization Schema (Plone 3+)
or the equivalent Keywords field on the Properties Tab (Plone < 3)
of a content object.
Keywords are drawn from the field vocabulary and/or the unique values for the
field in a specified catalog.
Additional keywords may be added unless the enforceVocabulary property of the
field is True.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| vocab_source | portal_catalog | Sets the catalog to search for additional vocabulary to be combined with the vocabulary defined for the field. Additional keywords from existing content are found using catalog.uniqueValuesFor(fieldName). | |
| roleBasedAdd | True | Only shows the "New keywords" input for adding keywords if the current user has one of the roles stored in the allowRolesToAddKeywords property in the site_properties property sheet in portal_properties |
LabelWidget
Used to display labels on forms -- without values or form input elements.
LinesWidget
Displays a text area so that users can enter a list of values, one per line.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| rows | integer | 5 | Rows of the lines widget; sets the HTML textarea tag's rows attribute. |
| cols | integer | 40 | Columns of the lines widget; sets the HTML textarea tag's cols attribute. |
MultiSelectionWidget
A selection widget; by default it's an HTML select widget which can be used to choose multiple values. As a checkbox users can choose one or more values from a list (useful if the list is short).
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| format | string | select | Possible values: 'select' or 'checkbox'. Uses a either a series of checkboxes or a multi-selection list. Note that checkboxes have much better usability for short vocabularies. Consider using the InAndOutWidget for longer vocabularies. |
| size | 5 | Defines the size of the multi-select list. Does not apply for checkboxes. |
PasswordWidget
Renders an HTML password input.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| maxlength | 255 | Maximum input size; sets the HTML input tag's maxlength attribute. | |
| size | 20 | Size of the input field; sets the HTML input tag's size attribute. |
Standard Properties
| Name | Type | Default |
|---|---|---|
| populate | boolean | False |
| postback | boolean | False |
| modes | ('edit',) |
PicklistWidget
Similar to the InAndOutWidget, but the values stay in the source list after selection.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| size | integer | 6 | Size of the selection box; sets the HTML select tag's size attribute. |
ReferenceWidget
Renders an HTML text input box which accepts a list of possible reference
values. Used in combination with the Reference Field.
Note: In Plone 2.5 and above, the ReferenceBrowserWidget is
a usually a better choice for a reference widget due to its ability to browse for content
referenceable objects.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| checkbox_bound | 5 | When the number of items exceeds this value, multi-selection lists are used. Otherwise, radio buttons or checkboxes are used. | |
| destination | None | May be:
|
|
| addable | False | Create createObject link for every addable type | |
| destination_types | None | Either a single type given as a string, or a list of types given as a string, defining what types we allow adding to. Only applies when addable is set on the widget. |
ReferenceBrowserWidget
A sophisticated widget for browsing, adding and deleting references.
Standard in Plone 2.5+, available for earlier versions as an add-on product.
Import from Products.ATReferenceBrowserWidget.ATReferenceBrowserWidget in Plone 2.5 and 3. In Plone 4, this widget has been improved and now lives inarchetypes.referencebrowserwidget.ReferenceBrowserWidget.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| size | integer | Size of the field if not multiValued; sets the HTML input tag's size attribute. | |
| default_search_index | string | SearchableText | when a user searches in the popup, this index is used by default |
| show_indexes | boolean | False | If True, a drop-down list is shown in the popup to select the index used for searching. If set to False, default_search_index will be used. |
| available_indexes | dict | {} | Optional dictionary containing all the indexes that can be used for searching along with their friendly names. Format: {'catalogindex':'Friendly Name of Index', ... } The friendly names are shown in the widget. Caution: If you set show_indexes True, but do not use this property to specify indexes, then all the indexes will be shown. |
| allow_search | boolean | True | If True, a search form is included in the popup. |
| allow_browse | True | Allows the user to browse content to find referenceable content. | |
| startup_directory | string | '' | Directory shown when the popup opens. Optional. When empty, the current folder is used. See the ATReferenceBrowser readme.txt for advanced usage. |
| base_query | dict or name of method | Defines query terms that will apply to all searches, mainly useful to create specific restrictions when allow_browse=0. Can be either a dictonary with query parameters, or the name of a method or callable available in cotext that will return such a dictionary. | |
| force_close_on_insert | boolean | False | If true, closes the popup when the user choses insert. This overrides the default behavior in multiselect mode. |
| search_catalog | string | 'portal_catalog' | Specifies the catalog used for searches |
| allow_sorting | boolean | False | Allows changing the order of referenced objects (requires multiValued). |
| show_review_state | boolean | False | If True, popup will display the workflow state for objects. |
| show_path | boolean | False | If True, display the relative path (relative to the portal object) of referenced objects. |
| only_for_review_states | None | If set, content items are only referenceable if their workflow state matches one of the specified states. If None there will be no filtering by workflow state. | |
| image_portal_types | sequence | () | Use to specify a list of image portal_types. Instances of these portal types are previewed within the popup widget |
| image_method | string | None | Specifies the name of a method that is added to the image URL to preview the image in a particular resolution (e.g. 'mini' for thumbnails). |
| history_length | integer | 0 | If not zero, enables a history feature that show the paths of the last N visited folders. |
| restrict_browsing_to_startup_directory | boolean | False | If True, the user will not be able to browse above the starting directory. |
The cited Plone 4 implementation of this widget includes the following additional properties:
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| startup_directory_method | string | '' |
The name of a method or variable that, if available at the instance, will be used to obtain the path of the startup directory. If present, 'startup_directory' will be ignored. |
| show_results_without_query | bool |
False |
Don't ignore empty queries, but display results. |
| hide_inaccessible | bool |
False |
Don't show inaccessible objects (no permission) in view mode. |
| popup_width | integer |
500 |
Width of popup window in pixels. |
| popup_height | integer |
550 |
Height of popup window in pixels |
| popup_name | string |
'popup' |
Name of template to be used for popup. To use another template you have to register a named adapter for this template. |
Example of registering a popup in ZCML:
<zope:adapter
for="Products.Five.BrowserView"
factory=".view.default_popup_template"
name="popup"
provides="zope.formlib.namedtemplate.INamedTemplate" />
RichWidget
Allows the input of text, or upload of a file, in multiple formats that are then transformed as necessary for display. For example, allows you to type some content, choose formatting and/or upload a file. If available, the visual editor set in personal preferences is used for editing and formatting.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| rows | integer | 5 | Number of rows. (Since the visual mode of the RichWidget is controlled by JavaScript, this is not very useful.) |
| cols | integer | 40 | Number of columns. (Since the visual mode of the RichWidget is controlled by JavaScript, this is not very useful.) |
| allow_file_upload | boolean | True | If True, a file upload option is included with the field. |
SelectionWidget
Renders an HTML selection widget, which can be represented as a dropdown, or as a group of radio buttons.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| format | string | 'flex' | Possible values: 'flex', 'select', 'radio'. Uses radio buttons when set to radio, and a single-selection list when set to select. Using flex will automatically use single-selection lists for more than three settings at a time, and a single-select list for up to three settings. |
StringWidget
Renders an HTML text input box which accepts a single line of text. For simple text lines such as author.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| maxlength | integer | 255 | Maximum input length in characters; sets the HTML input tag's maxlength attribute. |
| size | 30 | Size of the input widget; sets the HTML input tag's size attribute. |
TextAreaWidget
Renders an HTML text area for typing a few lines of text. Also provides for the entry of the content in multiple formats when allowed_content_types in the enclosing TextField allows it.
Special Properties
| Name | Type | Default | Description |
|---|---|---|---|
| rows | integer | 5 | Number of rows for the edit widget; sets the HTML textarea tag's rows attribute. |
| cols | integer | 40 | Column width of the edit widget; sets the HTML textarea tag's cols attribute. |
| append_only | boolean | False | Set this attribute to True to make an append-only TextArea widget. New text gets added to the top of the existing text, dividing the new text from the existing text using the divider property. The existing text is shown below the TextArea, and is not editable. This currently works with TextArea widgets and using plain text format. |
| divider | string | ======================== | Divider text marker to use for append only text areas. Only used then the append_only property is True. |
| maxlength | integer | False | If non-zero, sets a maximum input length in characters. Since the HTML textarea tag has no maxlength property, this is enforced via a JavaScript snippet. So, it is is not applicable when JavaScript is unavailable. |
Add-on Widgets
To find all available add-on widgets contributed by the community, follow this link.
2.3.3. Validator Reference
A quick reference to the built-in Archetypes validators.
Using Validators
Archetypes fields may have validators specified in the Field schema. For example, the schema for the basic page type includes the stanza:
ATDocumentSchema = ATContentTypeSchema.copy() + Schema((
TextField('text',
...
validators = ('isTidyHtmlWithCleanup',),
...
),
This specifies that the isTidyHtmlWithCleanup test will be applied to validate form input.
You may specify a sequence of validators:
validators = ('isMaxSize', 'isTidyHtmlWithCleanup',),
and the validators will tested in order.
The validators sequence may contain two kinds of entries:
- The string names of validators registered with the validation service (see Products.validation);
- Instances of classes implementing the IValidator interface (Products.validation.interfaces.IValidator.IValidator).
A validation specification using a validator class instance can look like:
validators = ( ExpressionValidator('python: int(value) == 5'), )
Registered Validators
These are validators pre-registered with the validation service. They may be specified by name.
| Name | Use | Details |
|---|---|---|
| isDecimal | Is the input a decimal number. | Allows exponent notation. |
| isInt | Is the input an integer. | |
| isPrintable | Does not contain unprintable characters | r'[a-zA-Z0-9\s]+$' |
| isSSN | Is a well-formed social-security number | Very naive: r'^\d{9}$' |
| isUSPhoneNumber | Is a valid US phone number | Looks for 10 digits, ignores spaces, parens and dashes |
| isInternationalPhoneNumber | Is a valid international phone number | Looks for any number of digits, ignores spaces, parens and dashes |
| isZipCode | Very naive: is five or nine digits | |
| isURL | Is a valid URL | Recognizes most protocols |
| isEmail | Is a valid e-mail address | A pretty good regular expression test |
| isMailTo | Is an e-mail address preceded by "mailto:" | |
| isUnixLikeName | Passes the basic test to be a Unix-style name | r"^[A-Za-z][\w\d\-\_]{0,7}$" |
| isMaxSize | Tests if an upload, file or something supporting len() is smaller than a given max size value. | Tests against a maxsize attribute on the field |
| isValidDate | Tests whether or not input value can be converted to a DateTime object. | |
| isEmpty | Input value must be empty. | |
| isEmptyNoError | Input value must be empty. | Validation will fail if input value is not empty; but no error will show. |
| isValidId | Input value is a valid identifier. | |
| isTidyHtml | Uses mx.Tidy to validate HTML input. Fails on errors and warnings. | |
| isTidyHtmlWithCleanup | Uses mx.Tidy to validate HTML input. Fails only on errors; cleans up. | |
| isNonEmptyFile | The uploaded file is not empty. | |
| isTAL | Validates as Template Attribute Language |
Useful Validation Classes
These classes are useful for creating your own validation class instances. Imports and prototypes are shown. See source for details.
ExpressionValidator
Evaluates an expression to test the input value.
from Products.validation.validators.ExpressionValidator import ExpressionValidator def ExpressionValidator(expression=None, errormsg=None)
RegexValidator
Tests value against a regular expression after removing ignorecharacters.
from Products.validation.validators.RegexValidator import RegexValidator def RegexValidator(name, regex, title=name, description='', errmsg='fails tests of %s' % name, ignore=None)
RangeValidator
Tests to see if specified minval <= input_value < maxval
from Products.validation.validators.RangeValidator import RangeValidator def RangeValidator(name, minval=0.0, maxval=0.0, title='', description='')
2.4. Other Useful Archetypes Features
Complementary features you'd be pleased to know about.
2.4.1. How to use events to hook the Archetypes creation process
Times have changed since the days of at_post_create_script(). Here is the way to hook into Zope3 (or Five's) event system in order to execute code during the Archetypes content creation and or editing process.
In the old days the only way to execute code during the object creation process for Archetypes was to add a method to your content type called at_post_create_script. In this script you would add any code that should execute after Archetypes was done creating the object.
The new method for hooking the Archetypes object creation and editing process is to use Zope3 style events, like Products.Archetypes.interfaces.IObjectInitializedEvent.
Prerequisites
Have a content type handy so we can add a post creation hook to it. To learn how to create a content type, check previous sections of this manual.
We're going to use a content type called ExampleContent with the interface IExampleContent for this how to. The code structure will look like this:
tutorial/configure.zcml tutorial/interfaces.py tutorial/content/examplecontent.py
Step by step
First let's create the interface for our ExampleContent type. In interfaces.py, add:
from zope.interfaces import Interface
class IExampleContent(Interface):
''' Interface for the ExampleContent type
'''
You can store the implementation for your event handlers anywhere but for the purpose of this example we're going to put it in the same module as the ExampleContent type:
from zope.interface import implements
from Products.ATContentTypes import atct
def addSubFolder(obj, event):
obj.invokeFactory(type_name='Folder', id='subfolder')
class ExampleContent(atct.ATFolder):
implements(IExampleContent)
portal_type = archetype_name = 'ExampleContent' # <-- this is no longer needed with GenericSetup.
All we need to do now is register the addSubFolder method as a handler for Products.Archetypes.interfaces.IObjectInitializedEvent and for anything implementing the IExampleContent interface. We do this in a configure.zcml file:
<subscriber for=".interfaces.IExampleContent
Products.Archetypes.interfaces.IObjectInitializedEvent"
handler=".content.examplecontent.addSubFolder" />
Notice that there are two interfaces in the "for" attribute. This is because we are registering a multi-adapter. Now when you add an ExampleContent type the addSubFolder method will be executed after Archetypes has created the object. The object itself will be passed to the handler and we can use the object reference to make additional modifications, in this case adding a sub folder.
You can register as many handlers as you need.
Warnings from your future
Having implemented all of your content type's event hooks you might then run off and try using invokeFactory somewhere in your code only to realize that your IObjectInitializedEvent handlers are not being executed. This is because invokeFactory does not notify Zope's event system that new objects are being created. You have to provide these notifications yourself. So here is an example:
import zope.event from Products.Archetypes.event import ObjectInitializedEvent some_folder.invokeFactory(type_name='ExampleContent', id='foobar') obj = getattr(some_folder, 'foobar') zope.event.notify(ObjectInitializedEvent(obj))
This will both create your object and invoke any IObjectInitializedEvent handlers you have registered. Notice that we are importing ObjectInitializedEvent, not the interface IObjectInitializedEvent. We want to actually instantiate an event passing it our newly created object as the single parameter and then pass the event to zope.event.notify. From there, Zope takes care of figuring out which handlers need to execute.
So for example, in our addSubFolder method above, any events registered for the folder we created would not fire. To complete our hook in this case we should provide a notification for our newly created folder. Archetypes or other products may be expecting notifications so when using invokeFactory, always send an IObjectInitializedEvent for the object. The complete method looks like this:
def addSubFolder(obj, event):
obj.invokeFactory(type_name='Folder', id='subfolder')
folder = getattr(obj, 'subfolder')
zope.event.notify(ObjectInitializedEvent(folder))
Further information
The IObjectInitializedEvent is fired once during the objects creation process. To hook the editing process for an object use IObjectEditedEvent.
The Sending and handling events tutorial is a little out of date but provides a broader explanation of the underlying mechanics. Walking through Five to Zope 3 - Events is another great introduction to events handling.
2.5. Appendix: Practicals
2.5.1. How-To Extend A Basic Archetype Content Type
This How-To describes what to do next after you've gotten the basic Archetype examples working. You can add functions, views, actions, and edit-page validation.
Ok, so you've got the Archetype examples to work, and now you'd like to know how to flesh out the basic example into something useful. (That's how I started; I knew I needed to make new Content Types, but didn't know how they worked. I got the examples going, then tried to figure out how to modify them to do what I wanted.) I found learning Plone/Zope very frustrating until I got to this point. Then, once I figured out how to make Content Types do what I wanted, it all made sense.
Adding FunctionsThis is probably one of the first things on your mind. Where do I add functions to my Content Type? How do I call them? What syntax do I use? Well, these were the things that I wondered about. I figured out how to do them after some trial and error. I never made a real website before, where I had to write scripts (a blog doesn't count). So even though I had a lot of python experience, I was confused at first. [First, know that the Zope server converts a URL path into an object path, to find the object that will render your page. This is covered in the Zope Developer's guide. The key point you need to know is that there's a parallel between the URL and your object hierarchy, but it's not exactly the same.]
To cut to the chase, Zope figures out which object/function the URL is pointing to, and it takes the query string (?arg=val,arg2=val2, etc.) and uses it to figure out all the right arguments for the function call. So, you define your function in the ususal way, i.e.,
class MyExample (BaseContent):
""" My example Archetype Content Type. """
# define the schema
# override the default actions
def my_function (self, foo1, foo2):
""" You need a doc string here!! I lost a lot of time finding this out.
Archetypes needs this when registering the function in the framework.
You'll get a 404 error if you forget the doc string. """
temp1 = "foo" # this is not persisted in the ZODB
self.this_is_a_persisted_member_in_the_ZODB = "I'm here to stay %s" % (foo1,)
# if you don't return anything, then the Zope server will not re-render a page
# anything you return will be rendered
# return "got here" will show up as text
# return context.index_html() will return the default page (should be reasonable in any content; people won't get lost)
# return context.base_edit() has the effect of "jumping" to the edit page
Adding Actions
I was confused when I saw the description of actions in the Plone manual. Here's how I think of them: they're just the hyper-linked tabs along the top of the Content Type (content actions) or horizontal site navagation (site actions). The links are typically to a Content Type function (that returns a page), or to a page template (I only know how to make .pt and .cpt types so far). The actions for the Content Type are defined (overriden) using the Factory Type Information format, and the process well described in the Archetype tutorials. I'll just add that you can make most of the tabs (actions) visible=True or visible=False. You can append your own actions that show up as tabs for your content type.
Changing Page ViewsYou can change the various views of your content type by defining new page templates to display your data. Typically, these page templates (.pt) are placed within the skins/ directory of your product. I can't fill you in on exactly how Zope maps the URL (http://.../myArchExample/my_view) to the my_view.pt, but the details are taken care of (by the Install.py script?) and you should put your page templates in the skins/ directory. [This section needs updating, as soon as I learn how it's done.]
Validating the Edit FormYou probably have a need to validate the data that users enter on the edit page. This process is called validation, and the scripts that implement the rules are called validators. There's a clean way to do this in Archetypes using built-in field validators and your own post_validation() function for the Content Type. You don't have to write any (.cpt,.cpy,.vpy) form templates, or controller scripts. Of course, validation is optional, so you can skip either step.
- Use field validators on individual entries (see the validator = (,) field attribute). This is the first-line of validation.
- Define a post_validation() function. This allows you to validate fields in the context of the entire class, and set error (re-do) flags for individual fields.
After the user hits the 'submit' button on the edit page, the field validators are run first. If any validators fail, the input field is highlighted, and the user is sent back to the edit page to fix the errors.
If all the field validators pass, then your post_validate(self, REQUEST, errors) is called. The form keys and data are passed to you in the REQUEST dictionary. Your code will validate the edit form values in the REQUEST dictionary. If you see errors that require fixing, you'll set them in the errors dictionary (using the corresponding key in the REQUEST). For example, here's a
class MyExample (BaseContent):
""" My example Archetype Content Type. """
# define the schema
# override the default actions
def post_validate (self, REQUEST, errors):
""" This function checks the edit form values in context.
It's called after the field validation passes. """
if REQUEST['type'] == 'buy' and REQUEST['quantity'] == 0:
error['quantity'] = "Quantity must be non-zero."
Adding Child Members
If your content type is a folder-like object, you can write functions that will add child objects. This may be useful, for example, if your Content Type is a ledger, and you need to add new transactions when the user hits an action. The following example code shows how to do this.
class MyExample (BaseFolder):
""" My example Archetype Content Type. """
# define the schema
# override the default actions
def addTransaction (self, type, quantity):
""" This function creates a new MyTransaction object in the folder. """
# create a unique id for this transaction
newId = self.generateUniqueId('MyTransaction')
# create a new MyTransaction object
self.invokeFactory(id=newId, type_name='MyTransaction')
myTransaction = getattr(self, newId)
return myTransaction.base_edit() # send the user to the edit page
2.5.2. Implement Archetypes ComputedField and ComputedWidget on your Product and reference other Fields
A simple use of ComputedField and ComputedWidget referencing other fields, built-in or 3rd party, in the same Plone product
Motivation
There are many reasons why this how-to exists:
- Almost no Archetypes examples using ComputedField and ComputedWidget
- We want our product to process some data on itself, but reload isn't a matter of concern
- We love PZP (Python-Zope-Plone)!
What do we need?
- A Plone installation
- A nice text editor (my wintel box runs SciTE)
- Some product (for real dummies like me, try http://plone.org/documentation/tutorial/anonymously-adding-custom-content-types-with-argouml-and-archgenxml/view)
What we are going to achieve?
- Make a page process it's own information
Let's
say you created a product, maybe using ArgoUML (an UML editor) and
ArchGenXML. One thing you might realize it's missing on creating UML's
is that: we create classes (Plone products), their types are also
classes (Archetypes' fields and widgets) and Attributes (Fields and
Widgets' properties) as TD's (tagged data) for Archetypes' types, but
we have no methods!
But we could do more if we inserted
code: classes are made of attributes and methods (code). But as UML
editors are not that Python friendly, we do that by hand.
So, how?
If
you already have navigated the path of a product, you've stumbled on
some source files (.py) inside, so take some time to read their source (Read the source, Luke!). Probably you've seen some like this (for example, MyOrder.py):
from AccessControl import ClassSecurityInfo
from Products.Archetypes.atapi import *
from Products.Laborde.config import *
from Products.DataGridField import DataGridField, DataGridWidget # we talk about this later
from Products.DataGridField.Column import Column #really!
schema = Schema((
StringField(
name='PurchaseOrderID',
widget=StringWidget(
label="PurchaseOrderID",
description="Enter this purchase order unique identification number.",
label_msgid='MyOrder_label_PurchaseOrderID',
description_msgid='MyOrder_help_PurchaseOrderID',
i18n_domain='MyOrder',
),
required=True,
searchable= True
),
DataGridField(
name='PurchaseOrderItems',
required=True,
searchable=True,
widget=DataGridWidget(),
allow_empty_rows = False,
columns=(
"Maker",
"Model",
"Description",
"UnitaryCost",
"Quantity"
),
),
ComputedField(
name='TotalCost',
searchable=True,
expression="context.calculateTotal()",
widget=ComputedWidget(
label="Total",
modes=('view')
),
),
),
)
PurchaseOrder_schema = BaseSchema.copy() + \
schema.copy()
class PurchaseOrder(BaseContent):
"""
"""
# some class defitnitions
# a function that calculates total
# but it doesn't even check (try-except) data it uses
def calculateTotal(self):
Total = 0.0
for n in self.PurchaseOrderItems:
Quantity = float(n['Quantity'])
UnitaryCost = float(n['UnitaryCost'])
Total = Total + Quantity * UnitaryCost
Total = '%1.2f' % Total # this makes our total have 2 decimals for display
return Total
registerType(PurchaseOrder, PROJECTNAME)
Aargh! I've just core dumped and almost killed 30!
The
above code can be divided in two parts: Schema and Class (Product). We
have declared 3 different fields in the schema: the first is a bultin
trivial Archetype field; the second is imported from the Product
DataGridField (you need it installled on your Plone instance to work);
the third is our the field we want to change as someone changes values
on the form.
expression="dir()" # useful to check avaible objects
,
expression="1+1" # 10 if you have two neurons, like me. Otherwise, 2.
,
expression="dir(context)" # avaible context child objects
or
expression="context.calculateTotal()" # Voilá! Reference to some real(?) code!
We've just called something (a function, in fact) named calculateTotal.
But
smart as we are, we realized that expresssions called this way must be
somewhere in our context scope. I mean, inside our class definition.
The
function definition itself isn't that simple: it adds up the total and
returns its value. What isn't simple? Our generous DataGridField
returns a tuple of dictionaries like:
(
{"Maker":"HP","Model":"scanjet 4670","Description":"scanner","UnitaryCost":"99.00","Quantity":"1"},
{"Maker":"LG","Model":"L173SA","Description":"17 LCD monitor","UnitaryCost":"299.95","Quantity":"2"},
{"Maker":"Seagate","Model":"SA32300","Description":"Hard drive","UnitaryCost":"134.50","Quantity":"2"}
)
The for
loop iterates over every item on the tuple and searches for two
dictionary items. Other field are rather simple to retrieve data: just
use field's name attribute.
The widget=SomeWidget(modes='view',...) realizes
the feat of showing this field only on the view mode: not when adding
the item and editting, nor when editting an existing item.
What's next?
- What could we do with PhotoField (ImageWidget)?
- try-except is always recomended
- Could this better than mutate?
- Can we make a file avaible for download with some strange mime type based on the information of this product?
2.5.3. Making the view page of a content type use your schemata declarations
How to make the schemata declarations in a Archetypes schema be used in the view page of a content type.
2.5.4. Enabling versioning on your custom content-types
Plone 3 includes a robust versioning system as well as a tool for viewing diffs, which allows you to easily see the changes between two revisions. This document explains how to integrate versioning and diff functionality with your custom Archetypes-based content-types.
2.5.5. b-org: Creating content types the Plone 2.5 way
Plone 2.5 brings us closer to the promised land of Zope 3. Zope 3 brings us a new way of working. This tutorial will show how to marry the old and the new, to make Plone products that are more extensible, better tested and easier to maintain.
2.5.5.1. Introduction
What is b-org, and what will you learn here?
b-org stands for "base-organisation". The name had nothing whatsoever to do with my desire to get an svn URL of http://svn.plone.org/svn/collective/borg. Promise. In fact, it used to be called company, which some people rightly pointed out is a bit too generic and opens up the possibility of conflicts with other people's code. It just proves that naming generic components is difficult.
Generic is the key word here. Functionally, b-org provides infrastructure to help you manage Departments, Employees and Projects in a natural way. Departments are containers for employees, employees are linked to projects by references. Using membrane, these objects become sources for users and groups, so that a department is a group for all the employees in it, and employees become real users of the system, with usernames and passwords. Projects manage local roles, so that employees that have been associated with the project are able to add and modify content in it. Other users may or may not be able to view content in a project, depending on its workflow state.
However, b-org makes no assumptions about which metadata you want to associate with departments, employees or projects. For that, it expects you to plug in your own content schema. It also delegates almost all its functionality to smaller components, so that if you, for example, want to store authentication details via LDAP or change the way in which users are employees to projects, you can do so by implementing small, isolated components rather than sub-classing and re-implementing large chunks of the three basic content types.
That's all well and good, but you're probably not going to want to read a lengthy tutorial just about how great b-org is. As the title promises, this tutorial is about leveraging new technologies available in Plone 2.5 to write better content types and other software in Plone. Hopefully, you will find the techniques described here useful whether you are writing a member management module using membrane (mmmm), or other code. I for one, want to go and rewrite several of my products (like Poi) to make them more extensible and flexible after having adopted these techniques. Hopefully, you will also learn something about the development process, in particular test-driven development, that I followed, and how the future of Plone is entangled in Zope 3.
This tutorial should be viewed as complementary to, rather than superceding, my earlier tutorial entitled RichDocument - Creating Content Types the Plone 2.1 way. The techniques of RichDocument, in particular relating to extending ATContentTypes, are still valid in Plone 2.5. What Plone 2.5 allows us to do, however, is to achieve better separation of concerns between content storage, business logic and view logic, due to the added spices of Zope 3. For RichDocument, the gain wouldn't be that great since it's relatively simple (and focuses on doing as little as possible by re-using as much as possible from ATContentTypes). Hence, I didn't update the RichDocument tutorial, nor do I feel as compelled to update RichDocument itself (yet). b-org is a more ambitious example which allows us to illustrate the new techniques more fully.
One thing to note is that this tutorial is still centered on Archetypes, and assumes you know the basics of Archetypes development on the filesystem. Archetypes is rooted in a pre-Zope 3 world, and there are times when we have to accommodate it in ways that make our clean patterns a bit messier - luckily, not too often. There are ways of managing content in Zope 3 that can be applied to Plone, for example by way of zope.formlib, but these are generally not quite ready to replace what we can do today with Archetypes. In the future, they may be, but more likely Archetypes will converge a bit more with its Zope 3 equivalents and blur the lines between the two approaches. The upshot is that what you know about Archetypes today continues to be relevant, and is augmented by the Zope 3-inspired techniques you will find here.
2.5.5.2. A whirlwind tour of Zope 3
Zope 3 is still fairly new. After reading this tutorial, it should hopefully start to feel a bit more familiar. In this section, we will give a brief overview of what is different in Zope 3 and how it fits into Plone.
The name Zope 3 is a lie. True - it is brought to you by many of the same clever people who built Zope 2, one of the most advanced open source app servers of its day. True, it is still Python, it still publishes things over the web, and there are still Zope Page Templates. However, Zope 3 is about small, re-usable components orchestrated into a flexible framework. It is this flexibility that allows us to use Zope 3 technologies in Zope 2 applications like Plone.
A piece of wizardry called Five (Zope 2 + Zope 3 = Five, geddit?) makes a number of Zope 3 components directly available in Zope 2, and since Zope 2.8, almost all of Zope 3 has shipped with Zope 2 as a python library. Plone 2.5's primary purpose was to lay the foundations for taking advantage of Zope 3 technologies in Plone.
Zope 3 may seem a bit alien at first, because it uses strange concepts such as adapters and utilities. Luckily, these are not so difficult to understand, and once you do, you will find that they help you focus your development on smaller and more manageable components. You will also find that these basic concepts underpin most of the innovative parts of Zope 3.
Interfaces
Everything in Zope 3 starts with interfaces. Unlike Java or C#, say, Python does not have a native type for an interface, so an interface in Zope 3 is basically a class that contains only empty methods and attributes, and inherits from Interface. Here is a basic example:
from zope.interface import Interface, Attribute
class IShoe(Interface):
"""A shoe
"""
color = Attribute("Color of the shoe")
size = Attribute("Shoe size")
class IShoeWearing(Interface):
"""An object that may wear shoes
"""
def wear(left, right):
"""Wear the given pair of shoes
"""
Interfaces are primarily documentation - everything has docstrings. Also note that the wear() method lacks a body (there is not even a pass statement - the docstring is enough to keep the syntax valid), and does not take a self parameter. That is because you will never instantiate or use an interface directly, only use it to specify the behaviour of an object.
An object can be associated with an interface in a few different ways. The most common way is via its class. We say that the class implements an interface, and objects of that class provide that interface:
from zope.interface import implements
class Shoe(object):
"""A regular shoe
"""
implements(IShoe)
color = u''
size = 0
The implements(IShoe) line means that objects of this class will provide IShoe. Further, we fulfill the interface by setting the two attributes (we could have implemented them as properties or used a an __init__() method as well). The IShoeWearing interface will be implemented in the section on adapters below.
We use interfaces to model components. Interfaces are normally the first stage of design, in that you should define clear interfaces and write actual classes to fulfill those interfaces. This formalism makes for great documentation - interfaces are conventionally found in an interfaces module, and this is typically the first place you look after browsing a package's documentation. It also underpins the adapter and utility system - otherwise known as the Component Architecture - as described below.
Note that you can use common OOP techniques in designing interfaces. If one interface describes a component that has an "is-a" or "has-a" relationship to another component, you can let interfaces subclass or reference each other. An object will provide the interfaces of its class, and all its base-classes, and all base-interfaces of those interfaces. Don't worry about untangling that - it works the way you would expect.
You can also apply interfaces directly to an object. Of course, if that interface has methods and attributes, they must be provided by the object, and unless you resort to crazy dynamic programming, the object will get those from its class, which means that you may as well have applied the interface to the class. However, some interfaces don't have methods or attributes, but are used as markers to distinguish a particular feature of an object. Such marker interfaces may be used as follows:
class IDamaged(Interface):
"""A shoe that is damaged
"""
>>> from zope.interface import alsoProvides >>> boot = Shoe() >>> IDamaged.providedBy(boot) False >>> alsoProvides(boot, IDamaged) >>> IDamaged.providedBy(boot) True
Marker interfaces are very useful for things that change at run-time in response to some event (e.g. some user action), and thus cannot be determined in advance. In a moment, you see that what you will learn about adapters and adapter factories below also applies to marker interfaces - it is possible to alter which adapter factory is invoked by applying a different marker interface.
It's also possible to apply interfaces directly to classes (that is the class itself provides the interface, as opposed to the more usual case where the class implements the interface so that objects of that class provides it - this is useful because it allows you to group those classes together and describe the type of class they are) and to modules (where you want to describe the public methods and variables of a module). These constructs are less common, so don't worry about them for now. Look at the documentation and interfaces (!) in the zope.interface package for more.
Adapters
The most important thing that Zope 3 promises is separation of concerns. In Zope 2, almost everything has a base class that pulls in a number of mix-in classes, such as SimpleItem (surely, the most ironically named class in Zope 2) and its plethora of base classes that include RoleManager, Acquisition.Implicit and many others. This means that a class written for Zope 2 is nearly impossible to re-use outside of Zope.
Furthermore, in Zope 2 we are tightly wedded to the context (aka here) because it is so convenient to use in page templates, workflow scripts etc. For example, people often write an Archetypes class that contains a schema (storage logic), methods for providing various operations (business logic) and methods for preparing things to display in a page template (view logic). Often, people do this simply because they can't think of a better place to put things, but it does mean that re-using any part of the functionality becomes impossible without importing the whole class - and its base classes, which include Archetypes' BaseObject, CMF's DynamicType, and Zope's SimpleItem - to name a few!
Think about the example above. The Shoe class is well-contained and only concerned with one thing - storing the attributes of shoes. It can be used as an abstraction of shoe anywhere, and is very lightweight. Now let's consider that we may want to wear shoes as well. We can create a pair of shoes easily enough:
>>> left = Shoe() >>> right = Shoe() >>> left.size = right.size = 10 >>> left.color = right.color = u"brown"
Now we want someone to wear these shoes. Let's say we have a person:
class IPerson(Interface):
"""A person
"""
name = Attribute("The person's name")
apparel = Attribute("A list of things this person is wearing")
class Person(object):
implements(IPerson)
name = u''
apparel = ()
In a Zope 2 world, we may have required Person to mix in some ShoeWearingMixin class that specified exactly how shoes should be worn. That makes for fat interfaces that are difficult to understand. In a Zope 3 world, we would more likely use an adapter.
An adapter is a glue component that can adapt an object providing one interface (or a particular combination of interfaces, in the case of a multi-adapter) to another interface. We already have a specification for something that wears shoes, in the form of IShoeWearing. Here is a snippet of code that may use this interface:
>>> wearing = ... >>> wearing.wear(left, right)
The question is what to do with the '...' - how do we obtain an object that provides IShoeWearing? Code like this is normally operating on some context, which in this case may be a Person. If that Person implemented IShoeWearing (or at least the wear() method), it would work, but then we are making undue demands on Person. What we need is a way to adapt this IPerson to something that is IShoeWearing. To do that, we need to write an adapter:
from zope.interface import implements
from zope.component import adapts
class PersonWearingShoes(object):
"""Adapter allowing a person to wear shoes
"""
implements(IShoeWearing)
adapts(IPerson)
def __init__(self, context):
self.context = context
def wear(self, left, right):
self.context.apparel += (left, right)
Here, we implement the IShoeWearing interface. Note how the wear() method now has a self parameter, since this is a real object. Also note the __init__() method, which takes a parameter conventionally called context. This is the thing that is being adapted, in this case an object providing IPerson. We store this as an instance variable and then reference it later. Note that adapters are almost always transient objects that are created on the fly (we will see how in a second).
We could now do something like this:
>>> wearing = PersonWearingShoes(person) >>> wearing.wear(left, right)
However, this still requires that we know exactly which adapter to invoke for the particular object (person in this case), effectively creating a tight coupling between the adapter, the thing being adapted, and the code using the adapter.
Luckily, the Zope 3 Component Architecture knows how to find the right adapter if you only tell it about the available adapters. We do that using ZCML, the Zope Configuration Markup Language. This is an XML dialect that is used to configure many aspects of Zope 3 code, such as permissions and component registration. You can do what ZCML does in Python code as well, but typically it's more convenient to use ZCML because it allows you to separate your logic from your configuration.
ZCML directives are stored in file called configure.zcml, which itself may include other files. A configure.zcml file in your product directory (Products/myproduct/configure.zcml) will be picked up automatically by Five. Here is a snippet that will register the above adapter:
<adapter factory=".shoes.PersonWearingShoes" />
You will sometimes see a fuller form of this directive, like:
<adapter
factory=".shoes.PersonWearingShoes"
for=".interfaces.IPerson"
provides=".interfaces.IShoeWearing"
/>
Here, we are specifying full dotted names to interfaces in the for or provides attributes. These are equivalent to the adapts() and implements() calls we used when defining the adapter. Note that adapts() did not work prior to Zope 2.9 (so the ZCML for attribute is mandatory), and that if your adapter class for some reason implements more than one interface (e.g. because it's inheriting another adapter that has its own implements() call), you may need to specify provides to let Zope 3 know which interface you're really adapting to.
Notice here that the dotted names begin with dot. This means "relative to the current package". You can write "..foo.bar" to reference the parent package as well. You could specify an absolute path instead, e.g. Products.Archetypes.interfaces.IBaseObject or zope.app.annotation.interfaces.IAttributeAnnotatable. Typically, you use the full dotted name for things in other packages and the relative name for things in your own package.
The factory attribute normally references a class. In Python, a class is just a callable (taking the parameters specified in its __init__() method) that returns an instance of itself. You can reference another callable as well if you need to, such as a function that takes the same parameters (only context in this case - obviously there is no self for functions), finds or constructs and object (which must provide IShoeWearing) and then returns it. This is rarely used, but can be very powerful (for example, it could find an object providing the given interface in the adapted object's annotations - but don't worry if you don't understand that for now).
With this wiring in place, we can now find an adapter for an IPerson to IShoeWearing. The Component Architecture will ensure that we find the correct adapter:
>>> wearing = IShoeWearing(person) >>> wearing.wear(left, right) >>> person.apparel == (left, right,) True
We are "calling" the interface, which is a convenience syntax for an adapter lookup. If an adapter cold not be found, you will get a ComponentLookupError. There are plenty of functions in zope.component to discover adapters and other components - see zope.component.interfaces for the full story.
It is important to realise that the adapter lookup is essentially a search. The Component Architecture will look at the interfaces provided by person and look for a suitable adapter to IShoeWearing. As mentioned before, it's possible for an object to provide many interfaces, e.g. inherited from its base classes, implemented explicitly by the object (by declaring implements(IFoo, IBar)), via ZCML or because an object directly provides an interface. It is therefore possible that there are multiple adapters that could be applicable. In this case, Zope 3 will use the interface resolution order (IRO) to find the most specific adapter. The IRO is much like you would expect of polymorphism in traditional OOP:
- an interface directly provided by the object is more specific than one provided by its class
- an object provided by an object's class is more specific than that provided by a base class
- if an object has multiple base classes, interfaces are inherited in the same order as methods are inherited
- if a class implements multiple interfaces, the first one specified is more specific than the second one, and so on
Remember marker interfaces? One use of marker interfaces is to imply a particular adapter. Think about the case where you may have specific adapter to IShoeWearing for some marker interface IAmputee. If you mark a person as an IAmputee due to some unforunate accident, the IShoeWearing adapter may raise a warning rather than modify the apparel list.
All of this may seem a little roundabout and unfamiliar, but you'll get to grips with it soon enough. Let's re-cap how we arrived at this:
- We modelled our application domain with some interfaces - IPerson, IShoe
- We modelled an aspect of a person (or other object) for wearing shoes - IShoeWearing
- We wrote some simple classes that implemented the domain interfaces IPerson and IShoe
- We wrote and registered a simple adapter that could adapt an IPerson to IShoeWearing
Then we showed how this could be used by some hypothetical client code. The upshot is that the client code only needed to know about IPerson and IShoeWearing, not how the aspect of a person that involves wearing shoes is implemented. The Component Architecture will ensure that the appropriate adapter is found, regardless of whether the person is a vanilla IPerson, a sub-class with a more specific sub-interface, or an instance with a marker interface applied.
Multi-adapters, named adapters and views
In the example above, we used an adapter with a single context. That is the most common form of adapter, but sometimes there is more than one object that forms the context of an adapter. As a rule of thumb, if you find yourself passing a particular parameter into every method of an adapter, it should probably be a multi-adapter.
The most common example of a multi-adapter that you will come across is that of a view, which incidentally is also how Zope 3 solves the "where do I put my view logic" code. We will cover views in detail later, but for now think of them as a python class that is automatically instantiated and bound to a page template when it's rendered. In the template, the variable view refers to the view instance and can be used in TAL expressions to gain things to render or loop on.
When dealing with a view, there are two things that make up its context - the context content object (conventionally called context) and the current request (conventionally called request). Thus, a view class is a multi-adapter from the tuple (context, request) to IBrowserView. As it happens, there are ZCML directives called browser:page and browser:view that make it easier to register a view and bind a page template to it, handle security etc. However, abstractly a view looks like this:
class PersonView(object):
implements(IBrowserView)
adapts(IPerson, IHttpRequest)
def __init__(self, context, request):
self.context = context
self.request = request
def name(self):
return self.context.name
def requested_shoes(self):
return self.request.get('requested_shoes', [])
Notice how this adapts both IPerson and IHttpRequest, and thus takes two parameters in its __init__() method. As you will learn later, views typically inherit the BrowserView base class for convenience, but the principle is the same.
To obtain a multi-adapter, you can't use the "calling an interface" syntax that you use for a regular adapter. Instead, you must use the getMultiAdapter() method:
>>> from zope.component import getMultiAdapter
...
>>> personView = getMultiAdapter((person, request,), IBrowserView)
You could use queryMultiAdapter() instead if you wanted it to return None instead of raise a ComponentLookupError when it fails to find the adapter.
The above code has a problem, however (apart from being an incomplete example) - what if you have more than one view on the same object, say for two different tabs? To resolve this ambiguity, views are actually named multi-adapters. The names correspond to the names used as part a URL, and are registered using the name attribute in ZCML. This is used in browser:page and browser:view directives, but can also be used in the standard adapter directive:
<adapter factory=".sampleviews.PersonView" name="index.html" />
To get this particular view, we can write:
>>> personView = getMultiAdapter((person, request,), name=u'index.html')
conventionally, we leave off the required interface when we used named adapters, although you can supply it if necessary.
Multi-adapters are useful for other things as well. If you have an adapter and find that every method takes at a common parameter, it's a good candidate for a multi-adapter. Also observe that in the case above, we could register a different adapter for a different type of request as well as for a different type of object. Again, the Component Arhictecture will find the most specific one looking at both interfaces.
Named adapters do not have to be multi-adapters, of course. They are typically used in cases where something (e.g. the user) is making a selection from a set of possible choices (such as choosing the particular view among many possible views).
Utilities
In the CMF, we have tools, which are essentially singletons. They contain various methods and attributes and may be found using the ubiquitous getToolByName() function. The main problem with tools is that they live in content space, as objects in the ZODB, and require a lot of Zope 2 specific things.
Let's say we had a shoe locating service (very useful when you can't find your shoes):
class IShoeLocator(Interface):
"""A service for finding your shoes
"""
def findShoes(owner):
"""Find all shoes for the given owner.
"""
class DefaultShoeLocator(object):
implements(IShoeLocator)
def findShoes(self, owner):
return ...
The Component Architecture contains a very flexible utility registry, which lets you look up things by interface and possibly by name. Unlike adapters, utilities do not have context, and they are instantiated only once, when Zope starts up. Global utilities are not persistent (but local utilities are - see below).
As with adapters, we register utilities with ZCML:
<utility factory=".locator.DefaultShoeLocator" />
Alternatively, you could skip the implements() call on the factory and set it in ZCML. This may also be necessary in order to disambiguate if you have more than one interface being provided by the utility component:
<utility
factory=".locator.DefaultShoeLocator"
provides=".interfaces.IShoeLocator
/>
Now you can find the utility using getUtility():
>>> from zope.component import getUtility
>>> locator = getUtility(IShoeLocator)
>>> locator.findShoes(u"optilude")
...
The utility registry turns out to be a very useful generic registry, because like the adapter registry, it can manage named utilities. Let's say that you had a few different shoes you wanted to keep around:
>>> left = Shoe()
>>> right = Shoe()
...
>>> from zope.component import provideUtility
>>> provideUtility(left, name=u'left-shoe')
>>> provideUtility(right, name=u'right-shoe')
We can now find these utilities again using the name argument to getUtility().
>>> to_put_on = getUtility(IShoe, name=u'left-shoe')
Of course, we are still using the transient global utility registry, so these will diseappear when Zope is restarted. We could use local components instead (see below), or we could register them using ZCML. If we had defined the shoes left and right in a module shoes.py, we could write:
<utility
component=".shoes.left"
name="left"
/>
<utility
component=".shoes.right"
name="right"
/>
An alternative would have been to define two classes LeftShoe and RightShoe and use the factory attribute of the directive instead of component (which refers to an instance, rather than a class/factory).
Local components
The examples above all use global, transient registries that are reloaded each time Zope is restarted. That is certainly what you want for code and functionality. Sometimes, you would like for utilities to be a bit more like their CMF cousins and also manage persistent state. To achieve that you need to use local components, which are stored in the ZODB.
Prior to Zope 3.3, which is included in Zope 2.10, local components were a bit of a black art. Then came the jim-adapter branch and everything was greatly simplified. The theory is still the same, the API is just much more sane. Each time Zope executes a request (or if you implicitly invoke zope.component.setSite(), for example in a test), it discovers which is the nearest site to the context. In Plone, the site is normally the root of the Plone instance, but in theory any folder could be turned into a site.
A site has a local component registry, where local utilities and adapters may be defined. This means that a particular utility or adapter can be specific to a particular Plone site, not affecting other Plone instances in the same Zope instance. You cannot use ZCML to register local components, since ZCML is inherently global (at least for now) - it does not know anything about your particular sites. However, you can register them with Python code, e.g. in an Install.py or a GenericSetup profile, using calls like provideUtility() (and its equivalent, provideAdapter()) called on a local site manager instance:
>>> from zope.component import getSiteManager >>> getUtility(IShoe, name=u'left-shoe) is left True >>> sm = getSiteManager(context) >>> sm.provideUtility(myShoe, name=u'left-shoe') >>> getUtility(IShoe, name=u'left-shoe) is myShoe True
Unfortunately, Plone 2.5 does not run on Zope 2.10. We won't cover local components here, because, well, I never learnt how to do it the Zope 2.9 way, and what I saw of it scared me. I'm told it's not that bad, and there is documentation in Five and in Zope 3 itself. Local components will become more important in Plone 3.0, where Zope 2.10 or later will be required and more things that use local components will be part of the core.
b-org does not use local components yet, and we will see how the extension mechanism would benefit from local components so that you could have one b-org extension installed in one Plone instance and another extension installed in another Plone instance, without the two interfering. Luckily, to code that uses adapters and utilities, it is completely transparent whether they are global or local.
Conclusion
That's it! If you can master the concepts of interfaces, adapters and utilities you will go far in a Zope 3 world. They will become much more natural as you use them a few times, and you'll probably wonder how you ever managed without them. Hopefully, that point will come before the end of this tutorial, which is largely focused on showing how the principle of separation of concerns can be imposed upon your Archetypes and Plone code.
2.5.5.3. Overview of b-org
The big picture
To the user, b-org presents itself as three content types:
- Department
- A container for employees, and a source of groups. That is, each department becomes a group, and the employees within that department become group members.
- Employee
- Information about employees, and a source of users. That is, each active employee object becomes a user who can log in and interact with the portal.
- Project
- A project workspace - a folder where employees can collaborate on content. Content inside the project folder has a custom workflow, and employees who are related to the project (by reference) have elevated permissions over this content.
This seemingly innocuous orchestration of functionality is achieved by a variety of means:
- Archetypes
- Used to build the actual content types and their schemata.
- The Zope 3 Component Architecture
- Is used to make all this exensibility possible - you will see lots of examples of interfaces, adapters and utilities.
- Membrane
- The content types are registered with membrane to be able to act as groups and users
- PAS and PlonePAS
- The Pluggable Authentication Service is used by membrane to actually provide user sources. A custom PAS plug-in is also used to manage local roles for members and managers within projects and departments.
- GenericSetup
- The next-generation set-up and installation framework is used to install and configure b-org. charity demonstrates how GenericSetup XML profiles can be used directly, without depending on the actual GenericSetup import mechanism.
- Zope 3 events
- Zope 3's event dispatch mechanism is used to ensure employee users actually own their own Employee objects, among other things.
- Zope 3 views
- The charity demo uses views for its display templates.
- Annotations
- Employees' passwords are hashed and stored in an annotation
- Placeful workflow
- To let content inside projects have a different workflow to that of the rest of the site, each project uses a CMFPlacefulWorkflow policy.
On the following pages, you will learn about each of these components and how it fits together. Meanwhile, you can follow along the code by looking in the subversion repository, or getting b-org from its product page.
2.5.5.4. To Archetype or not to Archetype
Archetypes is still the most complete framework for building content types quickly. With the advent of Zope 3, there is an alternative in Zope 3 schemas. Here's why b-org doesn't use them.
There is a growing consensus that Archetypes has grown a little too organically. On the one hand, Archetypes has given us a lot of flexibility, and made many of us more productive than we would ever have thought possible (for those who remember the heady days of plain Zope 2, and then plain CMF development). On the other hand, Archetypes has become fairly monolithic. The reference engine, for example, is woven tightly into the field type machinery, and the way that views are composed from widgets makes these almost impossible to re-use outside of Archetypes.
In practical terms, the biggest headache that arises from Archetypes' evolution is the very same problem we identified when introducing Zope 3 concepts - it's hard to re-use Archetypes-based components without sub-classing and repeating a large portion of a type's configuration. Take the Poi issue tracker, for example - I frequently get requests from people who want to add a few use-case specific fields to each issue, or add some new functionality such as having private issues or issues submitted on behalf of someone else. The problem is that I don't want to put all this functionality in Poi itself, because this would increase the complexity of the product and thus the maintenance burden and probably impact the intuitiveness of the UI, when in reality not everyone would benefit from such new features.
Ideally, someone would be able to plug in their own schema fields and add some logic in well-defined places without having to re-invent all of Poi. However, this is difficult, because, for example, the "add issue" button assumes you are adding a PoiIssue object, which has a schema defined wholly in Products/Poi/content/PoiIssue.py. There are custom form controller scripts to handle saving of issues, and a lot of methods are found in the various content classes to do things like send mail notifications or perform issue searches for various lists. Again, changing the logic of who gets an email notification or how a particular list of open issues is calculated may involve subclassing one or all of Poi's content types, re-registering view templates and other content type information, and possibly customise a number of templates and scripts to reference the new subclassed types. Of course, when Poi itself changes, keeping these customisations up-to-date becomes difficult.
Zope 3 has, in keeping with its philosophy, approached these problems by promising separation of concerns. In Zope 3, you would typically define an interface that specifies the schema of a content type, and then create a class that is only concerned with holding and persisting the data for this schema:
from zope.interface import Interface
from zope import schema
class IIssue(Interface):
"""A tracker issue
"""
title = schema.TextLine(title=u"The short title of this issue", required=True)
severity = schema.Int(title=u"The severity of this issue", required=True, default=3)
...
from persistent import Persistent
from zope.interface import implements
class Issue(Persistent):
implementS(IIssue)
title = u""
severity = 0
The actual functionality for sending notifications etc would be in various adapters (e.g to INotifying), the view logic in views. Forms can be created from schema interfaces like IIssue above, using zope.formlib. This can handle proper add forms (so the object is not created until the form has been filled in, which is another headache with CMF content types and therefore also Archetypes), validation, edit forms etc. Each form, adapter and menu entry (for the "add" menu, say) is registered separately, meaning that they can also be overridden and customised separately. Rocky Burt has written an excellent tutorial on how to use formlib in a Plone context that may be enlightening.
There are voices that say we should dump Archetypes entirely in favour of Zope 3-style content objects. Other voices (including my own) say that this may be a bit premature. Certainly, Zope 3 schemas and content objects are not yet fully integrated into CMF and Plone, so you end up depending on some CMF base classes at the very least. Moreover, the number and richness of widgets available for Zope 3 forms does not yet match that of Archetypes. Fundamentally, Archetypes has been around for a long time and has grown to meet a wide variety of use cases, whereas in the context of Plone at least, Zope 3 schemas are a new kid on the block.
The point is - Archetypes is not going to go away, not for a long time anyway, and are still the right choice for many types of applications. Almost all of Plone's add-on products use Archetypes, and it is well-understood by our developer community. The more likely scenario is that Archetypes will evolve in the same way that Zope 2 is evolving, by seeing its internals refactored piecemeal and pragmatically to take advantage of Zope 3 equivalents and concepts, until theoretically an Archetypes schema and content object is just a different spelling for what Zope 3 is doing, and Zope 3's content type story offers the same richness as Archetypes does (and more).
In the meantime, Archetypes is the right choice for b-org (and for other membrane-based systems). What we will try to do, however, is to alleviate the aforementioned problems by making use of Zope 3 design techniques, in order to make b-org extensible and flexible.
2.5.5.5. The extension story
One of the main drivers behind the componentisation of b-org is that it should be easy to extend and customise for third party developers. We'll take a look at how such customisations may look, before considering how we made it possible.
b-org ships with an example called charity, found in the examples/charity directory, which demonstrates one use-case specific implementation of b-org. This is quite simple, consisting of the following top-level files and directories:
- configure.zcml
- Registers the schema extension adapters (see below) and references the browser package
- Extensions/
- Contains an Install.py script that configures the Factory Type Information for the Department, Employee and Project content types. It does so by using GenericSetup XML files, but invokes the import handlers explicitly rather than through a GenericSetup profile.
- Â browser/
- Contains Zope 3 views for the charity department, employee and project content types, and a configure.zcml to register these. More on views in a later section.
- schema/
- Contains adapters that extend the schemas for Departments, Employees and Projects with use-case specific fields.
To use charity you should copy or symlink it from Products/borg/examples/charity to Products/charity. It can be installed as normal, but you must install b-org first. See borg/README.txt for the full install instructions!
A key aim is to make it possible to meaningfully extend b-org without needing to subclass all its types. Of course, you can do that, but in most cases it's not necessary. Unfortunately, the mechanisms and techniques described here will be "global" in nature. That is, you will not be able to have two different modes of customisation for two different Plone instances in the same Zope instance. This is because prior to Zope 2.10 (which Plone 2.5 does not support - it wasn't out until several months after Plone 2.5 was released), the "local" components story in Zope 3 was not fully developed. There is also a specific problem with the way the schema extension mechanism works which makes it inherently global.
When Plone 3.0 rolls around, it will support local components much better, and Archetypes 1.5, in conjunction with a third-party product called ContentFlavors (or possibly another similar tool), will enable the kind of extension story described here to work on almost any type. At that point, the forerunner you see in b-org now will be obsolete.
Of course, if you don't need two different b-org customisations for two different Plone sites in the same Zope instance (which I suspect most people can work around - having two separate Zope instances of course isolates you from all of this), you should be fine.
The schemas extenders
If you look at charity/configure.zcml you will see the following registrations:
 <adapter factory=".schema.department.DepartmentSchemaExtender" />
 <adapter factory=".schema.employee.EmployeeSchemaExtender" />
 <adapter factory=".schema.project.ProjectSchemaExtender" />
These schema extenders are adapters that hook into a specific part of b-org. We will describe this in more detail later, but here is how they look from the point of view of the extending product:
from zope.interface import implements
from zope.component import adapts
from Products.Archetypes.atapi import *
from Products.borg.interfaces import IEmployeeContent
from Products.borg.interfaces import ISchemaExtender
CharityEmployeeSchema = Schema((
StringField('title',
accessor='Title',
required=True,
user_property='fullname',
widget=StringWidget(
label=u"Full name",
description=u"Full name of this employee",
),
),
StringField('email',
validators=('isEmail',),
required=True,
searchable=True,
user_property=True,
widget=StringWidget(
label=u"Email address",
description=u"Enter the employee's email address",
),
),
StringField('phone',
required=False,
searchable=True,
user_property=True,
widget=StringWidget(
label=u"Phone number",
description=u"Enter the employee's phone number",
),
),
StringField('mobilePhone',
required=False,
searchable=True,
user_property=True,
widget=StringWidget(
label=u"Mobile phone number",
description=u"Enter the employee's mobile phone number",
),
),
StringField('location',
searchable=True,
user_property=True,
widget=StringWidget(
label=u"Location",
description=u"Your location - either city and country - or in a company setting, where your office is located.",
),
),
StringField('language',
user_property=True,
vocabulary="availableLanguages",
widget=SelectionWidget(
label=u"Language",
description=u"Your preferred language.",
),
),
TextField('description',
required=True,
searchable=True,
user_property=True,
default_content_type='text/html',
default_output_type = 'text/x-html-safe',
allowable_content_types = ('text/html', 'text/structured', 'text/x-web-intelligent',),
widget=RichWidget(
label=u"Biography",
description=u"Enter a short biography of the employee",
),
),
))
class EmployeeSchemaExtender(object):
"""Extend the schema of an employee to include additional fields.
"""
implements(ISchemaExtender)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def extend(self, schema):
schema = schema + CharityEmployeeSchema
# Reorder some fields
schema.moveField('description', after='mobilePhone')
schema.moveField('location', before='description')
schema.moveField('language', before='description')
schema.moveField('roles_', after='description')
return schema
This example is employee.py. The other extensions are simpler, and work on the exact same principle. When calculating the schema of a content type, the b-org types (by virtue of Products.borg.content.schema.ExtensibleSchemaSupport, a mix-in class that all the b-org types uses, and which the aforementioned changes to Archetypes should make obsolete) will look up an adapter from the content object (which is marked with IEmployeeContent, in this case), to ISchemaExtender. This will be given the chance to extend (and modify) the schema of the type.
The returned value is cached (to avoid an expensive re-calculation each time the schema is used). This cache can be invalidated upon an event, which you will see in charity/Extensions/Install.py:
from zope.event import notifyThe event is an instance of a class that implements ISchemaInvalidatedEvent, and takes a class as an argument to know which class the schema is being invalidated for.
from Products.borg.content.schema import SchemaInvalidatedEvent
from Products.borg.content.employee import Employee
...
def install(self, reinstall=False):
...
notify(SchemaInvalidatedEvent(Employee))
Defining new views and type information
We have now managed to add new schema fields to Department, Employee and Project. The auto-generated edit form will pick these up for editing, but we probably also want some custom views. We may also want to change other aspects of the Factory Type Information (FTI) which controls how the type is presented within Plone's UI (an FTI is an object in portal_types).First, we define some views in the browser package. These are described in a later section, but lookin at charity/configure.zcml, you will see:
<include package=".browser" />This will bring in charity/browser/configure.zcml, which contains several directives like:
<pageThis, along with the class Products.charity.browser.employee.EmployeeView and the template charity/browser/employee.pt will make a view @@charity_employee_view (the @@ is optional, but serves to disambiguate views from content objects, for example) available on any employee (or rather, any object providing IEmployeeContent).
name="charity_employee_view"
for="Products.borg.interfaces.IEmployeeContent"
class=".employee.EmployeeView"
template="employee.pt"
permission="zope2.View"
/>
We then need to tell Plone that this view should be invoked when you view an Employee object or click its 'View' tab. This is done by setting the (Default) and view method aliases for the Employee type. See this page of the RichDocument tutorial for some background.
To achieve this, we could modify portal_types/Employee in Python during the Install.py script. However, to make it easier to define the FTI, we use a GenericSetup XML file instead. Take a look at charity/Extensions/setup/types/Employee.py, for example:
<?xml version="1.0"?>This defines the various aspects of the FTI, and is basically a modified copy of the equivalent file from the b-org extension profile. You'll learn more about these in the section on GenericSetup, but for now observe that we invoke this explicitly in Install.py, via some boilerplate utility code:
<object name="Employee"
meta_type="Factory-based Type Information"
xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<property name="title">Employee</property>
<property name="description">A charity employee or volunteer.</property>
<property name="content_icon">employee.gif</property>
<property name="content_meta_type">Employee</property>
<property name="product">borg</property>
<property name="factory">addEmployee</property>
<property name="immediate_view">base_edit</property>
<property name="global_allow">False</property>
<property name="filter_content_types">False</property>
<property name="allowed_content_types" />
<property name="allow_discussion">False</property>
<alias from="(Default)" to="@@charity_employee_view"/>
<alias from="view" to="@@charity_employee_view"/>
<alias from="edit" to="base_edit"/>
<alias from="properties" to="base_metadata"/>
<alias from="sharing" to="folder_localrole_form"/>
<action title="View" action_id="view" category="object" condition_expr=""
url_expr="string:${object_url}" visible="True">
<permission value="View"/>
</action>
<action title="Edit" action_id="edit" category="object" condition_expr=""
url_expr="string:${object_url}/edit" visible="True">
<permission value="Modify portal content"/>
</action>
<action title="Properties" action_id="metadata" category="object" condition_expr=""
url_expr="string:${object_url}/properties" visible="True">
<permission value="Modify portal content"/>
</action>
<action title="Sharing" action_id="local_roles" category="object" condition_expr=""
url_expr="string:${object_url}/sharing" visible="True">
<permission value="Modify portal content"/>
</action>
</object>
from Products.charity.Extensions.utils import updateFTIThis will update the FTIs by examing Products/charity/Extensions/setup/types. Each file there is named corresponding to the name of the FTI it modifies.
def install(self, reinstall=False):
...
if not reinstall:
updateFTI(self, charity, 'Department')
updateFTI(self, charity, 'Employee')
updateFTI(self, charity, 'Project')
Adding new functionality
Extending the schema and modifying the FTI to support different views is probably enough for a large number of use cases. If you find yourself thinking "I wish I could add a method to the Employee class to support ...", take your left hand, hold it out, raise you right hand and slap your left wrist sternly, then read the section on adapters again.For example, let's say you wanted to send an email to administrators when a particular button in the view was clicked. You could do that in an adapter. For examples, in your interfaces module, you could could have:
from zope.interface import Interface
class IAdministratorNagging(Interface):
"""Someone who will nag the admin
"""
def nag(message):
"""Send nagging email
"""
Then, an adapter from IEmployee in module nag.py:
from zope.interface import implements
from zope.component import adapts
from interfaces import IAdministratorNagging
from Products.borg.interfaces import IEmployeeContent
from Products.CMFCore.utils import getToolByName
class NaggingEmployee(object):
implements(IAdministratorNagging)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def nag(self, message):
mailHost = getToolByName(self.context, 'MailHost')
...
And finally, in your configure.zcml:
<adapter factory=".nag.NaggingEmployee" />
Then, in the form handler that is about to nag the employee, you would do:
from Products.myproduct.interfaces import IAdministratorNagging
nagger = IAdministratorNagging(employee)
nagger.nag("Give me more disk space!")
Obviously, this is a somewhat contrived example, but hopefully you get the gist.
Modifying workflow and other configuration
The b-org workflows are not special. In your Install.py, you could modify them or change the workflow assignments as you would any other content type. You can also use CMFPlacefulWorkflow to assign different workflows depending on context, if need be.
Similarly, if you need to modify the behaviour of the Department, Employee and Project types in other ways, for example by modifying settings in portal_properties, you are of course free to do so. The intended pattern is that your b-org customisation product encapsulates the various settings and extensions that describe your use case.
Changing fundamental b-org behaviour
Lastly, as you learn about b-org you will see how it uses adapters to hook into membrane. If you need to override its behaviour, you can add an overrides.zcml to your product, which is otherwise identical to a configure.zcml in format, but is able to override earlier registrations (such s those in b-org). For example, you could override the adapter from IEmployeeContent to IUseRelated to change the way in which user ids is assigned, or the adapter to IUserAuthentication to change the way in which authentication is performed.
2.5.5.6. Filesystem organisation
b-org attempts to adhere to modern ideal about how code should be laid out on the filesystem.
In the Zope 3 world, the Products pseudo-namespace is frowned upon. In Zope 2, every extension module lives in the Products/ folder. This raises some obvious namespace clash concerns, but also separates Zope modules further from plain-Python modules. In Zope 3, you can install a module anywhere in your PYTHONPATH. For example, in Plone 3.0, there will be a module called plone.portlets, normally installed in lib/python/plone/portlets.
For modules that need to act like Zope products (i.e. they need an initialize() method, they install content types, they register a GenericSetup profile or CMF skins or use an Extensions/Install.py method, say), this works in Zope 2.10 and later. It can also be made to work in earlier version of Zope using a product (ironically) called pythonproducts.
For the purposes of borg, we stick with the traditional Products/ installation. It's nice to have imports like from borg import ..., but fundamentally, b-org is very closely tied to Zope (2) and Plone, so the re-use argument goes away, and that nice import syntax is not really worth the extra dependency and configuration.
One thing you may notice, though, is that the borg product is named in lowercase, in keeping with Zope 3 and Python naming conventions. Looking inside it, you will see the following key files and directories:
- __init__.py
- Initialises the Zope 2 product machinery, registers content types, the skin layer and the GenericSetup extension profile that is used to install b-org.
- config.py
- Holds various constants
- configure.zcml
- Starts the Zope 3 snowball going. This references other packages with their own configure.zcml files.
- content/
- Contains the Archetypes content types for Department, Employee and Project. Also contains some utilities, like EmployeeLocator, an adapter to find employees, two utilities used to provide vocabularies AddableTypesProvider and ValidRolesProvider, and the the schema extension mechanism in schema.py.
- events/
- Contains event subscribers which modify ownership of an Employee object so that the employee user owns it (and can thus edit their own profiles, for example), as well as to set up the local workflow when a Project is created.
- interfaces/
- Contains all the interfaces that b-org defines, in various sub-modules like interfaces/employee.py for the Employee-related interfaces. All of these are imported into interfaces/__init__.py, so that you can write from Products.borg.interfaces import ....
- membership/
- Contains various adapters for plugging into membrane which enable b-orgs user-and-group functionality.
- pas/
- Contains a custom PAS plug-in which is used to manage the local roles for Project members
- permissions.py
- Contains custom add-content permissions, so that the ability to add Department, Employee and Project content objects can be controlled by different permissions.
- profiles/
- Contains the GenericSetup extension profile that sets up b-org. This is registered in the borg/__init__.py.
- setuphandlers.py
- Defines a custom GenericSetup "import step" which configures aspects of b-org that cannot be expressed in the existing GenericSetup XML formats.
- skins/
- Contains the borg skin layer, which is registered in borg/__init__.py. This contains only the b-org icons. These could potentially have been defined in a browser package using Zope 3 resources, but are included in a traditional skin layer to make them easier to customise using conventional methods. See the section on Zope 3 views for more details.
- tests/
- Contains unit and integration tests.
- zmi/
- Defines a ZMI page for adding the PAS plug-in, for completeness' sake.
You will notice that there are many directories, and many of these directories contain the same set of files - employee.py, department.py and project.py. This is a side-effect of the finer-grained components and increased separation of concerns that stem from Zope 3 design concepts. For products that act less as framework, the degree of separation may be lower, and thus the product may appear smaller. However, as you browse b-org's source code, it should become obvious why things are placed where they are, and how code is grouped together by logical functionality rather than a tight coupling to Archetypes content types.
2.5.5.7. Interfaces
In Zope 3, everything is connected to an interface in some way. Sure enough, b-org has a slew of them. Getting the interface design right is often more than half the battle, so pay attention to this part.
If you were trying to understand b-org without a comprehensive tutorial to hand, you would do well to look at the interfaces package. You will notice that this is subdivided into various files
- interfaces/department.py
- Contains a description of a department (IDepartment) and a marker interface for the content object that stores the department (IDepartmentContent).
- interfaces/employee.py
- Contains the equivalent interfaces, IEmployee and IEmployeeContent, as well as the definition of a specific event interface, IEmployeeModified.
- interfaces/project.py
- Again contains IProject and IProjectContent, as well ILocalWorkflowSelection, which is used to denote a utility that defines the placeful workflow policy that projects will use.
- interfaces/workspace.py
- Holds the interface IWorkspace, which is used by the local-role PAS plug-in to extract which users should have which local roles in a project.
- interfaces/schema.py
- Contains interfaces relevant to the custom schema extension mechanism - ISchemaExtender, IExtensibleSchemaProvider and ISchemaInvalidatedEvent.
- interfaces/utils.py
- Defines interfaces that are used as input to various vocabularies - IEmployeeLocator, IAddableTypesProvider and IValidRolesProvider.
In order to understand what each of these interfaces describes in more detail, look at the files above. Recall that interfaces are mainly documentation - these interfaces are accompanied by docstrings and generally self-documenting code.
The various interfaces intended for public consumption are imported to interfaces/__init__.py, so that client code can write, e.g.:
from Products.borg.interfaces import IEmployeeThis is a common idiom. If you find yourself with too many interfaces to manage in interfaces/__init__.py, you don't necessarily need to do this, but it's probably a sign that you should be breaking your code into smaller packages!
Remember that unless you have a particular need to depend on Zope 2, then you don't need to pollute the Products namespace with such components! (and even if you do, with PythonProducts or Zope 2.10, you can do without the Products/ namespace too). For example, we could have placed the employee functionality in a package borg.employee, found in lib/python/borg/employee as a plain-python library, possibly depending on Zope 3 components (i.e. packages in the zope.* namespace).
Conversely, if you have relatively few interfaces, you can simply have an interfaces.py module without a directory.
Separating Archetypes from real components
One thing you may notice is that we have split the interface describing the concept of e.g. an employee (IEmployee) from the interface that describes the employee content object in the ZODB (IEmployeeContent). Whether this is always the right thing to do is debatable, but the reasoning goes something like this:Archetypes objects contain a very large API. Archetypes schemas and the infamous ClassGen generate methods on the content objects corresponding to schema fields, so that a field name gets an accessor called getName() and a mutator called setName(). This is all rather Archetypes-specific, and in Zope 3 schemas, we typically prefer simple properties (a name attribute) to pairs of methods. To avoid being constrained by the Archetypes when defining interfaces (Archetypes is just one implementation choice), we created IEmployee as follows:
class IEmployee(Interface):To support this, we could put the relevant properties into the Archetypes content object, but this is cumbersome, since the property() declaration normally used to convert methods to properties will only work when those methods actually exist, not when they are created by ClassGen.
"""An employee, which is also a user.
"""
id = schema.TextLine(title=u'Identifier',
description=u'An identifier for the employee',
required=True,
readonly=True)
fullname = schema.TextLine(title=u'Full name',
description=u"The employee's full name for display purposes",
required=True,
readonly=True)
Instead, we mark the content object with a marker interface, IEmployeeContent and then register an adapter to IEmployee. Strictly speaking, this is cheating, since the adapter makes assumptions about its context (such as which methods are available, and the fact that it uses Archetypes) that are not formally defined in the interface. To save excessive typing and retain some sanity in the interface definitions, it's not a terrible compromise though. Here's the adapter, from membership/employee.py:
class Employee(object):
"""Provide department information.
"""
implements(IEmployee)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
@property
def id(self):
return self.context.getId()
@property
def fullname(self):
return self.context.Title()
Now, you can write:
emp = IEmployee(some_employee_content_object)
print emp.fullname
Another side-effect of this pattern is that we can separate things that are Archetypes-dependent from things that operate on the more general notion of an employee. For example, membrane generally makes assumptions about operating on Archetypes content objects, so the various membrane adapters adapt IEmployeeContent, whereas the view for charity employees is only concerned with "real" employees and so adapts the context to IEmployee.
This pattern is repeated for Departments and Projects as well.
Interfaces intended for utilities and adapters
Although interface design should generally not be too concerned with how those interfaces are implemented, you will often think "this is going to be used a a utility" or "this will most likely be an adapter". In this case, you may want to make some reference in the doc-string at least. For example, the ILocalWorkflowSelection interface states:class ILocalWorkflowSelection(Interface):Conversely, many interfaces are context-dependent, which means that most likely they will either be directly provided by a particular object or adaptable to it. Take the IAddableTypesProvider:
"""A selection of a local workflow for projects.
This will normally be looked up as a utility.
"""
workflowPolicy = schema.TextLine(title=u'Workflow policy identifier',
description=u'The id of the placeful workflow policy to use',
required=True,
readonly=True)
class IAddableTypesProvider(Interface):The implication here is that client code will do something like:
"""A component capable of finding addable types in a given context.
"""
availableTypes = schema.Tuple(title=u'Available types',
description=u'A list of all addable types',
value_type=schema.Object(ITypeInformation))
defaultAddableTypes = schema.Tuple(title=u'Default addable types',
description=u'A list of types to be addable by default',
value_type=schema.Object(ITypeInformation))
from Products.borg.interfaces import IAddableTypesProviderWhether IAddableTypesProvider was provided directly by the context or (more likely) provided via an adapter is not important. The only time this distinction is really useful is in the case of marker interfaces, such as IEmployeeContent:
addableTypes = IAddableTypesProvider(context).availableTypes
class IEmployeeContent(Interface):These are often checked with providedBy():
"""Marker interface for employee content objects"""
assert IEmployeeContent.providedBy(employeeContentObject)Again, the guiding principle here is separation of concerns. The aspect of a component that can provide a list of addable types (IAddableTypesProvider) is logically distinct from (and could be varied independently of) the aspect of a component that specifies it represents a project (IProject), even though it so happens that at present projects are the only time we concern ourselves with restricting addable types.
# we've got an employee, good
In the olden days, we would probably have put methods like getAvailableProjectAddableTypes() into the Project content type. Hopefully, you'll see why this is less optimal than having it in a separate component (hint: what if you in your customisation of b-org wanted to be much more particular about which types were addable?). You will hopefully start to pick up "fat" interfaces during interface design - if you had a neat IProject interface that described attributes of a project that were to be saved alongside the project object, and then found a couple of methods about defining addable types that were related to one another but not so much to the data of a project in general, you would hopefully reach for a new interface. If so - well done, you're getting there.
2.5.5.8. Test-driven development
Testing should come first, not last, when doing development.
One of the greatest things that Zope 3 has established is a culture of test-driven development. Because Zope 3 components tend to be small and not dependent on a large framework or (typically) a running application server, tests are easier to write and execute faster. Most Zope 3 testing happens in the form of testable documentation - DocTests - which tell the story of how a component should be used along with testable examples.
The testing tutorial explains the philosophy behind test-driven development and the tools and techniques available in Zope. It is required reading if you are not familiar with testing in Zope, and probably quite useful even if you are.
Testing strategy
Tests were (largely) written against interfaces and stub implementations, before the actual functionality was written. One of the first test cases to be created was test_adapters.py, which simply verifies that the various adapter registrations are in effect. This is obviously an integration test (using PloneTestCase), since it is verifying what happens on a "normal" Zope start-up.You will also notice tests named after the three content types, test_department.py, test_employee.py and test_project.py. Each of these contains tests that verify the given type is available and can be instantiated and edited. This catches errors in Archetypes registrations or schemas. There are then further tests for the membrane integration and for the adapters to the canonical interfaces IDepartment, IEmployee and IProject. Lastly, non-trivial methods in content types and relevant adapters are given their own test fixtures.
By being systematic and diligent with tests, many, many bugs were caught and dealt with before they ever hit a live system. Of course, this does not replace in-browser acceptance testing, which was also performed regularly.
At the time of writing, there are no zope.testbrowser based functional tests for the user interface. That is regrettable - and this is an open source project after all, so feel free to contribute some!
Test set-up
You will find b-org's tests in the tests module. Most of these use are DocTest integration tests, using PloneTestCase. Make sure you use a recent version of PloneTestCase (or svn trunk) since there have been some recent changes in how Zope 3 components (or rather, ZCML registrations) are loaded for test runs. The upshot is that with PloneTestCase, things should "just work" for integration testing - components you have defined in ZCML in your products will be loaded as they would when Zope is started.
The file base.py contains an insulating base class for b-org tests, called BorgTestCase and its sister-class BorgFunctionalTesetCase. When imported, this file will trigger the setup of a Plone site with the membrane and borg extension profiles installed, as such:
from Testing import ZopeTestCase
# Let Zope know about the two products we require above-and-beyond a basic
# Plone install (PloneTestCase takes care of these).
ZopeTestCase.installProduct('membrane')
ZopeTestCase.installProduct('borg')
# Import PloneTestCase - this registers more products with Zope as a side effect
from Products.PloneTestCase.PloneTestCase import PloneTestCase
from Products.PloneTestCase.PloneTestCase import FunctionalTestCase
from Products.PloneTestCase.PloneTestCase import setupPloneSite
# Set up a Plone site, and apply the membrane and borg extension profiles
# to make sure they are installed.
setupPloneSite(extension_profiles=('membrane:default', 'borg:default'))
Integration and unit tests
Most of the tests are integration test that are set up like so:
import unittest
from Testing.ZopeTestCase import ZopeDocTestSuite
from base import BorgTestCase
from utils import optionflags
def test_creation():
"""Test that departments can be created an initiated.
>>> self.setRoles(('Manager',))
>>> id = self.portal.invokeFactory('Department', 'dept')
>>> dept = self.portal.dept
Set roles.
>>> dept.setRoles(('Reviewer',))
>>> tuple(dept.getRoles())
('Reviewer',)
Add an employee and set it as a manager.
>>> id = dept.invokeFactory('Employee', 'emp')
>>> dept.setManagers((dept.emp.UID(),))
>>> tuple(dept.getManagers())
(<Employee at ...>,)
"""
...
def test_suite():
return unittest.TestSuite((
ZopeDocTestSuite(test_class=BorgTestCase,
optionflags=optionflags),
))
There is also a plain-python (no loading of Zope necessary, which is much faster) unit test for the password digest in test_passwords.py. This is appropriate because the functionality under test does not depend on the Zope application server or database being loaded. Use plain-python (or perhaps rather, plain Zope 3) tests whenever you can to reduce interdependencies and test load times:
import unittest
from zope.testing.doctestunit import DocTestSuite
from utils import configurationSetUp, configurationTearDown, optionflags
def test_passwords_hashed():
"""Check that passwords are hashed
We expect that the password will be saved as a SHA-1 digest.
>>> import sha
>>> digest = sha.sha('secret').digest()
Set a password.
>>> from Products.borg.content.employee import Employee
>>> e = Employee('emp')
>>> e.setPassword('secret')
The value is stored in an annotation, and there is no direct way to
access it (deliberately). Thus, check the annotation directly.
>>> from zope.app.annotation.interfaces import IAnnotations
>>> from Products.borg.config import PASSWORD_KEY
>>> annotations = IAnnotations(e)
>>> password = annotations[PASSWORD_KEY]
Ensure it is what we expected:
>>> password == digest
True
"""
...
def test_suite():
return unittest.TestSuite((
DocTestSuite(setUp=configurationSetUp,
tearDown=configurationTearDown,
optionflags=optionflags),
))
The functions configurationSetUp() and configurationTearDown() are defined in utils.py and are used to load specific ZCML files that enable the test environment to function. This is necessary because without PloneTestCase's integration test layer in effect, there will be no compnent registrations when the tests are run! This may be more cumbersome (though in reality, the same set of components tend to be used), but also allows better control over the environment in which test are run, in addition to (much) faster test execution times.
From utils.py:
import doctest
from zope.app.tests import placelesssetup
from zope.configuration.xmlconfig import XMLConfig
# Standard options for DocTests
optionflags = (doctest.ELLIPSIS |
doctest.NORMALIZE_WHITESPACE |
doctest.REPORT_ONLY_FIRST_FAILURE)
def configurationSetUp(self):
"""Set up Zope 3 test environment
"""
placelesssetup.setUp()
# Ensure that the ZCML registrations in membrane and borg are in effect
# Also ensure the Five directives and permissions are available
import Products.Five
import Products.membrane
import Products.borg
XMLConfig('configure.zcml', Products.Five)()
XMLConfig('meta.zcml', Products.Five)()
XMLConfig('configure.zcml', Products.membrane)()
XMLConfig('configure.zcml', Products.borg)()
def configurationTearDown(self):
"""Tear down Zope 3 test environment
"""
placelesssetup.tearDown()
You will also find a regular unit test in test_setup.py, simply because this was quicker to write:
from base import BorgTestCase
from Products.membrane.interfaces import ICategoryMapper
from Products.membrane.config import ACTIVE_STATUS_CATEGORY
from Products.membrane.utils import generateCategorySetIdForType
from Products.borg.config import LOCALROLES_PLUGIN_NAME, PLACEFUL_WORKFLOW_POLICY
class TestProductInstall(BorgTestCase):
def afterSetUp(self):
self.types = ('Department', 'Employee', 'Project',)
def testTypesInstalled(self):
for t in self.types:
self.failUnless(t in self.portal.portal_types.objectIds(),
'%s content type not installed' % t)
...
def test_suite():
from unittest import TestSuite, makeSuite
suite = TestSuite()
suite.addTest(makeSuite(TestProductInstall))
return suite
Finally, there is an docstring DocTest for the ExtensibleSchemaSupport class. This is because this class if largely standalone (it probably shouldn't be b-org at all, but in a more general module, except Archetypes will gain similar functionality of its own for Plone 3.0) and the test provided important documentation in the class' docstring.
The class looks like this:
class ExtensibleSchemaSupport(Base):
"""Mixin class to support instance-based schemas.
Note: you must mix this in before BaseFolder or BaseContent, e.g.:
class Foo(ExtensibleSchemaSupport, BaseContent):
...
This is based on Archetype's VariableSchemaSupport.
Define a content type with a marker interface:
>>> from zope.interface import Interface, implements
>>> class IMyType(Interface):
... pass
>>> from Products.Archetypes.atapi import *
>>> from Products.borg.content.schema import ExtensibleSchemaSupport
>>> class MyType(ExtensibleSchemaSupport, BaseObject):
... implements(IMyType)
... schema = BaseSchema.copy() + Schema((StringField('foo'),))
>>> registerType(MyType, 'testing')
Create a schema extender:
...
"""
implements(IExtensibleSchemaProvider)
...
And the test runner, in test_schema.py, contains:
import unittest
from Testing.ZopeTestCase import ZopeDocTestSuite
from base import BorgTestCase
from utils import optionflags
def test_suite():
return unittest.TestSuite((
ZopeDocTestSuite('Products.borg.content.schema',
test_class=BorgTestCase,
optionflags=optionflags),
))
2.5.5.9. Setup using GenericSetup
b-org uses GenericSetup to impose itself on your Plone instance. Here's how it works.
Hands up if you have ever written a workflow definition in Python and tried to figure out how to install it in your Extensions.py and thought, this is the least useful API I have ever had to deal with. Actually, the API is not that bad, it's just not very good for performing set-up. Similarly, it may start to make your separation-of-concerns-brainwashed mind a little uneasy that we tend to define aspects of the type's configuration as class attributes in an Archetypes class (though of course it's better than using a CMF FTI dict or mangling portal_types directly).
The fine folks who gave us the CMF came up with another way, called GenericSetup (after a few name changes - you may see the names CMFSetup and ContentSetup as well, which refer to predecessors of what is not GenericSetup). This is based on a declarative XML syntax that can represent site configuration. The configuration of an entire site is called a profile and can be imported and exported to replicate state across multiple Plone (or CMF) sites. There is a smaller version of a profile called an extension profile which can be used to extend a base profile. Both membrane and b-org use extension profiles to install themselves.
GenericSetup is described a tutorial by Rob Miller, cheif GenericSetup protagonist, so we won't repeat too much of the detail here. However, you should be aware that in Plone 2.5, GenericSetup has a slightly awkward user experience and does not have any well-defined way of performing uninstall, which stems from the fact that it was originally designed for the use case of taking a snapshot of the configuration of an entire site, not for installing and uninstalling products and extensions!
The other main shortcoming at the moment is that there is no way to specify interdependencies between profiles. It is important that membrane is installed before b-org, but if you're not careful it will happen the other way around. When you create a Plone site, you will be able to choose a number of extension profiles to apply (including meaningless ones like Archetypes - meaningless because Plone already invokes those when you set up a site). In this list, "Base organisation" comes before "membrane" by virtue of alphabetical sorting. Therefore, you can't just choose both and click "Add". Instead, you should select "membrane" first, and then add "Base organisation" via portal_setup, as described in the b-org README.txt:
- Go to portal_setup in the ZMI
- Click the Properties tab
- Select "Base organisation" as the active profile (since this is an extension profile, it won't override the base profile that set up your Plone site) and click Update.
- Go to the Import tab and click Import all steps at the bottom. Note that although it seems like this will re-install a whole bunch of stuff, it will only execute those steps that are actually listed in the import_steps.xml for the active profile, which in this case is b-org's.
If you didn't already set up membrane and you created a Plone site without the membrane extension profile, follow the same steps to install membrane before you install b-org.
So why did we do all this? Firt of all, both membrane and b-org are really infrastructure that fundamentally influence how you build your site, so the lack of uninstall isn't as bad as it would have been for more user-facing products. Secondly, with Plone 3.0, this will become easier, as the QuckInstaller (and hence the Add/Remove Products control panel page) becomes Extension Profile aware and gives some uninstall support.
At the end of this section, you will see how you can use a traditional QuickInstaller Install.py method and still get the nice XML syntax, with a bit of extra work.
Import steps
To GenericSetup, the installation of a third party product via an extension profile is considered to be the importing of that profile. A file import_steps.xml is used to determine which actual import steps will be executed. First, we need to tell GenericSetup where the import steps are defined, though, by registering the extension profile. This is done in the product's __init__.py:
from Products.CMFPlone.interfaces import IPloneSiteRoot
from Products.GenericSetup import EXTENSION, profile_registry
...
def initialize(context):
...
profile_registry.registerProfile('default',
'Base organisation',
'Organisation and project infrastructure',
'profiles/default',
'borg',
EXTENSION,
for_=IPloneSiteRoot)
This references the directory profiles/default, which contains various files:
- import_steps.xml
- Lists the steps to be performed during import (set-up)
- export_steps.xml
- Lists the steps to be performed during export - that is, if the configuration is changed in the ZODB and the site admin wishes to export the configuration to a file, these steps will be performed.
- membrane_tool.xml
- Configuration for membrane tools
- skins.xml
- Sets up skins in portal_types
- types.xml
- Configures FTIs (Factory Type Information settings) for the content types that b-org ships with. Each of the types listed here has a corresponding file in profiles/default/types (the name of the type and the name of the file should match). This file contains all the various FTI settings, such as friendly name, meta type, actions and aliases.
- workflows.xml
- Configures workflows. This works in the same way as types.xml - the main file configures the names of the workflows and the bindings of workflows to content types. The actual workflow definitions, including states and transitions, are found in profiles/default/workflows.
<?xml version="1.0"?>Note that we don't actually specify most of the files - they are referenced by the base profile that was used to set up Plone or the extension profile for membrane. GenericSetup knows all the registered profiles' steps, and looks for the corresponding files.
<import-steps>
<import-step id="borg_various" version="20060803-01"
handler="Products.borg.setuphandlers.importVarious"
title="Various base-org Settings">
<dependency step="typeinfo"/>
<dependency step="skins"/>
<dependency step="workflow"/>
</import-step>
</import-steps>
Various setup handlers
The one setup handler you do see is the "various" handler. This is dependent on the set-up of type info, skins and workflow. Ordinarily, setup handlers will utilise GenericSetup base classes, adapters and utility functions to parse XML files. However, it's not always convenient to invent a generic XML syntax for all types of configuration. The importVarious pattern is used by many products that need to perform some custom set-up in Python. It is invoked as if it were a handler for an XML file, but it just happens to have different side-effects. The main caveat with this type of set-up, of course, is that it cannot symmetrically export (and then re-import) the configuration, and it is more difficult to re-use.
importVarious looks as follows:
from StringIO import StringIO
...
def setupPlugins(portal, out):
"""Install and prioritize the project local-role PAS plug-in.
"""
...
def setupPortalFactory(portal, out):
"""Add borg types to portal_factory
"""
...
def addProjectPlacefulWorkflowPolicy(portal, out):
"""Add the placeful workflow policy used by project spaces.
"""
....
def importVarious(context):
"""
Import various settings.
Provisional handler that does initialization that is not yet taken
care of by other handlers.
"""
site = context.getSite()
out = StringIO()
setupPlugins(site, out)
setupPortalFactory(site, out)
addProjectPlacefulWorkflowPolicy(site, out)
logger = context.getLogger("borg")
logger.info(out.getvalue())
We set up the PAS plugins, register our types with portal_factory and add a placeful workflow policy. The exact code to perform each of these steps is not listed here to save space, but they use the same techniques you would use in an Install.py file. Note that the portal_factory setup is available in a more friendly XML format in Plone 2.5.1 and later, which was released after b-org.
GenericSetup without portal_setup
When Plone 3.0 arrives, it will make the Add/Remove Products control panel aware of extension profiles, and thus provide a more user friendly way of performing install using GenericSetup. It will also support uninstall. Until that time, however, it is possible to re-use the GenericSetup XML handlers to parse files like types.xml and workflow.xml from a regular Install.py installation. We do this in the charity example.When importing, GenericSetup requires a setup environment, and usually an object to work on. A simple SetupEnviron is found in charity/Extensions/utils.py, along with a method called updateFTI() which can take an FTI object and update its settings based on a types.xml-like file. This method takes a module and the id of an FTI to update, and finds the corresponding file.
It is used in charity/Extensions/Install.py as follows:
from Products import charity
from Products.charity.Extensions.utils import updateFTI
def install(self, reinstall=False):
...
if not reinstall:
updateFTI(self, charity, 'Department')
updateFTI(self, charity, 'Employee')
updateFTI(self, charity, 'Project')
The relevant files may be found in charity/Extensions/setup/types/.
2.5.5.10. Using membrane to provide membership behaviour
How b-org uses membrane to let employees be users and departments be groups
Since version 2.5, the user management infrastructure in Plone has been replaced by PAS, the Zope Pluggable Authentication Service, and PlonePAS, a Plone integration layer for this. PAS offers several advantages over plain user folders, mainly in terms of flexibility. Unfortunately, it is also more difficult to work with through-the-web and has a very decentralised API, based on the notion of plugin components, that can be difficult to understand at first.
Membrane (or rather, membrane with a lowercase m) is a component first developed by Plone Solutions and later improved by Rob Miller and others. It is similar to CMFMember in that it can turn content objects into users, although it is less concerned with replicating existing Plone functionality and more concerned with making a thin integration layer to plug into. It therefore fits b-org very well.
Membrane works on Archetypes objects (though theoretically it could be used with other objects as well). It adds a tool called membrane_tool which contains a registry of content types that are member- or group-sources, as well as a special catalog. Using the Archetypes catalog multiplex, it is able to catalog objects (which may also be cataloged in portal_catalog) and find them again based on various interfaces (that is, it catalogs the interfaces provided by an object). membrane provides a number of PAS plug-ins that will search this catalog when looking for users and delegate to the content objects (or rather, adapters on the content object) for obtaining user information, performing authentication and so on.
Registering with membrane
membrane_tool contains an API for registering content types as membership providers, but the easiest option is to use a GenericSetup profile (see the section on GenericSetup for the full story). In profiles/default/membrane_tool.xml, you will find:<?xml version="1.0"?>This registers the three content types (by their portal type), and specifies the workflow states in which they are "active" as member and group sources.
<object name="membrane_tool" meta_type="MembraneTool">
<membrane-type name="Department">
<active-workflow-state name="active" />
</membrane-type>
<membrane-type name="Employee">
<active-workflow-state name="active" />
</membrane-type>
<membrane-type name="Project">
<active-workflow-state name="published" />
<active-workflow-state name="private" />
</membrane-type>
</object>
Applying marker interfaces
When looking for content objects that provide group and member information, membrane will use a number of marker interfaces that indicate support for various types of behaviour. These are implemented by the three content type classes.In content/department.py, you will find:
from Products.membrane.interfaces import IPropertiesProviderAll this means is that the Department's schema is capable of providing properties to PAS. Properties (normally related to users, but groups can have properties as well) are just metadata about the user or group. Membrane supports as PAS properties plugin that will look for Archetypes schema fields with member_property=True set and report these back as user properties. Although Department does not use any such properties at the moment, we add this marker so that extension modules that use the schema extension mechanism can benefit from this.
...
class Department(ExtensibleSchemaSupport, BaseFolder):
"""A borg department.
Departments can contain other employees.
"""
implements(IDepartmentContent, IPropertiesProvider)
The equivalent setup for Employees, in content/employee.py, is a little more interesting.
from Products.membrane.interfaces import IUserAuthProviderHere, we are saying that:
from Products.membrane.interfaces import IPropertiesProvider
from Products.membrane.interfaces import IGroupsProvider
from Products.membrane.interfaces import IGroupAwareRolesProvider
...
class Employee(ExtensibleSchemaSupport, BaseContent):
"""A borg employee.
Employees are also users.
"""
implements(IEmployeeContent,
IUserAuthProvider,
IPropertiesProvider,
IGroupsProvider,
IGroupAwareRolesProvider,
IAttributeAnnotatable)
- An Employee can be used as a source of authentication (i.e. as a user), since it is marked with IUserAuthProvider. Note that the actual authentication is performed by a different adapter.
- An Employee can provide user properties to PAS via membrane, following IPropertiesProvider.
- An Employee can be part of a group, because of IGroupsProvider.
- An employee can be given roles. There is an IRolesProvider interface that we cold use for basic role awareness. The IGroupAwareRolesProvider is a sub-interface that will cause membrane to also look at the user's groups.
Projects does not require any particular marker interfaces.
Providing membership behaviour
When membrane looks for objects to provide membership-related behaviour, it will not only look for objects directly providing a particular interface, but also for objects that can be adapted to that interface. For example, the presence of the interface IGroup informs membrane that an object can act as a group, and contains methods that describe the members of that group.Of course, we could have declared that Department implemented IGroup and written these methods directly in the Department content object. Hopefully you'll agree now that this would not be optimal, since it mixes the content-object aspect and the group-behaviour aspect of Department into a single monolithic object. Instead, we will use an adapter, which also means that if you require different behaviour in an extension to b-org, you have only to override the adapter, leaving the core content object alone.
In membership/department.py, you will see:
class Group(object):Mostly, this is about examining the Department content object to find roles (which are listed in an Archetypes field, editable by the Manager role). When calculating roles, we make sure that we don't give roles if the Department group-source is actually disabled (by virtue of its workflow state and the settings in membrane_tool). The group title and id are taken from the object as well.
"""Allow departments to act as groups for contained employees
"""
implements(IGroup)
adapts(IDepartmentContent)
def __init__(self, context):
self.context = context
def Title(self):
return self.context.Title()
def getRoles(self):
"""Get roles for this department-group.
Return an empty list of roles if the department is in a workflow state
that is not active in membrane_tool.
"""
mb = getToolByName(self.context, MEMBRANE_TOOL)
wf = getToolByName(self.context, 'portal_workflow')
reviewState = wf.getInfoFor(self.context, 'review_state')
wfmapper = ICategoryMapper(mb)
categories = generateCategorySetIdForType(self.context.portal_type)
if wfmapper.isInCategory(categories, ACTIVE_STATUS_CATEGORY, reviewState):
return self.context.getRoles()
else:
return ()
def getGroupId(self):
return self.context.getId()
def getGroupMembers(self):
mt = getToolByName(self.context, MEMBRANE_TOOL)
usr = mt.unrestrictedSearchResults
members = {}
for m in usr(object_implements=IMembraneUserAuth.__identifier__,
path='/'.join(self.context.getPhysicalPath())):
members[m.getUserId] = 1
return tuple(members.keys())
The most interesting method is getGroupMembers(). Here, we perform a search in the membrane_tool catalog for objects adaptable to IMembraneUserAuth. This interface is the basic interface in membrane describing things that can act as users - there is an adapter from IUserAuthProvider to IMembraneUserAuth. We restrict this to objects inside the Department object. The net effect is that all Employee objects inside the Department are returned.
Now, let's say you had a need for a Department which in addition to acting as a group for all members inside it, also allowed some members from other departments to be in that group. In this case, you could use a schema extender to add a ReferenceField to the schema of Department that allowed the Department owner to reference other Employees. You would then provide an override adapter, perhaps subclassing Products.borg.membership.department.Group but overriding getGroupMembers() to append the ids of the referenced users as well as the contained ones ... or instead of, depending on your needs.
As it happens, Projects also act as groups, with members being assigned by reference, using two reference fields - one for project members, and one for project manangers. Here is the equivalent adapter from membership/project.py:
class Group(object):As may be expected, the membrane adapters for Employee are a bit more involved. They consist of the following:
"""Allow projects to be groups for related members and managers
"""
implements(IGroup)
adapts(IProjectContent)
def __init__(self, context):
self.context = context
def Title(self):
return self.context.Title()
def getRoles(self):
# The project does not imply any special roles *globally*, although
# the IWorkspace adapter above enables some local roles
return ()
def getGroupId(self):
return self.context.getId()
def getGroupMembers(self):
return [IUserRelated(m).getUserId() for m in
self.context.getRefs(PROJECT_RELATIONSHIP) +
self.context.getRefs(PROJECT_MANAGER_RELATIONSHIP)]
- IUserRelated adapter
- Provides a user id for employees. Note that user ids and user names are possibly different when PAS is used: the user id must be globally unique; the user name is the named used for logging in.
- IUserAuthentication adapter
- Used to perform actual authentication by validating a supplied username and password.
- IUserRoles adapter
- Used to determine which roles the particular user is given.
- IMembraneUserManagement
- Used by membrane and Plone's UI to deal with changes to the user, such as the adding of a new user (not implemented here, since we
The IUserRelated adapter is the simplest, as it simply invokes the user name. Note that by default, membrane will use the Archetypes UID() function as the user id. This is sensible, but unfortunately Plone's UI (and that of third party products) is not always aware of the distinction between user id and user name. Ideally, only the user name would ever be displayed, the user id being an internal concept, but in practice you may end up with things like member folder names that are long, unfriendly UID strings. Sometimes this may even be unavoidable in the general case, because it's possible that two different sources of users could use the same user name for two different user ids! For the purposes of b-org, however, we assume user names are unique and well-defined. The adapter is therefore quite trivial:
class UserRelated(object):The id of the content object that represents the employee is used as the user id. This is also used as the user name, as defined in the IUserAuthentication adapter:
"""Provide a user id for employees.
The user id will simply be the id of the member object. This overrides the
use of UIDs
"""
implements(IUserRelated)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def getUserId(self):
return self.context.getId()
class UserAuthentication(object):In the verifyCredentials() method, the adapter is passed the login and password as entered by the user in a dict (credentials) and then compares those to the values stored on its context (the Employee content object). The password is stored as a SHA1 digest in an annotation to make sure it cannot be read back by examining the content object - more on this in the section on annotations. Be aware also that the IUserAuthentication adapter is called on every request after a user is logged in and can deny access for whatever reason by returning non-True. This means that it is important that the method is as efficient as possible - expensive database lookups, for example, are probably not a good idea here!
"""Provide authentication against employees.
"""
implements(IUserAuthentication)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def getUserName(self):
return self.context.getId()
def verifyCredentials(self, credentials):
login = credentials.get('login', None)
password = credentials.get('password', None)
if login is None or password is None:
return False
digest = sha(password).digest()
annotations = IAnnotations(self.context)
passwordDigest = annotations.get(PASSWORD_KEY, None)
return (login == self.getUserName() and digest == passwordDigest)
The IUserRoles adapter is trivial. Roles are stored on the content object in a field that is editable only by managers. Of course, we could have picked roles from some other rule if necessary:
class UserRoles(object):The getRoles() method returns an iterable of strings representing applicable roles. Note that depending on group membership (and the IGroupAwareRolesProvider marker as described above) and local roles the user may in fact have more roles than what this method returns! The IUserRoles interface is concerned only with global roles intrinsic to the user.
"""Provide roles for employee users.
Roles may be set (by sufficiently privilged users) on the user object.
"""
implements(IUserRoles)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def getRoles(self):
return self.context.getRoles()
Finally, we have the IMembraneUserManagement adapter. This lets membrane know what to do when it is asked by Plone's UI to add, edit or remove users. In particular, the doChangeUser() method enables the PasswordResetTool to do its magic. Note that we have not implemented doAddUser(), because there is no well-defined global policy for where the actual Employee content object should be added! Recently membrane has gained some functionality whereby a site-local utility providing IUserAdder from membrane can be queried for this policy. That may be useful for b-org extension products, but b-org is still not in a position to make a general policy for this, so it is not implemented out of the box.
class UserManagement(object):
"""Provides methods for adding deleting and changing users
This is an implementation of IUserManagement from PlonePAS
"""
implements(IMembraneUserManagement)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def doAddUser(self, login, password):
"""This can't be done unless we have a canonical place to store users
some implementations may wish to define one and implement this.
"""
raise NotImplementedError
def doChangeUser(self, login, password, **kw):
self.context.setPassword(password)
if kw:
self.context.edit(**kw)
def doDeleteUser(self, login):
parent = aq_parent(aq_inner(self.context))
parent.manage_delObjects([self.context.getId()])
That's it! Through these adapters, the three b-org content types are able to act as sources of groups and users. Hopefully, you will appreciate the flexibility of the separation of concerns into adapters for things like editing user properties, determining user id, calculating roles and performing authentication. If you extend b-org, you can provide a more specific adapter to any of the above interfaces to customise the membership behaviour.
2.5.5.11. Writing a custom PAS plug-in
Projects require that members are given particular local roles within a project space. This is achieved using a custom PAS plug-in.
PAS was introduced in the previous section on membrane. Truth be told, it can be a bit of a jungle of plug-ins and delegation because it is so very generic. Luckily, Plone (and membrane) takes care of most of the complexity for us. Sometimes, however, it is desirable to influence the authentication and role management at a lower level.
Workspace adapters
b-org ships with a bit of framework, adapted from some similar code in an unreleased version of teamspace by Wichert Akkerman, which can provide local roles in a "workspace" - in this case a Project. It relies on an adapter to the IWorkspace interface to determine the mapping of users and roles in the particular context. Before showing how this plug-in is written and registered, however, let's look at how it is used by a Project.In membership/project.py you will find:
class LocalRoles(object):This queries the lists of managers and members assigned (by reference) to the project and specifies that both managers and members should get the role TeamMember and managers should also get the role Manager.
"""Provide a local role manager for projects
"""
implements(IWorkspace)
adapts(IProjectContent)
def __init__(self, context):
self.context=context
def getLocalRoles(self):
project = IProject(self.context)
roles = {}
for m in project.getManagers():
roles[m.id] = ('Manager',)
for m in project.getMembers():
if m.id in roles:
roles[m.id] += ('TeamMember',)
else:
roles[m.id] = ('TeamMember',)
return roles
def getLocalRolesForPrincipal(self, principal):
r = self.getLocalRoles()
return r.get(principal, ())
As it turns out, this behaviour is also useful in Departments, which can be given one or more department managers by reference. The idea is that department managers should be allowed to add and remove Employees within that Department (recall that Department is a folderish container for Employee objects). The analogous adapter in membership/department.py reads:
class LocalRoles(object):Thus, a container wanting to use the PAS plug-in we're about to see to manage local roles only need to be adaptable to IWorkspace. In fact, this whole machinery ought to be factored out into a separate component, possibly sharing code to teamspace, another product which provides similar functionality. Mostly, this is down to laziness - creating another product (with all its boilerplate) and managing another dependency in the Products folder seemed too onerous when b-org was being developed. Hopefully, with Zope 2.10/Plone 3.0 and a growing preference for plain-Python packages and "eggs", it will seem a little less of an obstacle to split products up into multiple smaller pieces. So much for making excuses.
"""Provide a local role manager for departments
"""
implements(IWorkspace)
adapts(IDepartmentContent)
def __init__(self, context):
self.context = context
def getLocalRoles(self):
project = IDepartment(self.context)
roles = {}
for m in project.getManagers():
roles[m.id] = ('Manager',)
return roles
def getLocalRolesForPrincipal(self, principal):
r = self.getLocalRoles()
return r.get(principal, ())
The plug-in
The PAS plug-in that uses the IWorkspace interface can be found in pas/localrole.py. It looks like this:# Borrowed from Project pasification branch - written primarily byOn first glance, there is quite a lot going on here, but it is not so hard to understand. First, we define a good old-fashioned Zope 2 factory and ZMI add form. This is good practice, because PAS plug-ins can be managed via acl_users in the ZMI. If you find yourself wandering there, however, remember to bring a torch and keep a trail of breadcrumbs to find your way out. A backup wouldn't hurt either if you try to change things. It is, unfortunately, not the most intuitive of interfaces.
# Wichert Akkerman and Copyright Amaze Internet Services
# This module is releasd under the Zope Public License
from sets import Set
from Globals import InitializeClass
from Acquisition import aq_inner, aq_chain, aq_parent
from AccessControl import ClassSecurityInfo
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Products.PluggableAuthService.utils import classImplements
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from Products.PlonePAS.interfaces.plugins import ILocalRolesPlugin
from Products.borg.interfaces import IWorkspace
manage_addWorkspaceLocalRoleManagerForm = PageTemplateFile(
"../zmi/WorkspaceLocalRoleManagerForm.pt", globals(),
__name__="manage_addProjectRoleManagerForm")
def manage_addWorkspaceLocalRoleManager(dispatcher, id, title=None, REQUEST=None):
"""Add a WorkspaceLocalRoleManager to a Pluggable Authentication Services."""
plrm = WorkspaceLocalRoleManager(id, title)
dispatcher._setObject(plrm.getId(), plrm)
if REQUEST is not None:
REQUEST.RESPONSE.redirect(
'%s/manage_workspace?manage_tabs_message=WorkspaceLocalRoleManager+added.'
% dispatcher.absolute_url())
class WorkspaceLocalRoleManager(BasePlugin):
meta_type = "Workspace Roles Manager"
security = ClassSecurityInfo()
def __init__(self, id, title=None):
self.id = id
self.title = title
#
# ILocalRolesPlugin implementation
#
security.declarePrivate("getRolesInContext")
def getRolesInContext(self, user, object):
roles = []
uid = user.getId()
obj, workspace = self._findWorkspace(object)
if workspace is not None:
if user._check_context(obj):
roles.extend(workspace.getLocalRolesForPrincipal(uid))
return roles
security.declarePrivate("checkLocalRolesAllowed")
def checkLocalRolesAllowed(self, user, object, object_roles):
roles = []
uid = user.getId()
obj, workspace = self._findWorkspace(object)
if workspace is not None:
if not user._check_context(obj):
return 0
roles = workspace.getLocalRolesForPrincipal(uid)
for role in roles:
if role in object_roles:
return 1
return None
security.declarePrivate("getAllLocalRolesInContext")
def getAllLocalRolesInContext(self, object):
rolemap = {}
obj, workspace = self._findWorkspace(object)
if workspace is not None:
localRoleMap = workspace.getLocalRoles()
for (principal, roles) in localRoleMap.items():
rolemap.setdefault(principal, Set()).update(roles)
return rolemap
# Helper methods
security.declarePrivate("_findWorkspace")
def _findWorkspace(self, object):
"""Find the first workspace, if any, in the acquistion chain of this
object. Returns a tuple obj, workspace where workspace is the adapted
IWorkspace.
"""
for obj in self._chain(object):
workspace = IWorkspace(obj, None)
if workspace is not None:
return obj, workspace
return None, None
security.declarePrivate("_chain")
def _chain(self, object):
"""Generator to walk the acquistion chain of object, considering that it
could be a function.
"""
# Walk up the acquisition chain of the object, to be able to check
# each one for IWorkspace.
# If the thing we are accessing is actually a bound method on an
# instance, then after we've checked the method itself, get the
# instance it's bound to using im_self, so that we can continue to
# walk up the acquistion chain from it (incidentally, this is why we
# can't juse use aq_chain()).
context = aq_inner(object)
while context is not None:
yield context
funcObject = getattr(context, 'im_self', None)
if funcObject is not None:
context = aq_inner(funcObject)
else:
# Don't use aq_inner() since portal_factory (and probably other)
# things, depends on being able to wrap itself in a fake context.
context = aq_parent(context)
classImplements(WorkspaceLocalRoleManager, ILocalRolesPlugin)
InitializeClass(WorkspaceLocalRoleManager)
We will see how the plug-in is registered and activated in a moment, but first notice that the plug-in implements an interface, ILocalRolesPlugin, which is defined by PlonePAS, the PAS-in-Plone integration layer. This defines methods that will be called by the PAS machinery to determine, in this case, local roles. Note that this is not an adapter (perhaps it would have been if PAS had been invented in Zope 3, though Zope 3 has its own authentication machinery that is evolved from PAS and works slightly differently). When created, the ProjectLocalRoleManager is an Zope 2 object that lives in the ZODB in acl_users.
The methods of the ILocalRolesPlugin interface are fairly self-explanatory in purpose. They allow PAS to extract the local roles for a particular user in a particular context (getRolesInContext()), to check whether a user in fact has one of the roles required to access a particular method attribute in a particular context (checkLocalRolesAllowed()), and to get a map of users-to-roles in a particular context.
The complex parts are, as often is the case, concerned with acquistion. The helper method _findWorkspace() attempts to walk up the object hierarchy to find the first possible IWorkspace (it will only consider one) to get hold of the appropriate IWorkspace adapter that is then used to determine the actual roles that apply, as above. Without walking up the content hierarchy, it would not be possible to let the local roles of a particular project apply when in the context of a piece of content inside that project (i.e. a sub-object of the folderish Project object). There is some reasonably hairy acqusition-juggling going on in the _chain() method to return this chain as a generator. The hairiness comes from the fact that the thing that is being checked may in fact be a method that is being accessed, and aqusition chains can get themselves in all kinds of knots, especially when Five is in the mix.
Lastly, we need to declare a ClassSecurityInfo and call InitializeClass to get Zope 2 to play ball.
Registering the plug-in
To be able to use this plug-in, we must first register it with PAS. This is done when the product is loaded, in borg/__init__.py:from Products.PluggableAuthService import registerMultiPluginThis is similar to how CMF content types are initialised with ContentInit().initialize() and context.registerClass(). In other words, copy-and-paste and the less you know the happier you will be.
...
from pas import localrole
...
registerMultiPlugin(localrole.WorkspaceLocalRoleManager.meta_type)
def initialize(context):
context.registerClass(localrole.WorkspaceLocalRoleManager,
permission = AddUserFolders,
constructors = (localrole.manage_addWorkspaceLocalRoleManagerForm,
localrole.manage_addWorkspaceLocalRoleManager),
visibility = None)
...
By registering the plug-in, we could now ask our users to instantiate a Workspace Roles Manager within acl_users.... er... somwhere. Like we said - not necessarily obvious. Better to do it once, in the setup code for b-org. Please refer to the section on GenericSetup to learn how b-org is actually installed, but notice that the relevant code is in setuphandlers.py:
from Products.CMFCore.utils import getToolByNameAll we do here is get hold of the factory dispatcher for the user folder (from manage_addProduct, which has something to do with that registerClass call for the WorkspaceLocalRoleManager seen in the previous code example, but like we said, it's dont-ask, don't-tell) and if it is not there already, we create an instance of the plugin using the factory. We then need to activate it so that it actually takes effect. out is a StringIO output stream used for logging.
from Products.PlonePAS.Extensions.Install import activatePluginInterfaces
from config import LOCALROLES_PLUGIN_NAME
...
def setupPlugins(portal, out):
"""Install and prioritize the project local-role PAS plug-in.
"""
uf = getToolByName(portal, 'acl_users')
borg = uf.manage_addProduct['borg']
existing = uf.objectIds()
if LOCALROLES_PLUGIN_NAME not in existing:
borg.manage_addWorkspaceLocalRoleManager(LOCALROLES_PLUGIN_NAME)
print >> out, "Added Local Roles Manager."
activatePluginInterfaces(portal, LOCALROLES_PLUGIN_NAME, out)
2.5.5.12. Placeful workflow
b-org uses CMFPlacefulWorkflow, which ships with Plone 2.5, to manage the workflow of content objects inside a project.
Placeful workflows are based on the concept of policies. You can think of a policy as a mapping of workflows to types, in the same way as you could control from the portal_workflow tool. Policies are created, normally by copying an existing policy (possibly the default, global policy), and then applied to a context. In Plone, this can be done using the policy option in the state menu.
Placeful workflows are used in b-org Projects. Inside a project, project members should have elevated view and modify permissions over content. This is achieved using the following technique:
- A new role TeamMember is made available within any Project.
- A custom workflow, borg_project_default_workflow is a customisation of the default Plone workflow that has a simplified set of states and actions, and is aware of the TeamMember role.
- A placeful workflow policy sets the default workflow, as well as the workflow for folders, to this one.
- When a Project is created, this placeful workflow policy is enabled for the project.
The custom workflow is defined using GenericSetup, in profiles/default/workflows/borg_project_default_workflow/definition.xml. You can of course install your own workflow if necessary. The workflow policy is set up in the importVarious setup step, in setuphandlers.py:
from Products.CMFCore.utils import getToolByName
from config import LOCALROLES_PLUGIN_NAME, PLACEFUL_WORKFLOW_POLICY
...
def addProjectPlacefulWorkflowPolicy(portal, out):
"""Add the placeful workflow policy used by project spaces.
"""
placeful_workflow = getToolByName(portal, 'portal_placeful_workflow')
if PLACEFUL_WORKFLOW_POLICY not in placeful_workflow.objectIds():
placeful_workflow.manage_addWorkflowPolicy(PLACEFUL_WORKFLOW_POLICY,
duplicate_id='portal_workflow')
policy = placeful_workflow.getWorkflowPolicyById(PLACEFUL_WORKFLOW_POLICY)
policy.setTitle('[borg] Project content workflows')
policy.setDefaultChain(('borg_project_default_workflow',))
policy.setChainForPortalTypes(('Folder', 'Large Plone Folder',),
('borg_project_default_workflow',))
Again, you could add a different policy if you needed different settings.
Finally, we apply the policy when a project is created. We will see how this is set up when events are covered in the next section, but the relevant code is in events/project.py:
from zope.interface import implements
from zope.component import getUtility
from Products.CMFCore.utils import getToolByName
from Products.borg.config import PLACEFUL_WORKFLOW_POLICY
from Products.borg.interfaces import ILocalWorkflowSelection
class DefaultLocalWorkflowSelection(object):
"""Select the default local workflow policy.
Local adapters or overrides may supercede this.
"""
implements(ILocalWorkflowSelection)
workflowPolicy = PLACEFUL_WORKFLOW_POLICY
def addLocalProjectWorkflow(ob, event):
"""Apply the local workflow for project spaces when a project is added.
"""
# Add the TeamMember role if necessary
if 'TeamMember' not in ob.validRoles():
# Note: API sucks :-(
ob.manage_defined_roles(submit='Add Role',
REQUEST={'role': 'TeamMember'})
# Find out which workflow to use - this is looked up as a utility so
# that other components can override it.
workflowSelection = getUtility(ILocalWorkflowSelection, context=ob)
# Set the placeful (local) workflow
placeful_workflow = getToolByName(ob, 'portal_placeful_workflow')
ob.manage_addProduct['CMFPlacefulWorkflow'].manage_addWorkflowPolicyConfig()
config = placeful_workflow.getWorkflowPolicyConfig(ob)
config.setPolicyBelow(policy=workflowSelection.workflowPolicy)
Here, the local role is added to the newly created project instance (it is not made global so as not to pollute the global roles list), and the policy is associated with the contents of the (folderish) project object.
Note that we do not hard-code the name of the workflow policy! Instead, we ask a utility called ILocalWorkflowSelection. This could be overridden using a local utility, but the global one references the policy created above, as defined in DefaultLocalWorkflowSelection. This utility is registered in events/configure.zcml as follows:
<utility provides="..interfaces.ILocalWorkflowSelection"
factory=".project.DefaultLocalWorkflowSelection" />
2.5.5.13. Sending and handling events
Events is undoubtedly one of the most useful things that Zope 3 brings to the Zope 2 world. Here's how b-org uses them.
In the previous section, you saw how an event handler was used to apply a placeful workflow policy to newly created projects. This pattern is quite powerful - instead of needing to subclass Project just to add something to at_post_create_script() or initializeArchetype(), say, you simply register an appropriate event handler. This pattern can of course apply to other situations, such as when objects are modified, deleted, added to a container, or on any other type of event that may occur in your system. Events are synchronous, so when code emits an event, it will block until all event handlers are finished.
Recall the event handler for adding projects. It can be found in events/project.py and has the following signature:
def addLocalProjectWorkflow(ob, event):
...
The first argument is the object the event was fired on, the second is an instance of the event itself. In fact, this two-part event dispatcher is a special case of events described with IObjectEvent and its sub-interfaces. Internally, Zope 3 catches all IObjectEvents and re-dispatches the event based on the object that is passed along the event instance. The registration for the event handler in events/configure.zcml looks like this:
<subscriber for="..interfaces.IProjectContent
zope.app.container.interfaces.IObjectAddedEvent"
handler=".project.addLocalProjectWorkflow" />
Note that there are two interfaces the subscriber is registered for - the object type and the event type. These must be separated by whitespace, though a newline like above is customary. This is the same syntax that is used to explicitly define multi-adapters (if you are not using the adapts() syntax in an adapter class) - in fact, the events machinery uses the adapter registry internally to map subscribers to events when they are fired.
A more general-case event can be found in events/employee.py, which takes care of assigning ownership of an Employee object to the user that is tied to that employee. The code is borrowed and adapted from PloneTool, but notice the signature which only includes the event:
def modifyEmployeeOwnership(event):
"""Let employees own their own objects.
Stolen from Plone and CMF core, but made less picky about where users are
found.
"""
The registration in events/onfigure.zcml is similar to the one above, but only uses one for interface:
<subscriber for="..interfaces.IEmployeeModifiedEvent"
handler=".employee.modifyEmployeeOwnership" />
Sending custom events
You will notice that the IEmployeeModifiedEvent is a custom event. In Plone 3.0 (or rather, Archetypes 1.5) this won't be necessary, because Archetypes will take care of sending an event derived from IObjectModifiedEvent, which in turn derives from IObjectEvent and thus is subject to the same registration as the IObjectAddedEvent that includes the object type and the event type. For now, though, we need to send the event ourselves.
The event is described by an interface in interfaces/employee.py:
from zope.interface import Interface, AttributeThe implementation is trivial, and can be found in content/employee.py:
...
class IEmployeeModifiedEvent(Interface):
"""An event fired when an employee object is saved.
"""
context = Attribute("The content object that was saved.")
from zope.interface import implementsIt is of course the event class that we instantiate and send, whilst we register the event handler for the event interface. This means that we could provide alternative implementations for the same event interface, if need be. It also means that event handlers subscribed for a parent interface will be invoked for events that provide a sub-interface.
...
from Products.borg.interfaces import IEmployeeModifiedEvent
...
class EmployeeModifiedEvent(object):
"""Event to notify that employees have been saved.
"""
implements(IEmployeeModifiedEvent)
def __init__(self, context):
self.context = context
Sending the event is very simple. In the definition of Employee in content/employee.py, we have:
from zope.event import notify
...
class Employee(ExtensibleSchemaSupport, BaseContent):
...
Â
security.declarePrivate(permissions.View, 'at_post_create_script')
def at_post_create_script(self):
"""Notify that the employee has been saved.
"""
notify(EmployeeModifiedEvent(self))
security.declarePrivate(permissions.View, 'at_post_edit_script')
def at_post_edit_script(self):
"""Notify that the employee has been saved.
"""
notify(EmployeeModifiedEvent(self))
We construct an event instance and parameterise it with the right object (i.e. self) before sending it with notify(), all on one line.
2.5.5.14. Annotations
Annotations are an elegant solution to the "where do I store this?" problem, and are used in many Zope 3 applications.
It is often useful to be able to attach information to an object even if you don't have control over that object's type and schema. For example, a tagging solution may attach a list of tags to an object, or a notification tool may want to add a list of subscribers on a per-object basis. This is known in Zope 3 as "annotations".
Annotations work like this:
- A marker interface, normally IAttributeAnnotatable is applied to the class or object that is to be annotated. This particular marker means that annotations are stored in a persistent dict called __annotations__ that is added to the object, though this should be considered an implementation detail.
- An adapter exists from IAttributeAnnotable to IAnnotations. If you need a different annotation regime (e.g. one that stores the values keyed by object id in some local utility) you could provide a different adapter to IAnnotations.
- The code that wishes to annotate an object will adapt it to IAnnotations. The annotations adapter acts like a dict. Conventionally, each package that uses annotations will store all its (arbitrary) information under a particular key in that dict. The key name is normally the same as the name of the package. This is mainly to avoid conflicts between different packages annotating a particular object.
In b-org, we don't have quite the same need for annotating objects from other parts of Plone, but we use annotations to store users' passwords. This ensures that they cannot be accessed through-the-web (since Zope 2 won't publish the __annotations__ dict, as it begins with an underscore) and keeps passwords out of the way. Strictly speaking, this is probably overkill since the password is also hashed using the SHA1 one-way hasing algorithm, but that never stopped anyone before.
First, look at the definition of the Employee class in content/employee.py:
from zope.app.annotation.interfaces import IAttributeAnnotatable, IAnnotations
...
class Employee(ExtensibleSchemaSupport, BaseContent):
...
implements(IEmployeeContent,
IUserAuthProvider,
IPropertiesProvider,
IGroupsProvider,
IGroupAwareRolesProvider,
IAttributeAnnotatable)
Here, we explicitly say that Employee is attribute annotatable. Of course, this requires control over the class. If you are trying to annotate another type that isn't already marked as annotatable, you may be able to add the marker interface using classProvides() or directlyProvides() from zope.interface, or use the ZCML <implements /> directive. You need to be a bit careful, though, since the thing you are annotating should probably be persistent. You should also be polite - you're stuffing your own information onto someone else's object. Try not to break it.
Further down in content/employee.py, you will see the annotation being set:
security.declareProtected(permissions.SetPassword, 'setPassword')
def setPassword(self, value):
if value:
annotations = IAnnotations(self)
annotations[PASSWORD_KEY] = sha(value).digest()
PASSWORD_KEY comes from config.py, and is simply a string. The digest is verified in membership/employee.py, in the IUserAuthentication adapter:
class UserAuthentication(object):
"""Provide authentication against employees.
"""
implements(IUserAuthentication)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
def getUserName(self):
return self.context.getId()
def verifyCredentials(self, credentials):
login = credentials.get('login', None)
password = credentials.get('password', None)
if login is None or password is None:
return False
digest = sha(password).digest()
annotations = IAnnotations(self.context)
passwordDigest = annotations.get(PASSWORD_KEY, None)
return (login == self.getUserName() and digest == passwordDigest)
That's all there is to it. We get an IAnnotations adapter, and then look up the PASSWORD_KEY to find the digest. The annotations adapter has the same contract as a Python dict, so we can use functions like get() and setdefault().
2.5.5.15. Zope 3 Views
One of the nicest things that Zope 3 brough us is a way to manage view logic.
In Zope 2, a view (be that a view of a content object, or a more standalone template) typically consists of a Zope Page Template that pulls in data from its context. The problem is that non-trivial templates usually require some kind of "view logic" or "display logic". People tend to put these in a few places:
- Complex python: expressions in the ZPT. This is bad because it makes your templates hard to understand, and because there is a limit to what you can do with one-line Python expressions.
- External Python Scripts in a skin layer that get acquired in the page template, e.g. here/calculateDate. This is bad because it is cumbersome to create a new file for something which may be quite trivial, because all such scripts are part of a global namespace (and thus there may be conflicts between two different scripts with the same name), and also because Python scripts in the skin layers (and python: expressions) are slower than filesystem Python code and more restricted.
- A custom tool that provides some necessary functionality. This is bad because a tool is a singleton, so you will probably need to explicitly pass around a context. Tools are also part of that same global namespace (by way of acquisition from the portal root), and are a hassle to create and install.
- Methods on the context content object (where applicable). This is bad because it mixes presentation logic and the model (the schema) and storage logic. This often leads to an explosion of methods on each content type that are highly specific to a particular template. This pattern also requiers that you have the ability to add new methods to the content type class, even if you are just adding a new view template for it.
As usual, these problems indicate a lack of separation of concerns. Zope 3's answer is a view - a class (typically) which may be associated with a template.
Views are multi-adapters
You will often hear that views are named multi-adapters of a context and a request. In fact, the concept of a multi-adapter originated in the need for views. For most practical purposes, you can forget about this - it is an implementation detail. However, you may sometimes need to look up views yourself, which can be done using:
from zope.component import getMultiAdapter myView = getMultiAdapter((context, request), name='my_view')
More importantly, you need to know that to access the context the view is operating on inside that view, you can use self.context, and to access the request (including form variables submitted as part of that request, if applicable), using self.request.
Explicitly acquiring views
One of the easiest ways of using views with existing code is to make page templates in a skin layer as you normally would, and then acquire a view object that is used for rendering logic. One of the main reasons for using this approach is that it allows page templates to be customised using the normal skin layer mechanism. This is approach is used extensively in Plone 2.5. Here's an example from the "recent" portlet, starting with portlet_recent.pt:
...
<tal:recentlist tal:define="view context/@@recent_view;
results view/results;">
...
<tal:items tal:repeat="obj results">
...
</tal:items>
...
</tal:recentlist>
The important line here is context/@@recent_view. This will look up a view named recent_view relative to the current context (context in page templates is a now-preferred alias for the here variable that was used before - here still works in Zope 2 templates, but is gone in Zope 3).
This view is defined by a class and a ZCML directive. The ZCML directive looks like this:
<browser:view
for="*"
name="recent_view"
class=".portlets.recent.RecentPortlet"
permission="zope.Public"
allowed_attributes="results"
/>
Actually, this is not exactly what's in the file in Plone, since Plone is working around a few Zope 2.8 issues, but basically, this says that the view is available on all types of contexts (for="*" - this could specify a dotted name to an interface if needed, more on that below), has the name recent_view, is public (because of the magic permission zope.Public) and that when acquired, the attribute (method) results is allowed - more attributes could be specified separated by whitespace. The class that is referenced contains the view implementation. Here it is, again slightly modernised:
from Products.Five.browser import BrowserView
from Products.CMFCore.utils import getToolByName
from Acquisition import aq_inner
class RecentPortlet(BrowserView):
"""The recent portlet
"""
def results(self):
"""Get the search results
"""
context = aq_inner(self.context)
putils = getToolByName(context, 'plone_utils')
portal_catalog = getToolByName(context, 'portal_catalog')
typesToShow = putils.getUserFriendlyTypes()
return self.request.get(
'items',
portal_catalog.searchResults(sort_on='modified',
portal_type=typesToShow,
sort_order='reverse',
sort_limit=5)[:5])
The use of aq_inner() on self.context is not strictly necessary always, but is a useful rule of thumb to make acquisition do what you expect it to do (this is because the BrowserView base class extends Acquisition.Explicit, which causes self.context to gain an acquistion wrapper that can mess with its acqusition chain).
Views with templates
Zope 3 does not use views in this way. Instead, you would bind the template to the browser view explicitly. The main drawback of this technique is that the template is not present in the portal_skins tool, and so cannot be customised through-the-web. This may be possible in future versions of Zope and CMF, but for now the full-blown view technique is best used when it is not necessary to customise views through-the-web. Of course, you can still override view registrations using ZCML on more specific interfaces or an overrides.zcml.
Here is a view for departments in the charity example product, under charity/browser/configure.zcml. Notice how this entire XML file is in the browser namespace, and thus it is unnecessary to prefix each directive with browser:
<configure xmlns="http://namespaces.zope.org/browser"
i18n_domain="charity">
<page
name="charity_department_view"
for="Products.borg.interfaces.IDepartmentContent"
class=".department.DepartmentView"
template="department.pt"
permission="zope2.View"
/>
...
</configure>
Here, we explicitly state that this view is only available for IDepartmentContent objects. This means that if you try to invoke @@charity_department_view on anything that does not provide this interface, you will get a lookup error. The view is protected by the Zope 2 View permission. Also note that there is no allowed_attributes (or allowed_interface) attribute here. This is because the view is not intended to be used by other templates (if they tried, they would get an Unauthorized error when trying to access any attribute of the view) - all the logic is in the department.pt template.
The department.pt template is found in charity/browser, the same directory as the configure.zcml file above. You can use relative paths like ./templates/... if necessary to point to the template file on the filesystem. Here is the class:
from Products.Five.browser import BrowserView
from Products.borg.interfaces import IDepartment
class DepartmentView(BrowserView):
"""A view of a charity department"""
def __init__(self, context, request):
self.context = context
self.request = request
def name(self):
return self.context.Title()
def managers(self):
return self.context.getManagers()
def details(self):
return self.context.Description()
And here is the template that uses these methods:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en"
metal:use-macro="here/main_template/macros/master"
i18n:domain="charity">
<body>
<metal:main fill-slot="main">
<div metal:use-macro="here/document_actions/macros/document_actions">
Document actions (print, rss etc)
</div>
<h1 class="documentFirstHeading" tal:content="view/name" />
<table class="listing vertical" style="float:right" tal:condition="view/managers">
<tr>
<th>Manager(s)</th>
<td>
<div tal:repeat="obj view/managers">
<a href="#" tal:attributes="href obj/absolute_url" tal:content="obj/Title" />
</div>
</td>
</tr>
</table>
<div tal:content="structure view/details" />
<metal:listing use-macro="here/folder_listing/macros/listing" />
<div class="visualClear"><!----></div>
</metal:main>
</body>
</html>
Now, you can go to a hypothetical URL /mydept/@@charity_department_view to see this view rendered. In fact, this is set as the view and (Default) aliases for the Department content type when charity is installed, so the user will never see this URL.
Views without templates
It is also possible to make views without templates. This is useful if you need a URL to submit that does some processing. That processing would normally be done in the __call__() method, as in the hypothetical example below:
<browser:view
name="modify_customer"
for=".interfaces.ICustomer"
class=".customer.ModifyCustomerView"
permission="cmf.ModifyPortalContent"
/>
Now, we could write a form that has action="@@modify_customer", which would result in this being called:
class ModifyCustomerView(BrowserView):
"""Modify a customer from a form
"""
def __call__(self):
name = self.request.form.get('name', None)
dog = self.request.form.get('dog', None)
self.context.name = name
self.context.dog = dog
self.request.response.redirect('@@customer_view')
This is obviously a simplified example, but the important thing to realise is that the view will tend to use self.context and self.request to interact with the rest of the portal.
3. Using ArchGenXML
ArchGenXML is a code-generator for CMF/Plone applications (Products) based on the Archetypes framework. It parses UML models in XMI-Format (.xmi, .zargo, .zuml), created with applications such as ArgoUML, Poseidon or ObjectDomain. This tutorial will help you get started developing applications with the aid of ArchGenXML.
3.1. Getting Started
Introduction, Installation and Hello World.
3.1.1. Introduction
An introduction to ArchGenXML - what it is, reasons to use it, who made it.
What is ArchGenXML
With ArchGenXML you can create working python code without writing one single line of python. It is a command-line utility that generates fully functional Zope Products, based on the Archetypes framework, from UML models using XMI (.xmi, .zargo, .zuml) files. The most common use-case is to generate a set of custom content types and folders, possibly with a few tools, a member type and some workflows thrown in.
In practice, you draw your UML diagrams in a tool like ArgoUML or Poseidon which has the ability to generate XMI files. Once you are ready to test your product, you run ArchGenXML on the XMI file, which will generate the product directory. After generation, you will be able to install your product in Plone and have your new content types, tools and workflows available.
At present, round-trip support is not implemented: custom code can't be converted back into XMI (and thus diagrams). However, you can re-generate your product over existing code; method bodies and certain "protected" code sections will be preserved. This means that you can evolve your product's public interfaces, its methods and its attributes in the UML model, without fear of losing your hand-written code.
ArchGenXML is hosted at svn.plone.org as a subproject of the Archetypes project. It is released under GNU General Public Licence 2 or later.

Why should I use ArchGenXML?
Major reasons:
- You want to save time
- You are a lazy programmer
- You don't like to reinvent the wheel
- You don't like copying and pasting code (and bugs)
- You make heavy use of references and interfaces
- You have big projects with many different custom types
- You want or need a well-documented interface to your product
- You like structured model- and pattern-driven software development
- You want to maintain your project in future without getting a headache
and many other good and odd reasons.
Contributors
The project was initially started by Phil Auersperg. Thanks to his laziness :-)
Authors
- Phil Auersperg (Project Leader)
- BlueDynamics Alliance, Auersperg-Castell KEG, phil@bluedynamics.com,
- Jens Klein (Release Manager, Developer and Doc-Writer)
- BlueDynamics Alliance, Klein & Partner KEG, jens@bluedynamics.com,
- Reinout van Rees (Co-Release Manager, Developer and Doc-Writer)
- ZestSoftware
- Fabiano Weimar dos Santos (Ideas, Testing, Bugfixing, Workflow)
- Weimar Desenvolvimento e Consultoria em Informatica Ltda., xiru@xiru.org,
- Martin Aspeli (Improvements, bug fixes and documentation)
- Martin Aspeli
- Robert Niederreiter (Lots of base work on AGX 2.0)
- Robert Niederreiter
- and others
- thanks to everybody who contributed with testing, doc-writing or code-pieces!
Sponsors
- Xiru.org, Brazil (Fabiano Weimar dos Santos) sponsors a valuable amount of money into workflow support (State diagrams -> DCWorkflow, will go into release 1.2),
- PilotSystems, Paris, France (David Sapiro),
- OpenSource.ag, Innsbruck, Austria (Georg Pleger).
If you want to contribute ArchGenXML by improving the code, helping with documentation or sponsoring money to make us improve it, please contact one of us.
3.1.2. Installation
How to install ArchGenXML and get up and running.
Installation
ArchGenXML has a dependency on some zope 3 eggs. To not mess up your global site-packages directory, using buildout or virtualenv is recommended.
Note: In an older version of AGX, a Zope 3 installation could be configured in a ~/.agx_zope_path file. This case is not supported anymore. You can delete this file if you have it.
On Windows, I assume you installed Python 2.4.4 from the msi installer and installed it in the default location. If you have not already done, configure the Path environment variable to include your python path and scripts directory. For this, got to Control Panel, Advanced, Environment Variables, edit Path, append ;C:\\Python24;C:\\Python24\Scripts to the existing string.
Installing stable version
Attention: adding archgenxml egg to a Plone buildout is not supported! Plone 3.x buildout is shipped with old Zope 3.3. ArchGenXML depends on latest version of zope 3 eggs. So please create a buildout only for archgenxml like described below.
If you want to install archgenxml via buildout (recommended), read Using buildout and skip the Using easy_install in a virtualenv part.
It can happen ArchGenXML version on plone.org is older than Pypi because the release manager forgot to upload it on plone.org or for another reason. easy_install and buildout will get by default the latest ArchGenXML version on Pypi, so it's fine.
Using buildout
Create a fresh directory and go into it:
$ mkdir archgenxml_buildout $ cd archgenxml_buildout
Download the normal bootstrap.py and put it in this directory. You can copy an existing bootstrap.py file of one of your buildout, it's the same file.
Then create a buildout.cfg file in this directory with the following snippet:
[buildout]
parts =
archgenxml
[archgenxml]
recipe = zc.recipe.egg:scripts
eggs = archgenxml
Finally bootstrap the buildout and run it:
$ python bootstrap.py $ ./bin/buildout
On Windows, it's bin\buildout, you have to replace '/' by '\' in all following examples.
archgenxml is now available in ./bin/archgenxml.
To update archgenxml later, go in your directory and run buildout again:
$ ./bin/buildout
It will download latest version of archgenxml all all its dependencies.
I call below <path to archgenxml> the archgenxml_buildout directory.
Using easy_install in a virtualenv
If you don't want to use buildout, you can use virtualenv to create an isolated environment. You have to install setuptools egg to have the easy_install command available. On Ubuntu you can do it with apt-get install python-setuptools. On Windows, go to the setuptools pypi page, download the exe which match the Python version you are using and execute it to install it.
Install virtualenv with easy_install:
$ easy_install virtualenv
On Windows easy_install.exe is in C:\Python24\Scripts, so you have to invoke it with the full path if you don't have added this directory in your PATH.
Create the virtualenv with --no-site-packages option to not see packages installed globally:
$ virtualenv --no-site-packages agx $ cd agx/ $ source bin/activate $ easy_install archgenxml
Everytime you want use archgenxml, you have to go in the agx directory and type source bin/activate to activate the environment. To deactivate the environment, type deactivate.
To update archgenxml, you have to update each egg, the most important one are archgenxml and xmiparser:
$ easy_install -U archgenxml $ easy_install -U xmiparser
If you have a problem with archgenxml, please be sure to recreate the virtualenv completly so you have latest versions of all eggs before asking on the archetypes-users mailing-list.
I call below <path to archgenxml> the path to the virtualenv agx directory.
Installing development version
ArchGenXML's svn trunk is for the 2.x development version.
As usual, the goal is to keep trunk workable. Some of the developers work and develop on the edge: trunk.
If you need stability, use the latest release.
The only supported way to use ArchGenXML trunk is with buildout. You install it like this:
$ svn co https://svn.plone.org/svn/archetypes/ArchGenXML/buildout archgenxml_buildout $ cd archgenxml_buildout $ python bootstrap.py $ bin/buildout
To update your buildout:
$ cd archgenxml_buildout $ svn up $ bin/buildout
If you are intersted in AGX 3 development, see http://dev.plone.org/archetypes/browser/AGX
I call below <path to archgenxml> the archgenxml_buildout directory.
Get the ArchGenXML profile
The archgenxml_profile.xmi file contains information about stereotypes, fields, and other stuff that AGX needs to generate valid Python code from your model. You can get the profile from subversion.
Or you can regenerate it with <path to archgenxml>/bin/agx_argouml_profile. A archgenxml_profile.xmi file is generated in the current directory.
Create a "<path to archgenxml>/profiles" directory and put the file here.
Note: In an older version of AGX, this file was called 'argouml_profile.xmi'. You should not use it with ArgoUML > 0.24.
Troubleshooting
On Windows, you may have to install and configure the mingw32 compiler to compile the zope.proxy egg, an indirect dependency of archgenxml. Now zope.proxy eggs are built for Windows, so you should not have this problem anymore.
Support
For any questions or problems, please ask on the archetypes-users mailing-list. Please don't use comments on the manual pages. Not everybody is alerted when a comment is added.
If you want to contribute to this documentation, please post on the plone-docs mailing-list.
3.1.3. Configure ArgoUML to use the archgenxml profile
ArgoUML can load a "definition" file with predefined tagged values and stereotypes. So you have all the custom tagged values and stereotypes used by ArchGenXML available in dropdown menus.
Background and Notes
The screenshot below shows ArgoUML with the custom tags from the definition file. Having many of the possible options available from a menu has the following benefits:
- saves time - sometimes clicking is faster than typing
- reduces errors - no more mispelled tags
- presents a learning opportunity - seeing new tags and stereotypes provides a springboard for further investigation

ArgoUML with ArchGenXML profile
This walks you through how a setup of ArgoUML on an Ubuntu desktop system. The methodology should be easily adjusted to other platforms.
Assumptions
These are my assumptions:
You already have latest sun java JRE installed. You can get it on Ubuntu with:
apt-get install sun-java6-jdk
If:
update-alternatives --list java
don't give you /usr/lib/jvm/java-6-sun/jre/bin/java, you can set java sun as the default java implementation like this:
update-alternatives --set java /usr/lib/jvm/java-6-sun/jre/bin/java
You have copied archgenxml_profile.xmi in the <path to archgenxml>/profiles directory like described in the previous section.
Install ArgoUML
Go to http://argouml.tigris.org and download the latest stable version of ArgoUML (0.28 when these lines are written). Download the exe for Windows, the tar.gz archive for Linux.
On Windows, execute the downloaded exe to install it.
On Linux, unpack the archive somewhere, a good location is '/opt':
$ sudo mkdir /opt $ cd /opt $ sudo tar xvf /tmp/ArgoUML-0.28.tar.gz
It should create a argouml-0.28 directory.
It is handy to symlink 'argouml.sh' to '/usr/local/bin/argouml':
$ sudo ln -s /opt/argouml-0.28/argouml.sh /usr/local/bin/argouml
Configure ArgoUML to use the definition file
Note: ArgoUML 0.24 used to use the argo.defaultModel option to specify a profile to use like this:
java -Dargo.defaultModel=<path to archgenxml>/profiles/archgenxml_profile.xmi -jar /opt/ArgoUML/argouml.jar
It's not the case anymore with latest ArgoUML version. Please read on.
Launch ArgoUML and go to Edit -> Settings... -> Profiles
Click on Add and specify your <path to archgenxml>/profiles directory.
Note: archgenxml will use the list of profiles directories configured on ArgoUML.
Close ArgoUML and launch it again.
Go again in Edit -> Settings... -> Profiles AGXProfile should be visible now in "Available Profiles".
Click on ">>" to add it to "Default profiles".
You should remove the Java profile from the "Default profiles" list to be sure to not use stereotypes and tag definitions from this one. You should only have AGXProfile and UML 1.4 in this list. You can't remove the UML 1.4 here, but you remove it by project.
The global configuration is now done.
Now for every new project you create:
- Click on the fourth icon in the toolbar to configure Profiles for this project. Delete UML 1.4. You should really only have AGXProfile is this list.
Note: if you want to generate Plone 2.5 compatible code, set on the model root the plone_target_version tag definition with value 2.5.
Why ArgoUML?
Just a few notes on why using ArgoUML.
- Mature - it supports the important stuff (at least as far as ArchGenXML goes), state diagrams, tagged-values, stereotypes.
- It is pretty lightweight, which is important when your laptop only has 256 MB of RAM and a PIII 700 Mhz processor. Poseidon Community Edition, though nice, is just too memory hungry.
- Price is good (free and open source)
3.1.4. UML
A brief introduction to UML and pointers to further readings.
UML - the Unified Modeling Language - is a graphical language designed to
describe software through diagrams. There are several different types of
diagrams available, but the ones most relevant to ArchGenXML are:
* The class diagram
* The state diagram
Class diagrams are used to draw interfaces, content types (represented as
classes) and tools (represented as classes with the 'portal_tool' stereotype),
as well as the attributes and public operations on these. In addition,
associations in the diagram show how objects are aggregated within or referenced
from one another.
The goal of model-driven development is to create the "blueprints" for your
software in a well-defined, easily-communicated format: the UML model and
diagram thereof. You can design your model using visual tools until you have a
structure which adequately represents your needs, and ArchGenXML will generate
the necessary code.
You probably have to customise that code somewhat, filling in method bodies,
creating new page templates etc., but ArchGenXML takes care of all the
boilerplate for you. With tagged values and stereotypes you can customise the
generated code with a surprising degree of flexibility and control, and when
you need to hand-code something, ArchGenXML won't overwrite your changes
(provided you stick to the protected code sections, clearly marked in the
source code).
This manual does not aim to teach you UML and object-oriented, model-driven
software development. There are several other fine manuals about that on the
web. A very good starting point is the "OMG UML Resource Page":http://www.uml.org/
including its web-links to tutorials.
For a quick-start read "Practical-UML":http://bdn.borland.com/article/0,1410,31863,00.html
chapters 'class-diagram' and 'state-chart-diagram'.
3.1.5. Hello World
Create your first minimal content type with ArchGenXML
Open the UML tool of your choice. Make a new UML model and give it a name
'HelloWorld'. Then add a class diagram (most UML tools do this automatically).
Choose the tool for class creation and add a class to the diagram. Give it a
name such as "HelloType" and add an attribute 'HelloText' with type
'string'. The "helloworld3.zargo":helloworld3.zargo (Plone 3.0) / "helloworld25.zargo":helloworld25.zargo (Plone 2.5) was created with ArgoUML
and contains the type as described above.
"Example: HelloWorld with HelloType in UML":img:helloworld.png
Generating the product
Save your model as helloworld.zargo (ArgoUML) or helloworld.zuml (Poseidon), or export it as an XMI file with the name helloworld.xmi. Then go to the command line
and execute::
$ archgenxml helloworld.zargo
ArchGenXML will begin code generation. When it completes, you will have a new
folder 'HelloWorld' on your file system. Its contents looks like::
HelloWorld/
|-- Extensions (only with plone_target_version=2.5)
| |-- Install.py
| `-- __init__.py
|-- HelloType.py
|-- __init__.py
|-- config.py
|-- configure.zcml
|-- i18n
| `-- generated.pot
|-- interfaces.py
|-- locales
|-- profiles
| `-- default
| |-- HelloWorld_marker.txt
| |-- cssregistry.xml
| |-- factorytool.xml
| |-- import_steps.xml
| |-- jsregistry.xml
| |-- metadata.xml
| |-- skins.xml
| |-- types
| | `-- HelloType.xml
| `-- types.xml
|-- profiles.zcml
|-- refresh.txt
|-- setuphandlers.py
`-- skins
|-- helloworld_images
| `-- HelloType.gif
|-- helloworld_styles
`-- helloworld_templates
Installing and using the generated product
Move the whole folder 'HelloWorld' to your Zope/Plone 3 instance's 'Products'
folder. Restart Zope, open Plone in a browser and log in as Manager. Choose
'Plone Setup' from the personal bar and choose 'Add/Remove Products'. A new
product 'HelloWorld' should now appear in the list of products
available for install. Choose it and click 'install'. Go to your personal
folder. In the list of addable items you'll find the new product as an
addable content type. Add a test instance to see if it works.
"Plone: HelloType in action":img:helloworld-plone-shot.png
Thats the trick. By the way: the title is always part of the schema. But don't worry: you can hide or recycle it later.
3.2. Basic Features
Content Types, Fields, Widgets, References and Workflow
3.2.1. Classes / Content Types
Use classes to generate content types and portal tools.
Overview
By default, when you create a class in your class diagram, it represents an
Archetypes content type. You can add operations in your model to generate
methods on the class, and attributes to generate fields in the schema. The
quick reference at the end of this tutorial will tell you which field types
you can use. You should also browse the "Archetypes quick reference
documentation":/documentation/manual/archetypes-developer-manual/fields to
see what properties are available for each field and widget type. You may set
these using tagged values (see below).
There are three basic ways in which you can alter the way your content types
are generated:
* You may set one or more stereotypes on your class, which alters the "type" of
class. A stereotype '<<portal_tool>>', for example means you are generating a
portal tool rather than just a simple content type.
* You can use tagged values in your model to configure many aspects of your
classes, their attributes and their methods. A list of recognised tagged
values acting on classes, fields and methods are found in the "quick
reference":archgenxmlquickref at the end of this tutorial.
When reading tagged values, ArchGenXML will generally treat them as strings,
with a few exceptions where only non-string values are permitted, such as the
'required' tagged value. If you do not wish your value to be quoted as a
string, prefix it with 'python:'. For example, if you set the tagged value
'default' to 'python:["high", "low"]' on a 'lines' attribute, you will get
'default=["high", "low"]' in a LinesField in your schema.
* ArchGenXML is clever about aggregation and composition. If your class
aggregates other classes, it will be automatically made into a folder with
those classes as the allowed content types. If you use composition (signified
by a filled diamond in the diagram) rather than aggregation, the contained
class will only be addable inside the container, otherwise it will be addable
globally in your portal by default.
Variants of Content Types
Simple Classes
A simple class is what we had in HelloWorld in the previous chapter.
A simple class is based on 'BaseContent' and BrowserDefault'. This is the
default if no other options override.
Folderish Classes
The easiest way to make a content type folderish is to introduce composition
or aggregation in your model - the parent class will become folderish and will
be permitted to hold objects of the child classes. You can also make a class
folderish just by giving it the '<<folder>>' stereotype. Both of these
approaches will result in an object derived from 'BaseFolder'.
You can also give a class the '<<ordered>>' stereotype (possibly in addition
to '<<folder>>') in order to make it derive from 'OrderedBaseFolder' and thus
have ordering support. Alternatively, you can set the 'base_class' tagged
value on the class to 'OrderedBaseFolder'. This is a general technique which
you can use to override the base folder should you need to. As an aside, the
'additional_parents' tagged value permits you to derive from multiple parents.
Another option is to derive from ATFolder (from ATContentTypes) by giving the
class the stereotype '<<atfolder>>'.
Other tagged values which may be useful when generating folders are:
filter_content_types -- Set this to '0' or '1' to turn on/off filtering of
content types. If content types are not filtered, the class will act as a
general folder for all globally addable content.
allowed_content_types -- To explicitly set the allowable content types, for
example to only allow images and documents, set this to: 'Image, Document'.
Note that if you use aggregation or composition to create folderish types as
described above, setting the allowed content types manually is not necessary.
Portal tools
A portal tool is a unique singleton which other objects may find via
'getToolByName' and utilise. There are many tools which ship with Plone,
such as portal_actions or portal_skins. To create a portal tool instead of
a regular content type, give your class the '<<portal_tool>>' stereotype.
Tools can hold attributes and provide methods just like a regular content
type. Typically, these hold configuration data and utility methods for the
rest of your product to use. Tools may also have configlets - configuration
pages in the Plone control panel. See the quick reference at the end of this
document for details on the tagged values you must set to generate configlets.
Abstract mixin classes
By marking your class as 'abstract' in your model (usually a separate
tick-box), you are signifying that it will not be added as a content type.
Such classes are useful as mixin parents and as abstract base classes for more
complex content types, and will not have the standard Archetypes registration
machinery, factory type information or derive from BaseClass.
Stub classes
By giving your class the '<<stub>>' stereotype, you can prevent it from being
generated at all. This is useful if you wish to show content types which are
logically part of your model, but which do not belong to your product. For
instance, you could create a stub for Plone's standard Image type if you wish
to include this as an aggregated object inside your content type - that is,
your content type will become folderish, with Image as an allowable contained
type.
Deriving/Subclassing Classes
Deriving or subclassing a class is used to extend existing classes, or change
their behavior. Using generalisation arrows in your model, you can inherit
the methods and schema from another content type or mixin class in your class.
Simple Derivation
All content types in Archetypes are derived from one of the base classes -
BaseContent, BaseFolder, OrderedBaseFolder and so on. If you wish to turn
this off, for example because the base class is being inherited from a
parent class, you can set the 'base_class' tagged value to '0'.
Multiple Derivation
You can of course use multiple inheritance via multiple generalisation
arrows in your model. However, if you need to use a base class that is not
on your model, you can set the 'additional_parents' tagged value on your
class to a comma-separated list of parent classes.
Deriving from other Products
If you want to derive from a class of an other product create a stub class
with a tagged value 'import_from': This will generate a import line
'from VALUE import CLASSNAME' in classes derived from this class.
Deriving form ATContentTypes
To derive from ATDocument just use a stereotype '<<atdocument>>'. Also
possible with '<<atfile>>', '<<atevent>>' and '<<atfolder>>'.
Packages - bring order to your code
Packages are both a UML concept and a Python concept. In Python, packages are
directories under your product containing a set of modules (.py files). In
UML, a package is a logical grouping of classes, drawn as a large "folder"
with classes inside it. To modularise complex products, you should use
packages to group classes together.
3.2.2. Attributes / Fields / Indexing
How to control the fields of your schema.
Archetypes are using 'schemas' (also called 'schemata') with 'fields' to define the form-fields on your content. The schema and its fields of your content types is generated from the 'attributes' of your classes in your model and their tagged values. Each field has a type and a widget.
The "Archetypes documentation":/documentation/manual/archetypes-developer-manual/ and the quick reference at the end of this document describes which fields are available and what parameters they take as configuration.
Usage of tagged values
If you set a tagged value on an attribute of your class, in general that tagged value will be passed through as a parameter to the generated Archetypes field. Hence, if you set a tagged value 'enforceVocabulary' to the value '1' on an attribute, you will get 'enforceVocabulary=1' for that field in the generated schema. Similarly, you can set a field's widget properties by prefixing the tagged value with 'widget:'. 'widget:label' sets the label of a widget, for instance.
Non-string tagged values
As before, when reading tagged values, ArchGenXML will generally treat them as strings, with a few exceptions where only non-string values are permitted, such as the 'required' tagged value. If you do not wish your value to be quoted as a string, prefix it with 'python:'. For example, if you set the tagged value 'default' to 'python:["high", "low"]' on a 'lines' attribute, you will get 'default=["high", "low"]' in a LinesField in your schema.
field recycling - copy from parents schema or another source schema and modify
This feature alows you to copy a field from another source schema and rename the field.
Rather than subclass an entire class then delete unwanted fields, you can explicitly copy just the fields you need. You can keep the copied field "as-is" or modify it by overriding properties with tag values as needed.
For example you may need a 'Description' field that is usually defined in your parent classes (BaseContent, BaseFolder) Schema. You would create a new attribute in your class named 'description' with a type of 'copy.' If you want it to appear in your base_edit form rather then the default of properties/metadata page you just need to change one property of the field by adding the tag 'schemata = "default"'.
You may also copy from any other schema or from within the same schema. You need to specify the source schema using the tag 'copy_from' and if you need to rename the field use the 'source_name' tag to indicate the source field Id, otherwise the Id of the field in you schema is used.
Index and metadata in catalogs and Collection
ArchgenXML can create configuration files to create an index and/or metadata entries in the catalog such as portal_catalog.
Available are the following tagged values:
'catalog:index' -- add the field to the index. Boolean, 1 or 0. Default is 0. If set, you may need to provide 'index:*' tagged values too.
'catalog:metadata' -- add the field to the metadata record on the query result? Boolean, 1 or 0. If you do not provide 'index:attributes', the name of the accessor of the field is the default. If 'catalog:metadata_accessor' is given it will be used instead.
'catalog:metadata_accessor' -- the accessor used for the metadata (string).
'catalog:name' -- sometimes you need to add an index to a other catalog than 'portal_catalog' and its XML-File 'catalog.xml'. Provide a tuple of comma separated strings, id of the catalog and the filename of its configuration file. default is "portal_catalog, Plone Catalog Tool'.
'index:type' -- the type of index used as (string), for example 'FieldIndex', 'KeywordIndex', 'DateIndex' or any available index in your portal. For known types a default is guessed, such as FieldIndex for StringFields or DateIndex for DateFields. If no guess is possible, we assume a FieldIndex.
'index:attributes' -- the attributes to use for index (string or comma separated list of strings). This are the methods called at indexing time. Normally it is enough to provide one index method, but for some specific use cases you might need to provide alternatives. If you do not provide this tagged value, the name of the accessor of the field is the default.
'index:name' -- the name of the index used (string). Use this name in your queries. If you do not provide a name, the name of the accessor of the field is the default.
'index:extras' -- some indexes are using so called 'extras' on installation as configuration. If the index need extras you'll need to declare them here. provide a comma separated list.
'index:properties' -- some indexes are using 'properties' on installation as configuration. If the index need properties you'll need to declare them here. Provide a comma separated list.
'collection:criteria' -- add the index to the Collection (aka Smart Folder) Indexes available for defining Criteria. Provide a comma seprated list of criteria that will be available by default. Available criterias are: ATBooleanCriterion, ATDateCriteria, ATDateRangeCriterion, ATListCriterion, ATPortalTypeCriterion, ATReferenceCriterion, ATSelectionCriterion, ATSimpleIntCriterion, ATSimpleStringCriterion, ATSortCriterion, ATCurrentAuthorCriterion, ATPathCriterion, ATRelativePathCriterion. You must provide an 'index:type' as well.
'collection:criteria_label' -- the display name of the 'collection:criteria', called 'friendly name' (string). Its added to the 'generated.pot' as a literal. If not given the 'widget:label' is taken if provided.
'collection:criteria_description' -- a help text (string), used for 'collection:criteria'. Its added to the 'generated.pot' as a literal. if not provided the 'widget:description' is used.
'collection:metadata' -- register the 'catalog:metadata' as an available column in a Collection. Can be used as an alternative for 'catalog:metadata'. 'catalog:metadata_accessor' is used if given.
'collection:metadata_label' -- the display name of the 'collection:metadata', called 'friendly name' (string), used for index:criteria. Its added to the 'generated.pot' as a literal. If not given the 'widget:label' is taken if provided.
'collection:metadata_description' -- a help text (string), used for 'collection:criteria'. Its added to the 'generated.pot' as a literal. If not provided the 'collection:criteria_help' or - if not provided - 'widget:description' is used.
*DEPRECATED* For backward compatibility reasons we support a sub part of the old style in ArchGenxML Version 1.6 and earlier using the tagged value 'index'. This is deprecated and will be removed in one of the next version of ArchGenXML. A tagged value 'index' with value like 'index:type' above creates an index with the accessor. To include the index in catalog metadata (and have the attribute ready to use in the brain objects), append ':brains' (same as older ':schema'), (e.g. 'FieldIndex:brains'). ArchGenXML does longer provides the ability to define multiple indexes using the old declaration style.
3.2.3. Widgets
Setting up the Widgets for each field.
ArchGenXML will pick a default widget for your fields and fill in default labels and descriptions. For example, a 'string' field gets a 'StringWidget' by default, but a 'selection' field type gets 'SelectionWidget' on a StringField! You can override this in two ways. So ArchGenXML mixes up fields and widgets slightly for convinience reasons. Anyway, you can override all predefined definitions using widget options.
Widget options are specified with the prefix 'widget:'. As with normal field tagged values, unrecognised options will be passed straight through to the widget definition.
The most common widget options are:
widget:type -- sets the widget type used. Its the name of the widget class. You can use all widgets shipped within the Archetypes-Framework by just providing this tagged value. To use 3rd-Party widgets you additionally need to import the class using the *imports* tagged value on class level.
widget:label -- sets the widget's label
widget:description -- sets the widget's description
widget:label_msgid -- overrides the default label message id (i18n)
widget:description_msgid -- overrides the default description message id (i18n)
widget:i18n_domain -- sets the i18n domain (defaults to the product name)
You may also use widget-specific options, such as 'widget:size' where they apply. Look up possible widget-specific options at the documentation of the widget you want to use.
Changing the default widgets
To change the widget used for one field-type for a whole model, a product, a package or just for all fields in one class you can set on the product, package or class level the tagged value 'default:widget:FIELDNAMEABBREVIATION' to 'WIDGETNAME'. For example use the tagged value 'default:widget:Reference' set it to 'ReferenceBrowserWidget' to use the ReferenceBrowserWidget instead of the ReferenceWidget. You might also want to also use the 'imports' tagged value and set it to 'from ATReferenceBrowserWidget.ATReferenceBrowserWidget import ReferenceBrowserWidget' on your class to ensure that you get the widget definition imported into your class.
Creating new widgets
To define a new widget add a class to your model with the '<<widget>>' stereotype.
3.2.4. Methods and Actions
Defining Methods and Actions
To create a method in your class, add a method to the UML diagram, with the desired parameters. The types of the parameters and the type of the return value are ignored, since Python does not support this.
Methods can different access specifiers (also called visibilities) These are:
public (shown by a + before the method name) -- The method is part of the class' public interface. It will be declared public (accessible from unsafe/through-the-web code) by default. If you add a tagged value 'permission' (see below), it will be declared as protected by this permission.
protected (#) -- The method is not part of the class' public interface, but is meant for use by sub-classes. It will be declared private to prevent unsafe code from accessing it.
private (-) -- The method is internal to the class. It will be declared private to prevent unsafe code from accessing it.
package (~) -- The method is intended to be accessed by other code in the same module as the class. It will not gain any Zope security assertions, relying instead on the class/module defaults.
There are a few tagged values you can use to alter how the code is generated:
code -- Sets the python code body of the method. Only use this for short one-liners. If you fill in code manually in the generated files, method bodies will be preserved when you re-generate the product from the UML model.
documentation -- Content of the python doc-string of the method. You can also use the documentation feature of most UML modellers to set documentation strings.
permission -- Applies to methods with 'public' visiblity only. If you set the permission tagged value to 'My custom permission' results in security.declareProtected("""My custom permission""",'methodname') - that is, access to your method is protected by the permission with the name 'My custom permission'.
If you want to use the CMF core permissions, add an 'imports' tagged value to the method's class containing 'from Products.CMFCore import permissions', and then set the permission tagged value of your method to 'python:permissions.View', 'python:permissions.ModifyPortalContent' or any other core permission. You can also use the common paradigm of defining permissions in config.py as constants with names like EDIT_PERMISSION. A config.py is automatically generated and its contents imported, so you can just set the permission tagged value to, for example, 'python:EDIT_PERMISSION'.
Archetypes uses actions for generating custom tabs to access some view of an Archetype object. ArchGenXML can generate actions for you: Just define a method without any parameters and set its stereotype to <<action>>.
Once again tagged values can be set on the sterotyped methods in order to set some properties of the action:
action -- The TAL expression representing the action to be executed when the user invokes the action. Defaults to the methodname.
category -- The category of an action, view or form. Defaults to 'object'.
id -- The id of an action, view or form. Defaults to the methodname.
label -- The label of an action, view or form. Defaults to the methodname.
permission -- 'permission=My permission' results in 'permissions': ('''My Permission''',). See the description of the general 'permission' tagged value above for more.
condition -- A TALES expression giving a condition to control when the action is to be made available.
You can override the default Archetypes actions by using special names for the id. These are:
view -- for overriding the default view action.
edit -- for overriding the default edit action.
contents -- for overriding the default contents action.
3.2.5. Relationships between classes and objects
How to use references, associations, aggregations and compositions
With aggregations, compositions and associations you define where your new type will show up, what it might contain and to which content it can point to.
There is virtually no limit on how many aggregations, compositions and associations you can attach to a class.
Aggregations: Global Containment "Aggrigation Img":img:aggregation.png
Aggregation means: This content can exist global and in this container. The container class that gets the empty rhomb (diamond) attached is derived from BaseFolder and it's allowed_content_types is set to the class that is attached to it.If more than class is attached to one class by aggregations the allowed_content_types is extended accordingly. The attached class keeps the default ``global_allow=1``.
Compositions: Strict Containment "Composition Img":img:composition.png
Compositions are used to model parts that exist or live and die with their associated owner. So the code generated is similair to the one generated by aggregations, but with one major difference: The attached classes are only allowed to be generated in the folderish type of the class they're attached to (this is done by setting ``global_allow=0`` in the factory type information of the class).
Directed Associations: References "Directed Association Img":img:directedassociation.png
References are used to store the relation of an object to other objects.
Each content type that derives from ``IReferenceable`` is capable of being referenced. Objects from such a content type have an UID (Unique Identification) that's unique throughout the whole Plone site. Therefore References don't break if you move referenced objects around in the site.
To use ``ReferenceFields`` there are two possible ways. The by models-design clean way is to use directed associations. Another possibility is to define References as class-attributes.
Directed Associations
An directed association between two classes generates a ``ReferenceField`` in the class where the association starts.
The ``relationship`` itself is named after the association's name.
The multiplicity defines if the allows a 1:1 or 1:n relation. Attention: This only results in validation on the field. References at all don't know anything about multiplicity, so this is only a check on userinterface-level.
All other field settings are taken from the association's end, including information how to generate the widget. By default a ReferenceWidget is used. You can use tagged values on the association's end to define label, description, a different widget-type, schemata, etc. like you do it on a field (on a class attribute).
The big drawback of using associations to create ReferenceFields is that they always get attached to the end of the schema and there is no way to change that in the UML diagram. So if you need order in your fields read the next section.
References as class attributes
You can define an attribute with the type reference. Then you can apply any needed tagged values to it.
keys of interest are::
allowed_types : needs a list of allowed types
multiValued : set to 0 to only be able to select one object to reference to
relationship : name of the relationship in the reference_catalog
The benefit of using an attribute to define the reference is that you can define the place in the schema where the ReferenceField will show up.
Reference classes (advanced)
Sometimes it's needed to store information not in the origin or
destination class, but in the reference itself. UML has a notation to
model this: "association classes":http://argouml.tigris.org/documentation/defaulthtml/manual/ch17s11.html#s2.ref.association_multiway
ArchGenXML support them automatically. When a model includes an
association class, two things occur:
a) A new content type is created, named like the association name
b) The generated ReferenceField has a new attribute defined like this: ``referenceClass = ContentReferenceCreator('My_Association_Name')``
This causes that the class of the reference instances is now not "Archetypes.ReferenceEngine.Reference", but "Archetypes.ReferenceEngine.ContentReference", a subclass of it that has a new method: getContentObject(), that return the content inside the
reference.
The same effect can be reached without association classes, by defining
a content type and then adding the "association_class" tagged value to
the association (although I haven't been able to make this work).
To create the reference via code, use a special form of the addReference method::
origin = <the origin content>
destination = <the destination content>
assocName = <the association name>
origin.addReference(destination,
assocName,
referenceClass=ContentReferenceCreator(assocName),
attr1=value1,
attr2=value2...)
(where attr1, attr2... are the attributes of the association)
To read the data, we can't use the origin.getRefs(assocName) method, as usual, because it returns only the destination objects. One way to read it is by using the reference_catalog tool::
from Products.CMFCore.utils import getToolByName
tool = getToolByName(origin, 'reference_catalog')
refs = tool.getReferences(origin, assocName)
if not refs:
return []
else:
return [(ref.getContentObject(), ref.getTargetObject()) for ref in refs]
3.2.6. Workflows
Usage of state diagrams to create custom workflows.
ArchGenXML can use state diagrams to generate workflows for a portal type. Workflows are used to set the various states an object can be in, and the transitions between them.
Importantly, workflows control permissions of objects. By convention, and for convenience and consistency, most content types will use the permissions found in the CMFCorePermissions class in the CMFCore product to control access to their methods. The methods generated by and inherited from the CMF and Archetypes frameworks adhere to this principle. Although many different content types use the same basic permissions to control access, workflows are the means by which you can control permissions for an object in detail. For instance, you may wish to specify that in the testing state, Manager and Reviewer has Modify portal content permissions, and Owner, Manager and Reviewer has View permissions. For the completed state, you could have a different set of permissions. See the DCWorkflow documentation for more details about how to use workflows.
Problems with UML-Software
The workflow implementation of ArchGenXML has to date only been tested with ArgoUML and Poseidon (tested Version is 3.1 and 3.2 CE).
ObjectDomain is known not to work at this time, because it does not appear to correctly export the XMI for state diagrams. If you have different experiences, please add a comment to this document or contact us.
Creating a workflow
In your UML modeller, add a state diagram for the class you wish to create a custom workflow for. If you don't want to assign the workflow to a class use an class with stereotype stub. In Poseidon, this is done by right-clicking on the object in the tree on the left hand side, and selecting to add a new state diagram. The name of the state diagram becomes the name of the workflow.
States
On the state diagram, add a state item (a rounded-corner box) for each state. You must have an initial state of your workflow for it to work correctly. Use a "initial state" symbol (filled cirlce) for the state your object defaults to after creation. Optional you can use a normal state item and set a tagged value initial_state with value 1 to it.
At present, ArchGenXML does not support the "final state" UML symbols to represent final states, so you should stick to the standard state symbols.
The names of your states in UML become the names of the states in your workflow. The user-visible label can be set with the label tagged value; it defaults to the state name.

Transitions
For each possible transition between states, add a transition arrow to your UML model. The name of the transition becomes the name of the workflow action. You can set the label tagged value on the transition to set a custom label to display to the user.
If a transition with the same name/target is used more than one time, you can use the stereotype <<primary>> to define its settings once and use it by name on all similar transitions.
Transition guards
You can add a guard to a transition to restrict to whom and when it is made available. Set the expression field of a transition to a |-separated list of the following pairs:
- guard_roles
- Set
guard_roles:Owner; Managerto restrict the transition to users posessing the Owner or Manager role in the current context. - guard_permissions
- Set
guard_permissions:My custom permission;Viewto ensure that only those users withMy custom permissionorViewpermissions in the current context are allowed to access the transition. - guard_expr
- Set 'guard_expr:expression", where
expressionis a TALES expression, to have the expression be evaluated in order to determine whether the transition should be made available.
Thus, to restrict access to roles Reviewer and Manager, and only those users with permission My custom permission and View in the current context, you can set the expression of the transition to guard_roles:Reviewer;Manager|guard_permissions:My custom permission, View.
If you are using Poseidon, transition guards are located in the property of the transition arrow with the name [A] Guard. You can add an expression like the one outlined above to this field.
Permissions
ArchGenXML uses tagged values on states in a somewhat unconventional, though convenient, way to control permissions. With the exception of the special-case initial_state and label tagged values, you give the name of the permission as the tagged value key, and a comma-separated list of roles the permission should be enabled for as the value.
There are three shorthand permission names available:
- access
- referes to the
Access contents informationpermission, - view
- refers to the
Viewpermission, - modify
- refers to the
Modify portal contentpermission, - list
- refers to the
List folder contentspermission. - delete
- refers to the
Delete objectspermission.
Hence, if you want your state to permit anonymous users and members to view your content, only permit managers to modify, and permit both the owner and managers to add new objects controlled by the Add MySubTypes permission, you can add tagged values to the workflow state:
view ==> Anonymous, Member
modify ==> Manager
Add MySubTypes ==> Owner, Manager
If you want to aquire the permissions and add new ones you can use the value 'aquire':
view ==> acquire, Anonymous, Member
(One special case: if you leave the value empty, no one gets that permission (which is logical), but it also explicitly unsets acquisition of the permission).
Workflow actions
The portal_workflow tool allows a script to be executed before and/or after a transition is completed. This is no longer supported. Instead subscribers to the Workflow events are used. Event-subsribers are more flexible.
Actions are set using the effect field of a transition. The value given here gives the name of the subscriber to execute (and thus must be valid python method name). ArchGenXML will create or modify a subscriber for each workflow-action in a file wfsubsribers.py in your product. You must fill in the method bodies for the actions in this file. Method bodies will be preserved upon re-generation of your product from the UML model. In Plone 2.5 compatible mode DCWorkflow needs a patch with a backport. This patch is generated, if 2.5 is selected as plone_target_version (tagged value on model).
By default, actions specified in this way are post-transition actions, meaning that they are executed after the transition has taken place. If you wish to specify a pre-transition action, executed before the transition takes place, separate action names by semicolons: preActionName;postActionName. If you want only a pre-transition action, use preActionName; to specify that there is an empty post-transition action.
Attach workflow to more than one class
In UML there is no semantic to use a workflow for more than one class. We introduced the tagged value use_workflow for classes. Value is the workflow name.
Worklist support
You can attach objects in a certain state to a worklist. A worklist is something like the "documents to review" list you get when you're a reviewer in a Plone site. This is done by adding a tag worklist to the state with the name of the worklist as value (like review_list).
You can add more than one state to a worklist, just by specifying the same name for the worklist tagged value. Likewise, you can have more than one worklist (just not on the same state). The tagged value worklist:guard_permissions allows you to specify the permission you need to have to view the worklist. The default value is Review portal content.
3.2.7. Tests
Generation of tests from UML.
Overview
We hope there is no need for us to emphasize how important testing is for even the most simple products you may be developing. This makes it especially useful to include the creation of both your testing framework and individual tests within your UML model.
Within a new package with a stereotype of '<<tests>>' you can create your base test case class with a stereotype of '<<plone_testcase>>' and a test setup class with a stereotype of '<<setup_testcase>>'. You can then add additional test classes as needed with a stereotype of either '<<testcase>' or '<<doc_testcase>>'.
"sample UML":img:uml-testcase.png
Base Test Case
Creating a class in your tests package with a stereotype of '<<plone_testcase>>' generates the needed base test case for all other test cases.
Test Setup
Creating a class in your tests package with a stereotype of '<<setup_testcase>>' generates a testcase for the setup, with pre-defined common checks.
Test Cases
Test Case
Creating a class in your tests package with a stereotype of '<<testcase>>' generates a standard test case class.
Doc Tests
Creating a class in your tests package with a stereotype of '<<doc_testcase>>' generates a doc test file in the 'doc' folder of your product with a name of 'class + .txt'. You can use the tag of 'doctest_name' on your class (excluding any extension as '.txt' is appended automatically) to indicate a different name for your generated doc test.
Functional Tests
Generate browser functional tests using the '<<plonefunctional_testcase>' stereotype.
Interface Tests
Generate interface tests using the '<<interface_testcase>>' stereotype.
It is unverified whether these generated test cases will verify Z3 style interfaces but stay tuned!
3.3. Third Party Product Integration
The usage of add on products.
3.3.1. ATVocabularyManager
ATVocabularyManager is a product for letting site managers define vocabularies for fields through-the-web or by import from XML files. ArchGenXML can generate the necessary code to use this product.
ATVM manages dynamic vocabularies. It installs a tool, where a site Manager can add, change and delete vocabularies. These vocabularies can then be used anywhere on the site.
You can download ATVocabularyManager from the Plone.org products area: "http://plone.org/products/atvocabularymanager":/products/atvocabularymanager
Using simple flat vocabularies
Adding ATVM-vocabs to your UML model is quite easy.
1. Add a selection or multiselection field to your type.
2. Add a tag 'vocabulary:name' and give it a name, let's say 'countries'
3. Add a tag 'vocabulary:type' with the value 'ATVocabularyManager'
We are now finished with the UML. Save it and let AGX do the work. What still is missing, is to install the countries vocabulary. Therefore:
* Add a function called 'setupVocabularies' to the protected code section in 'setuphandlers.py' in your product and register it as an import step in '/profiles/default/import_steps.xml' in a code section (make it dependend from you '*QI-Dependencies' step.
* Add the following code to your setuphandler.yp (this sets up a vocabulary 'countries' with the given values, and registers it with ATVocabularyManager)::
from Products.ATVocabularyManager.config import TOOL_NAME as ATVOCABULARYTOOL
from Products.CMFCore.utils import getToolByName
from Products.ATVocabularyManager.utils.vocabs import createSimpleVocabs
def setupVocabularies(context):
"""let's install the countries vocab"""
vocabs = {}
vocabs['countries'] = (
('ice', u'Iceland'),
('nor', u'Norway'),
('fin', u'Finland'),
('tyr', u'Tyrol'),
('auf', u'Ausserfern'),
)
site = context.getSite()
atvm = getToolByName(site, ATVOCABULARYTOOL)
createSimpleVocabs(atvm, vocabs)
Using simple tree vocabularies
If youre interested in using and creating hierachical vocab:
* use additional tag 'vocabulary:vocabulary_type' with value 'TreeVocabulary',
* have a look at the doc-string of 'Products.ATVocabularyManager.utils.createHierarchicalVocabs'.
Using vocabularies based on the **IMS Vocabulary Definition Exchange** (VDEX) format.
"VDEX":http://www.imsglobal.org/vdex/index.html is a simple XML based format to define flat or hierachical multilingual vocabularies. ATVocabularyManager supports VDEX in most of its dialects.
To tell Archetypes to use them in your UML first take Steps 1 to 3 of the first section and skip the import part. Then add a tag 'vocabulary:vocabulary_type' and give it the value 'VdexVocabulary'.
Now add a folder called 'data' in your products folder. Inside the '/data' folder create a new file called 'countries.vdex' ("example":countries.vdex). It will be imported automatically on install or reinstall, but only if a vocabulary named countries does not exist.
3.3.2. Relations
Create relations between portal-types model-driven. Support for Relations Product (complex references). Define sets of rules for validation, creation and lifetime of Archetypes references. ArchGenXML can generate the necessary code and XML-configuration data to use this product.
Prerequisites
To enable Relations install the Product ("code-location":http://plone.org/products/relations/).
Basics
As an option on command line, up to a tagged-value on model-level or on a single UML-Association you just define the
'relation_implementation' and set it to 'relations'. A directed Assoziation results in one Relation.
**Give the association and its assoziation ends names.** They'll be used as the names for the RelationField. If you dont want a field turn it off by setting a tagged value 'generate_reference_fields' on class (or package, model) level to '0'.
Inverse Relation
If the association is not directed (navigable on both association ends) an inverse relation will be created.
The tagged-value 'inverse_relation_name' will be used for the back-relation on undirected associations. It defaults to a relation named 'toend_fromend', where these are the lowercased versions of the association ends. If the two ends are named the same, then the relation will be named 'association_inv', where 'association' is the name of the association. (Finally, if the option 'old_inverse_relation_name' is set, then it defaults to the association name postfixed by '_inverse'.)
Cardinality
You can use the Multiplicity on in UML to define the cardinality of an Relation.
You can use the minimum and maximun value here using the syntax '1..5' which means at least one relationrelated objects but not more than five.
Constraints
type-constraint -- as described above an association between two portal-types will be created.
interface-constraint -- an association between an archetypes class and an interface will create an interface-constraint. the relation is allowed to all classes implementing this interface.
Association classes
Association classes can be used to store data on the relation as an object. You can model it using the UML association class or using a tagged value 'association_class' on the association.
3.3.3. Remember
Generate 'Remember' based Member-Types. Its the successor of CMFMember.
Prerequisites
You must install to additional Products:
* "membrane":http://plone.org/products/membrane
* "remember":http://plone.org/products/remember
(using "Five 1.4.3+":http://codespeak.net/z3/five/)
You should also read the documentation of both and understand how they work!
A Content-Type based on remember
* Create a class in your class diagram and give it a a stereotype '<<remember>>'
* add the tagged value 'use_workflow' and set it to one of
'member_approval_workflow' or 'member_auto_workflow'. You can create also your
own workflow if you know what remember needs (look at the workflows shipped
with remember).
* set the 'active_workflow_states' tagged value to the class and declare which
states of the used workflow are the ones, where the user can log in with.
It expects a list of values, e.g. 'python:["private", "public"]'
* Add attributes (fields) as you need. Attention here, only override fields
of remembers BaseMember schema if you know what youre doing.
* Generate & Done
3.3.4. CompoundField and ArrayField
How to make custom fields: a list of some default field type, a compound of a two or more default fields.
Prerequisites
Install the "CompoundField":http://plone.org/products/compoundfield extension
into you Products folder.
List of fields - ArrayField
Assume you want to have content type where the user can provide one or more files.
Its easy by making the type folderish. But for some use-cases this is to heavy or to
difficult, you want the user to use a form for those files.
You could say, ok, up to 5 files is enough and model 5 file fields into your class.
Not very elegant, huh?
The easiest way is to to use the UML 'multiplicity' feature on your attribute aka field of the
class. If you want to enable unlimited attachments use multiplicity '*'.
Or choose a number like '5', as in our above example.
You can set the initial size of the array by using the tagged value 'array:size' to 'python:10' for example.
Prefixed with 'array:' you can access also the label 'array:widget:label' of it and so on.
If you prefer the EnhancedArrayWidget you need to add an tagged value 'imports'
'from Products.Compoundfield.EnhancedArrayWidget import EnhancedArrayWidget' to your class
and set on the attribute the tagged value 'array:widget:type' to 'EnhanceArrayWidget'.
Custom Fields compounds - CompoundField
With ArchGenXML you can create compounds of fields from existing fields.
Such a set of fields behaves almost like a normal field.
To create such a compounded field create a new class and give it the
stereotype '<<field>>'
Now add attributes to it like you would do on a content type class.
You can use almost every field type, just some special fields, mostyl those
acting as a proxy without own storage, wont work (such as ReferenceField or AttachementField).
For example we create a 'PointField' consisting out of two 'FloatFields' by
just adding a 'x' and 'y' attribute of type 'float'.
To use the new field create a fresh content class and name it 'Polygon'.
Take a dependency arrow pointing from your Polygon class to the field class.
This ensures it gets imported!
Next add an attribute 'points' to the class. The type of the new points attribute
is 'PointsField'. Now to make it a polygon give it a multiplicity of *and
your done: You have a list of Points as one field.
3.3.5. Content Flavors
When you want to add a couple of fields to an existing content type (including reference fields), you may decide to create a whole new product that subclasses that type. You then have a whole bunch of code to maintain and you are dependent on changes that may occur in your parent class. Or you let ArchGenXML use the Content Flavors product and your day gets brighter. Note that the use of the experimental content flavors product is now deprecated in favor of the more reliable and feature-rich archetypes.schemaextender.
Prerequisites
You must install the Content Flavors product.
Adding a field to an existing content typ
- Let an existing content type, e.g. "ExistingType", be present in your diagram (as a class with stereotypes <<archetype>> and <<stub>>)
- Create a class, e.g. "MyCoolFlavor", in your diagram and give it the <<flavor>> stereotype
- Add any field(s), e.g. "MyAdditionalField", to this flavor class
- Create a realization arrow from "ExistingType" to "MyCoolFlavor"
- Generate & Done
Now every new instance of ExistingType will have the MyAdditionalField field in its schema, default view and default edit form.
Limits
-
The use of the experimental content flavors product is now deprecated in favor of the more reliable and feature-rich archetypes.schemaextender.
- Content Flavors also allows custom views to be used by existing types but this feature is not supported by ArchGenXML yet.
- You may not be able to see the additional field(s) if the ExistingType uses some non-default view. You then have to manually manage this by overriding these existing views with some of your own, with or without the help of the Content Flavors product.
- The existing content type may have to be based on ATCT (to be tested)?
- Several flavors can be applied to a given type. The order of precedence can be managed through the web if the existing type follows some requirements detailed in the Content Flavors documentation.
- There are possible issues with indexing the additional fields, see CF documentation for details.
- Content Flavors was an experiment and is now deprecated in favour of the archetypes.schemaextender, which is also supported by AGX.
3.3.6. archetypes.schemaextender
When you want to add a couple of fields to an existing content type (including reference fields), you may decide to create a whole new product that subclasses that type. You then have a whole bunch of code to maintain and you are dependent on changes that may occur in your parent class. Or you let ArchGenXML make your product use archetypes.schemaextender and your day gets brighter.
Prerequisites
You must install the archetypes.schemaextender product.
Adding a field to an existing content type
Let's say you want to add superPower (a field) to HumanBeing
(a class).
- Let the content type to extend, e.g. "HumanBeing", be present in your diagram (as a class, possibly with the <<stub>> stereotype)
- Create an interface, e.g. "ISuperHero" (the "I" helps remembering this is an interface), in your diagram ; it has the <<interface>> stereotype
- Draw a realization arrow from "HumanBeing" to "ISuperHero"
- Create a class, e.g. "SuperHero", in your diagram and give it the <<extender>> stereotype
- Add any field(s), e.g. "superPower", to this extender
- Create a realization arrow from the extender class to the interface, e.g. from "SuperHero" to "ISuperHero", remove its <<realize>> stereotype and replace it with a <<adapts>> stereotype
- Generate & Done
Now every new instance of HumanBeing will have the superPower field in its schema, default view and default edit form. And even if the developer of the HumanBeing content type decides to remove its arms or legs, human beings (all of them) will still have super powers because you declared they are super heros (they implement ISuperHero).
Note that, in order to add these stereotypes (<<extender>>, <<adapts>>) to your (Argo)UML model, you have to create a stereotype, give it the proper name (extender, adapts) and select its proper "Super-class" in the properties tab (you either select class for class stereotypes, or abstraction for the "adapts" stereotype).
Example model and additional features
AGX support of archetypes schemaextender is illustrated by the example model in the screenshot below :

You may note from this example model that AGX support of archetypes.schemaextender comes with a bunch of funny features :
- support for adapters and named adapters
- adapted interfaces (or classes) can be external to your model (using the <<stub>> stereotype)
- you can even let a stub class implement a
stub interface : in the example above, the HumanBeing stub class has a realization arrow to IAStubInterfaceFromYetAnotherProduct, which will be turned by AGX into a
five:implement ZCML declaration. This is useful when you want to integrate 2 distinct third party products via your own product. Your model is then nothing more than a "glue" model, which is a good thing in many cases.
3.4. Reference
Reference of Stereotypes, Tagged Values and more.
3.4.1. Quick Reference
A quick reference sheet.
**TODO: UPDATE!**
Complete list of the field types including their default settings:
string -- StringField
- StringField
- searchable=1
text -- TextField
- StringField
- searchable=1
- TextAreaWidget()
richtext -- TextField
- TextField
- default_output_type=text/html
- allowed_content_types=('text/plain','text/structured','text/html','application/msword',)
selection -- StringField with SelectionWidget
- StringField
multiselection -- LinesField with SelctionWidget
- LinesField
- multiValued=1
integer -- IntegerField
- IntegerField
- searchable=1
float -- Floatfield
- FloatField
- searchable=1
- DecimalWidget()
boolean -- BoleanField
- BooleanField
- searchable=1
lines -- LinesField
- LinesField
- searchable=1
date -- DateTimeField
- DateTimeField
- searchable=1
image -- ImageField
- ImageField
- sizes ={'small':(100,100),'medium':(200,200),'large':(600,600)}
- AttributeStorage()
file -- FileField
- FileField
- AttributeStorage()
- FileWidget()
lines -- LinesField
- LinesField
- searchable=1
fixedpoint -- FixedPointField
- FixedPointField
reference -- ReferenceField
- ReferenceField
backreference -- BackReferenceField
- BackReferenceField
computed -- ComputedField
- ComputedField
color -- StringField w/Color picker
- StringField
country -- StringField
- StringField
- CountryWidget
datagrid -- DataGridField
- DataGridField
- DataGridWidget
photo -- PhotoField
- PhotoField
Tagged values for fields:
searchable -- register and index the field in the catalog,
* 1 .. register and index
* 0 .. don't register and index
storage -- AttributeStorage(), SQLStorage(), ....
sizes -- defines the sizes of the images in a ImageField
example: python:{'small':(80,80),'medium':(200,2000),'large':(600,600)}
default_method -- no idea what that does
required -- defines whether a field should be rendered required, or not.
- 1 .. field is required
- 0 .. field is not required
accessor -- defines the accessor of a field
vocabulary -- defines the vocabulary or the method generating a vocabulary
allowed_types -- defines the allowed types in a ReferenceField
relationship -- defines the relationship, used in a ReferenceField
multiValued -- defines whether a SelectionField accepts one or more values,
- 1 .. multivalued
- 0 .. singlevalued
These tagged values are just the ones handy for fields, the full lists of tagged values
and stereotypes are shown on the next two pages.
3.4.2. Tagged Values
All tagged values available in its context.
*This file was generated 2009-05-12 with bin/agx_taggedvalues 2.4.1. Don't forget to replace << by &lt;&lt; and >> by &gt;&gt; on this page.*
action
action -- For a stereotype 'action', this tagged value can be used to
overwrite the default URL ('..../name_of_method') into
'..../tagged_value'.
category -- The category for the action. Defaults to 'object'.
condition -- A TALES expression defining a condition which will be
evaluated to determine whether the action should be displayed.
id -- The id of the action. Use 'id',
label -- The label of the action - displayed to the user.
permission -- The permission used for the action, a string or comma
separated list of strings, default to 'View'.
visible -- Sets the visible property, default to 'True'
association
association_class -- You can use associations classes to store content
on the association itself. The class used is specified by this
setting. Don't forget to import the used class properly.
association_vocabulary -- Switch, defaults to False. Needs Product
'ATVocabularyManager'. Generates an empty vocabulary with the name
of the relation.
back_reference_field -- Use a custom field instead of ReferenceField.
field -- Synonymous with either reference_field or relation_field,
depending on whether you use it on the *from* end or the *to* end of
a relation. Works only together with 'Relations' Product and
relation_implementation set to 'relations'.
inverse_relation_name -- Together with 'Relations' Product you have
inverse relations. the name default to
'name_of_your_relation_inverse', but you can overrrule it using this
tagged value.
label -- Sets the readable name.
reference_field -- Use a custom field instead of ReferenceField.
relation_field -- Use a custom field instead of RelationField. Works
only together with 'Relations' Product and relation_implementation
set to 'relations'.
relation_implementation -- Sets the type of implementation is used for
an association: 'basic' (used as default) for classic style
archetypes references or 'relations' for use of the 'Relations'
Product.
relationship -- Standard relationship for ReferenceField
attribute
accessor -- Set the name of the accessor (getter) method. If you are
overriding one of the DC metadata fields such as 'title' or
'description' be sure to set the correct accessor names such as
'Title' and 'Description'; by default these accessors would be
generated as getTitle() or getDescription().
allowed_types -- Sets the types allowed for a ReferenceField. Default
is []
array:widget -- specify which custom ArrayWidget should be used for a
field (only applies if the field has cardinality >1.
catalog:index -- Add the field (or all fields of a class, package,
model) to the index. Boolean, 1 or 0. Default is 0. If set, you may
need to provide 'index:*' tagged values too.
catalog:metadata -- Adds the field to the metadata record on the query
result. Boolean, 1 or 0. If you do not provide 'index:attributes',
the name of the accessor of the field is the default. If
'catalog:attributes' is given for each attribute one field at the
record will be created.
catalog:name -- Sometimes you need to add an index to a other catalog
than 'portal_catalog' and its XML-File 'catalog.xml'. Provide a
tuple of comma separated strings, id of the catalog and the filename
of its configuration file. default is "portal_catalog, Plone Catalog
Tool'.
collection:criteria -- Add the index to the Collection (aka Smart
Folder) Indexes available for defining Criteria. Provide a comma
separated list of criteria that will be available by default.
Available criterias are: ATBooleanCriterion, ATDateCriteria,
ATDateRangeCriterion, ATListCriterion, ATPortalTypeCriterion,
ATReferenceCriterion, ATSelectionCriterion, ATSimpleIntCriterion,
ATSimpleStringCriterion, ATSortCriterion, ATCurrentAuthorCriterion,
ATPathCriterion, ATRelativePathCriterion. You must provide an
index:type as well.
collection:criteria_description -- A help text (string), used for
collection:criteria. Its added to the generated.pot as a literal. If
not provided the widget:description is used.
collection:criteria_label -- The display name of the
collection:criteria, called friendly name (string). Its added to the
generated.pot as a literal. If not given the widget:label is taken
if provided.
collection:metadata -- register the catalog:metadata as an available
column in a Collection. Can be used as an alternative for
catalog:metadata. catalog:metadata_accessor is used if given.
collection:metadata_description -- A help text (string), used for
collection:criteria. Its added to the generated.pot as a literal. If
not provided the collection:criteria_help or - if not provided -
widget:description is used.
collection:metadata_label -- the display name of the
collection:metadata, called friendly name (string), used for
index:criteria. Its added to the generated.pot as a literal. If not
given the widget:label is taken if provided.
copy_from -- To copy an attribute from another schema, give it the type
'copy'. The tagged value 'copy_from' is then used to specify which
schema to copy it from (for instance, 'BaseSchema' when copying
Description from the base schema). For copying your own schemas, add
an 'imports' tagged value to import your class (say 'MyClass') and
then put 'MyClass.schema' in your 'copy_from' value.
default -- Set a value to use as the default value of the field.
default_method -- Set the name of a method on the object which will be
called to determine the default value of the field.
enforceVocabulary -- Set to true (1) to ensure that only items from the
vocabulary are permitted.
expression -- evaluation expression for computed fields.
i18ncontent -- Enables the content type(s) for LinguaPlone. Only
allowed value is 'linguaplone'.
index -- DEPRECATED: Add an index to the attribute. Use catalog:index
and the index:* tagged value instead.
index:attributes -- The attributes to use for index or metadata (string
or comma separated list of strings). This are the methods called at
indexing time. Normally it is enough to provide one index method,
but for some specific use cases you might need to provide
alternatives. If you don not provide this tagged value, the name of
the accessor of the field is the default.
index:extras -- Some indexes are using so called 'extras' on
installation as configuration. If the index need extras you'll need
to declare them here. Provide a comma separated list.
index:name -- the name of the index used (string). Use this name in
your queries. If you do not provide a name, the name of the accessor
of the field is the default.
index:properties -- Some indexes are using 'properties' on installation
as configuration. If the index need properties you'll need to
declare them here. Provide a comma separated list.
index:type -- the type of index used as (string), for example
'FieldIndex', 'KeywordIndex', 'DateIndex' or any available index in
your portal. For known types a default is guessed, such as
FieldIndex for StringFields or DateIndex for DateFields. If no guess
is possible, we assume a FieldIndex.
indexMethod -- DEPRECATED: Declares method used for indexing.
label -- Sets the readable name.
move:after -- Move the current field after the given field (put the
field name between quote).
move:before -- Move the current field before the given field (put the
field name between quote).
move:bottom -- Move the current field to the bottom (put 1 for the
value).
move:pos -- Move the current field at the given position (an int).
move:top -- Move the current field to the top (put 1 for the value).
multiValued -- Certain fields, such as reference fields, can optionally
accept more than one value if multiValued is set to true (1)
mutator -- Similarly, set the name of the mutator (setter) method.
original_size -- Sets the maximum size for the original for an
ImageField widget.
read_permission -- Defines archetypes fields read-permission. Use it
together with workflow to control ability to view fields based on
roles/permissions.
required -- Set to true (1) to make the field required
schemata -- If you want to split your form with many, many attibutes in
multiple schemata ("sub-forms"), add a tagged value 'schemata' to
the attributes you want in a different schemata with the name of
that schemata (for instance "personal data"). The default schemata
is called "default", btw.
searchable -- Whether or not the field should be searchable when
performing a search in the portal.
sizes -- Sets the allowed sizes for an ImageField widget.
source_name -- With attribute type 'copy' sometimes schema-recycling is
fun, together with copy_from you can specify the source name of the
field in the schema given by copy_from.
validation_expression -- Use an ExpressionValidator and sets the by
value given expression.
validation_expression_errormsg -- Sets the error message to the
ExpressionValidator (use with validation_expression to define the
validation expression to which this error message applies).
validators -- TODO. Not supported for now.
vocabulary -- Set to a python list, a DisplayList or a method name
(quoted) which provides the vocabulary for a selection widget.
vocabulary:name -- Togther with Products 'ATVocabularyManager' this
sets the name of the vocabulary.
vocabulary:term_type -- For use with 'ATVocabularyManager'. Defaults to
'SimplevocabularyTerm'. Let you define the portal_type of the
vocabularyterm used for the default term that is created in
Install.py.
vocabulary:type -- Enables support for Products 'ATVocabularyManager'
by setting value to 'ATVocabularyManager'.
widget -- Allows you to set the widget to be used for this attribute.
widget:description -- Set the widget's description.
widget:description_msgid -- Set the description i18n message id.
Defaults to a name generated from the field name.
widget:i18n_domain -- Set the i18n domain. Defaults to the product
name.
widget:label -- Set the widget's label.
widget:label_msgid -- Set the label i18n message id. Defaults to a name
generated from the field name.
widget:type -- Set the name of the widget to use. Each field has an
associated default widget, but if you need a different one (e.g. a
SelectionWidget for a string field), use this value to override.
write_permission -- Defines archetypes fields write-permission. Use it
together with workflow to control ability to write data to a field
based on roles/permissions.
class
active_workflow_states -- The active workflow states for a remember
type. MUST be set on <<remember>> types. Format is ['state',
'anotherstate'].
additional_parents -- A comma-separated list of the names of classes
which should be used as additional parents to this class, in
addition to the Archetypes BaseContent, BaseFolder or
OrderedBaseFolder. Usually used in conjunction with 'imports' to
import the class before it is referenced.
alias -- FTI Alias definition in the form alias=fromvalue,tovalue
allow_discussion -- Whether or not the content type should be
discussable in the portal by default.
allowable_content_types -- A comma-separated list of allowed test
format for a textarea widget.
allowed_content_types -- A comma-separated list of allowed sub-types
for a (folderish) content type. Note that allowed content types are
automatically set when using aggregation and composition between
classes to specify containment.
archetype_name -- The name which will be shown in the "add new item"
drop-down and other user-interface elements. Defaults to the class
name, but whilst the class name must be valid and unique python
identifier, the archetype_name can be any string.
author -- You can set the author project-wide with the '--author'
commandline parameter (or in the config file). This TGV allows you
to use/ overwrite it on a class level.
base_actions -- Sets the base actions in the class's factory type
information (FTI).
base_class -- Explicitly set the base class of a content type,
overriding the automatic selection of BaseContent, BaseFolder or
OrderedBaseFolder as well as any parent classes in the model. What
you specify here ends up as the first item (or items: comma-separate
them) in the classes it inherits from. So this is also a handy way
to place one class explicitly in front of the other. See also
additional_parents.
base_schema -- Explicitly set the base schema for a content type,
overriding the automatic selection of the parent's schema or
BaseSchema, BaseFolderSchema or OrderedBaseFolderSchema.
catalog:index -- Add the field (or all fields of a class, package,
model) to the index. Boolean, 1 or 0. Default is 0. If set, you may
need to provide 'index:*' tagged values too.
catalog:metadata -- Adds the field to the metadata record on the query
result. Boolean, 1 or 0. If you do not provide 'index:attributes',
the name of the accessor of the field is the default. If
'catalog:attributes' is given for each attribute one field at the
record will be created.
catalog:name -- Sometimes you need to add an index to a other catalog
than 'portal_catalog' and its XML-File 'catalog.xml'. Provide a
tuple of comma separated strings, id of the catalog and the filename
of its configuration file. default is "portal_catalog, Plone Catalog
Tool'.
catalogmultiplex:black -- Remove an archetypes class (identified by
meta_type) from one or more catalogs to be cataloged in. Comma-
separated list of catalogs. Example-value: 'portal_catalog,
another_catalog'. Explaination: Instances of the class wont be
catalogged in portal_catalog anymore.
catalogmultiplex:white -- Add an archetypes class (identified by
meta_type) to one or more catalogs to be cataloged in. Comma-
separated list of catalogs. Example-value: 'myfancy_catalog,
another_catalog'. Explaination: Additionally to the default
'portal_catalog' the instances of this class will be catalogged in
the two given catalogs.
content_icon -- The name of an image file, which must be found in the
skins directory of the product. This will be used to represent the
content type in the user interface.
copyright -- You can set the copyright project-wide with the '--
copyright' commandline parameter (or in the config file). This TGV
allows you to use/ overwrite it on a class level.
creation_permission -- Sets the creation permission for the class.
Example: 'Add portal content'.
creation_roles -- You can set an own role who should be able to add a
type. Use an Tuple of Strings. Default and example for this value:
'("Manager", "Owner", "Member")'.
default_interface_type -- default type of interfaces (z2 or z3).
default_view -- The TemplateMixin class in Archetypes allows your class
to present several alternative view templates for a content type.
The default_view value sets the default one. Defaults to
'base_view'. Only relevant if you use TemplateMixin.
description -- A description of the type, a sentence or two in length.
Used to describe the type to the user.
detailed_creation_permissions -- Give the content-type (types in the
package, model) own creation permissions, named automagically
'ProductName: Add ClassName'.
disable_polymorphing -- Normally, archgenxml looks at the parents of
the current class for content types that are allowed as items in a
folderish class. So: parent's allowed content is also allowed in the
child. Likewise, subclasses of classes allowed as content are also
allowed on this class. Classic polymorphing. In case this isn't
desired, set the tagged value 'disable_polymorphing' to 1.
display_in_navigation -- Setting this boolean value adds the type to
'Displayed content types' in the portals navigation settings.
Default is True
doctest_name -- In a tests package, setting the stereotype
'<<doc_testcase>>' on a class turns it into a doctest. The doctest
itself is placed in the doc/ subdirectory. The 'doctest_name' tagged
value overwrites the default name for the file (which is the name of
the doctestcase class + '.txt'). ArchGenXML appends the '.txt'
extension automatically, so you don't need to specify it.
email -- You can set the email project-wide with the '--email'
commandline parameter (or in the config file). This TGV allows you
to use/ overwrite it on a class level.
filter_content_types -- If set to true (1), explicitly turn on the
filter_content_types factory type information value. If this is off,
all globally addable content types will be addable inside a
(folderish) type; if it is on, only those values in the
allowed_content_types list will be enabled. Note that when
aggregation or composition is used to define containment,
filtered_content_types will be automatically turned on.
folder_base_class -- Useful when using the '<<folder>>' stereotype in
order to set the folderish base class.
generate_reference_fields -- Per default (True) navigable reference (or
relation) ends are resulting in a ReferenceField (or RelationField).
Setting this value to False results in not generating
ReferenceFields automagically.
global_allow -- Overwrite the AGX-calculated 'global_allow' setting of
class. Setting it to '1' makes your content type addable everywhere
(in principle), setting it to '0' limits it to places where it's
explicitly allowed as content.
hide_actions -- A comma- or newline-separated list of action ids to
hide on the class. For example, set to 'metadata, sharing' to turn
off the metadata (properties) and sharing tabs.
hide_folder_tabs -- When you want to hide the folder tabs (mostly the
"contents" tab, just set this tagged value to 1.
i18ncontent -- Enables the content type(s) for LinguaPlone. Only
allowed value is 'linguaplone'.
immediate_view -- Set the immediate_view factory type information
value. This should be the name of a page template, and defaults to
'base_view'. Note that Plone at this time does not make use of
immediate_view, which in CMF core allows you to specify a different
template to be used when an object is first created from when it is
subsequently accessed.
import_from -- If you wish to include a class in your model (as a base
class or aggregated class, for example) which is actually defined in
another product, add the class to your model and set the import_from
tagged value to the class that should be imported in its place. You
probably don't want the class to be generated, so add a stereotype
'<<stub>>' as well.
imports -- A list of python import statements which will be placed at
the top of the generated file. Use this to make new field and widget
types available, for example. Note that in the generated code you
will be able to enter additional import statements in a preserved
code section near the top of the file. Prefer using the imports
tagged value when it imports something that is directly used by
another element in your model. You can have several import
statements, one per line, or by adding several tagged values with
the name 'imports'.
index:type -- the type of index used as (string), for example
'FieldIndex', 'KeywordIndex', 'DateIndex' or any available index in
your portal. For known types a default is guessed, such as
FieldIndex for StringFields or DateIndex for DateFields. If no guess
is possible, we assume a FieldIndex.
inherit_allowed_types -- By default, a child type will inherit the
allowable content types from its parents. Set this property to false
(0) to turn this off.
label -- Sets the readable name.
license -- You can set the license project-wide with the '--license'
commandline parameter (or in the config file). This TGV allows you
to use/ overwrite it on a class level.
marshaller -- Specify a marshaller to use for the class' schema.
module -- Like 'module_name', it overwrites the name of the directory
it'd be normally placed in.
module_name -- Like 'module', it overwrites the name of the directory
it'd be normally placed in.
parentclass_first -- if this tgv is set to true generalization parents
are used before the standard base classes (e.g. BaseContent) this
option is sometimes necessary when inheriting from some special
parents (e.g. 'remember' style classes).
parentclasses_first -- if this tgv is set to true generalization
parents are used before the standard base classes (e.g. BaseContent)
this option is sometimes necessary when inheriting from some special
parents (e.g. 'remember' style classes).
portal_type -- Sets the CMF portal-type this class will be registered
with, defaults to the class-name.
read_permission -- Defines archetypes fields read-permission. Use it
together with workflow to control ability to view fields based on
roles/permissions.
register -- 'Remember' related. Set as default member type.
searchable -- Per default a fields 'searchable' property is set to
False. Sometimes you want it for all fields True. This TGV let you
define the default for a class, package or model.
searchable_type -- Setting this boolean value adds the type to 'types
to be searched' in the portals search settings. Default is True
strict -- On a class with the <<interface_doctest>> stereotype: check
for inherited interfaces as well.
suppl_views -- The TemplateMixin class in Archetypes allows your class
to present several alternative view templates for a content type.
The suppl_views value sets the available views. Example:
'("my_view", "myother_view")'. Defaults to '()'. Only relevant if
you use TemplateMixin.
typeDescription -- DEPRECATED. Use 'description' instead.
use_dynamic_view -- Controles wether CMFDynamicViewFTI is used for a
type/class. Boolean, default is True.
use_portal_factory -- This boolean value controls the registration of
the type for use with portal_factory. Default: True.
use_workflow -- Tie the class to the named workflow. A state diagram
(=workflow) attached to a class in the UML diagram is automatically
used as that class's workflow; this tagged value allows you to tie
the workflow to other classes.
version_info -- Add ArchGenXML version information to the generated
file (default is 1).
vocabulary:type -- Enables support for Products 'ATVocabularyManager'
by setting value to 'ATVocabularyManager'.
vocabulary:vocabulary_type -- For use with 'ATVocabularyManager'.
Defaults to 'Simplevocabulary'. Let you define the portal_type of
the vocabulary used as initial vocabulary at Product install time.
If VdexVocabulary is used, the install-script tries to install a
vocabulary from a vdex file names
'Products/PRODUCTNAME/data/VOCABULARYNAME.vdex'.
write_permission -- Defines archetypes fields write-permission. Use it
together with workflow to control ability to write data to a field
based on roles/permissions.
field
description -- Sets a description for this field. It's used for field
documentation while registering inside Archetypes.
label -- Sets the readable name.
validation_expression -- Use an ExpressionValidator and sets the by
value given expression.
validation_expression_errormsg -- Sets the error message to the
ExpressionValidator (use with validation_expression to define the
validation expression to which this error message applies).
method
code -- The actual python code of the method. Only use this for simple
one-liners. Code filled into the generated file will be preserved
when the model is re-generated.
documentation -- You can add documention via this tag; it's better to
use your UML tool's documentation field.
label -- Sets the readable name.
permission -- For method with public visibility only, if a permission
is set, declare the method to be protected by this permission.
Methods with private or protected visiblity are always declared
private since they are not intended for through-the-web unsafe code
to access. Methods with package visibility use the class default
security and do not get security declarations at all.
model
alias -- FTI Alias definition in the form alias=fromvalue,tovalue
association_class -- You can use associations classes to store content
on the association itself. The class used is specified by this
setting. Don't forget to import the used class properly.
association_vocabulary -- Switch, defaults to False. Needs Product
'ATVocabularyManager'. Generates an empty vocabulary with the name
of the relation.
author -- You can set the author project-wide with the '--author'
commandline parameter (or in the config file). This TGV allows you
to use/ overwrite it on a model level.
catalog:index -- Add the field (or all fields of a class, package,
model) to the index. Boolean, 1 or 0. Default is 0. If set, you may
need to provide 'index:*' tagged values too.
catalog:metadata -- Adds the field to the metadata record on the query
result. Boolean, 1 or 0. If you do not provide 'index:attributes',
the name of the accessor of the field is the default. If
'catalog:attributes' is given for each attribute one field at the
record will be created.
catalog:name -- Sometimes you need to add an index to a other catalog
than 'portal_catalog' and its XML-File 'catalog.xml'. Provide a
tuple of comma separated strings, id of the catalog and the filename
of its configuration file. default is "portal_catalog, Plone Catalog
Tool'.
catalogmultiplex:black -- Remove an archetypes class (identified by
meta_type) from one or more catalogs to be cataloged in. Comma-
separated list of catalogs. Example-value: 'portal_catalog,
another_catalog'. Explaination: Instances of the class wont be
catalogged in portal_catalog anymore.
catalogmultiplex:white -- Add an archetypes class (identified by
meta_type) to one or more catalogs to be cataloged in. Comma-
separated list of catalogs. Example-value: 'myfancy_catalog,
another_catalog'. Explaination: Additionally to the default
'portal_catalog' the instances of this class will be catalogged in
the two given catalogs.
copyright -- You can set the copyright project-wide with the '--
copyright' commandline parameter (or in the config file). This TGV
allows you to use/ overwrite it on a model level.
creation_permission -- Sets the creation permission for the class.
Example: 'Add portal content'.
creation_roles -- You can set an own role who should be able to add a
type. Use an Tuple of Strings. Default and example for this value:
'("Manager", "Owner", "Member")'.
default_interface_type -- default type of interfaces (z2 or z3).
default_view -- The TemplateMixin class in Archetypes allows your class
to present several alternative view templates for a content type.
The default_view value sets the default one. Defaults to
'base_view'. Only relevant if you use TemplateMixin.
dependency_step_qi -- Generate Quickinstaller dependency installation
for your product. Boolean (1 or 0), default 0 (off). Dependencies
can be declared in AppConfig.py in a variable DEPENDENCIES.
dependend_profiles -- GenericSetup profiles your product depends on. A
list of profile names separated by commas. This list is used for the
dependencies tag inside the metadata.xml file of the product's
profile
detailed_creation_permissions -- Give the content-type (types in the
package, model) own creation permissions, named automagically
'ProductName: Add ClassName'.
display_in_navigation -- Setting this boolean value adds the type to
'Displayed content types' in the portals navigation settings.
Default is True
email -- You can set the email project-wide with the '--email'
commandline parameter (or in the config file). This TGV allows you
to use/ overwrite it on a model level.
fixtools -- Generate fixTools function in setuphandlers.py. It calls
initializeArchetypes for generated tools, thus reset existing data
in the tools. Boolean (1 or 0), default 0 (off).
generate_reference_fields -- Per default (True) navigable reference (or
relation) ends are resulting in a ReferenceField (or RelationField).
Setting this value to False results in not generating
ReferenceFields automagically.
global_allow -- Overwrite the AGX-calculated 'global_allow' setting of
class. Setting it to '1' makes your content type addable everywhere
(in principle), setting it to '0' limits it to places where it's
explicitly allowed as content.
i18ncontent -- Enables the content type(s) for LinguaPlone. Only
allowed value is 'linguaplone'.
immediate_view -- Set the immediate_view factory type information
value. This should be the name of a page template, and defaults to
'base_view'. Note that Plone at this time does not make use of
immediate_view, which in CMF core allows you to specify a different
template to be used when an object is first created from when it is
subsequently accessed.
imports -- A list of python import statements which will be placed at
the top of the generated file. Use this to make new field and widget
types available, for example. Note that in the generated code you
will be able to enter additional import statements in a preserved
code section near the top of the file. Prefer using the imports
tagged value when it imports something that is directly used by
another element in your model. You can have several import
statements, one per line, or by adding several tagged values with
the name 'imports'.
index:type -- the type of index used as (string), for example
'FieldIndex', 'KeywordIndex', 'DateIndex' or any available index in
your portal. For known types a default is guessed, such as
FieldIndex for StringFields or DateIndex for DateFields. If no guess
is possible, we assume a FieldIndex.
label -- Sets the readable name.
license -- You can set the license project-wide with the '--license'
commandline parameter (or in the config file). This TGV allows you
to use/ overwrite it on a model level.
module -- Like 'module_name', it overwrites the name of the directory
it'd be normally placed in.
module_name -- Like 'module', it overwrites the name of the directory
it'd be normally placed in.
plone_target_version -- The target version of Plone. Defaults to 3.0
Possible values are 2.5 and 3.0
product_description -- The description of the Product. This is placed
as description tag in the metadata.xml file of the product's profile
read_permission -- Defines archetypes fields read-permission. Use it
together with workflow to control ability to view fields based on
roles/permissions.
relation_implementation -- Sets the type of implementation is used for
an association: 'basic' (used as default) for classic style
archetypes references or 'relations' for use of the 'Relations'
Product.
searchable -- Per default a fields 'searchable' property is set to
False. Sometimes you want it for all fields True. This TGV let you
define the default for a class, package or model.
searchable_type -- Setting this boolean value adds the type to 'types
to be searched' in the portals search settings. Default is True
skin_directories -- A comma separated list of subdirectories to be
generated inside the product skins directory. Each of this
directories is prefixed with productname in lowercase. The default
value is "'templates', 'styles', 'images'".
suppl_views -- The TemplateMixin class in Archetypes allows your class
to present several alternative view templates for a content type.
The suppl_views value sets the available views. Example:
'("my_view", "myother_view")'. Defaults to '()'. Only relevant if
you use TemplateMixin.
use_dynamic_view -- Controles wether CMFDynamicViewFTI is used for a
type/class. Boolean, default is True.
use_portal_factory -- This boolean value controls the registration of
the type for use with portal_factory. Default: True.
use_workflow -- Tie the class to the named workflow. A state diagram
(=workflow) attached to a class in the UML diagram is automatically
used as that class's workflow; this tagged value allows you to tie
the workflow to other classes.
version_info -- Add ArchGenXML version information to the generated
file (default is 1).
vocabulary:type -- Enables support for Products 'ATVocabularyManager'
by setting value to 'ATVocabularyManager'.
vocabulary:vocabulary_type -- For use with 'ATVocabularyManager'.
Defaults to 'Simplevocabulary'. Let you define the portal_type of
the vocabulary used as initial vocabulary at Product install time.
If VdexVocabulary is used, the install-script tries to install a
vocabulary from a vdex file names
'Products/PRODUCTNAME/data/VOCABULARYNAME.vdex'.
write_permission -- Defines archetypes fields write-permission. Use it
together with workflow to control ability to write data to a field
based on roles/permissions.
package
alias -- FTI Alias definition in the form alias=fromvalue,tovalue
association_class -- You can use associations classes to store content
on the association itself. The class used is specified by this
setting. Don't forget to import the used class properly.
association_vocabulary -- Switch, defaults to False. Needs Product
'ATVocabularyManager'. Generates an empty vocabulary with the name
of the relation.
author -- You can set the author project-wide with the '--author'
commandline parameter (or in the config file). This TGV allows you
to use/ overwrite it on a package level.
catalog:index -- Add the field (or all fields of a class, package,
model) to the index. Boolean, 1 or 0. Default is 0. If set, you may
need to provide 'index:*' tagged values too.
catalog:metadata -- Adds the field to the metadata record on the query
result. Boolean, 1 or 0. If you do not provide 'index:attributes',
the name of the accessor of the field is the default. If
'catalog:attributes' is given for each attribute one field at the
record will be created.
catalog:name -- Sometimes you need to add an index to a other catalog
than 'portal_catalog' and its XML-File 'catalog.xml'. Provide a
tuple of comma separated strings, id of the catalog and the filename
of its configuration file. default is "portal_catalog, Plone Catalog
Tool'.
catalogmultiplex:black -- Remove an archetypes class (identified by
meta_type) from one or more catalogs to be cataloged in. Comma-
separated list of catalogs. Example-value: 'portal_catalog,
another_catalog'. Explaination: Instances of the class wont be
catalogged in portal_catalog anymore.
catalogmultiplex:white -- Add an archetypes class (identified by
meta_type) to one or more catalogs to be cataloged in. Comma-
separated list of catalogs. Example-value: 'myfancy_catalog,
another_catalog'. Explaination: Additionally to the default
'portal_catalog' the instances of this class will be catalogged in
the two given catalogs.
copyright -- You can set the copyright project-wide with the '--
copyright' commandline parameter (or in the config file). This TGV
allows you to use/ overwrite it on a package level.
creation_permission -- Sets the creation permission for the class.
Example: 'Add portal content'.
creation_roles -- You can set an own role who should be able to add a
type. Use an Tuple of Strings. Default and example for this value:
'("Manager", "Owner", "Member")'.
default_view -- The TemplateMixin class in Archetypes allows your class
to present several alternative view templates for a content type.
The default_view value sets the default one. Defaults to
'base_view'. Only relevant if you use TemplateMixin.
detailed_creation_permissions -- Give the content-type (types in the
package, model) own creation permissions, named automagically
'ProductName: Add ClassName'.
display_in_navigation -- Setting this boolean value adds the type to
'Displayed content types' in the portals navigation settings.
Default is True
email -- You can set the email project-wide with the '--email'
commandline parameter (or in the config file). This TGV allows you
to use/ overwrite it on a package level.
generate_reference_fields -- Per default (True) navigable reference (or
relation) ends are resulting in a ReferenceField (or RelationField).
Setting this value to False results in not generating
ReferenceFields automagically.
global_allow -- Overwrite the AGX-calculated 'global_allow' setting of
class. Setting it to '1' makes your content type addable everywhere
(in principle), setting it to '0' limits it to places where it's
explicitly allowed as content.
i18ncontent -- Enables the content type(s) for LinguaPlone. Only
allowed value is 'linguaplone'.
immediate_view -- Set the immediate_view factory type information
value. This should be the name of a page template, and defaults to
'base_view'. Note that Plone at this time does not make use of
immediate_view, which in CMF core allows you to specify a different
template to be used when an object is first created from when it is
subsequently accessed.
imports -- A list of python import statements which will be placed at
the top of the generated file. Use this to make new field and widget
types available, for example. Note that in the generated code you
will be able to enter additional import statements in a preserved
code section near the top of the file. Prefer using the imports
tagged value when it imports something that is directly used by
another element in your model. You can have several import
statements, one per line, or by adding several tagged values with
the name 'imports'.
index:type -- the type of index used as (string), for example
'FieldIndex', 'KeywordIndex', 'DateIndex' or any available index in
your portal. For known types a default is guessed, such as
FieldIndex for StringFields or DateIndex for DateFields. If no guess
is possible, we assume a FieldIndex.
label -- Sets the readable name.
license -- You can set the license project-wide with the '--license'
commandline parameter (or in the config file). This TGV allows you
to use/ overwrite it on a package level.
module -- Like 'module_name', it overwrites the name of the directory
it'd be normally placed in.
module_name -- Like 'module', it overwrites the name of the directory
it'd be normally placed in.
read_permission -- Defines archetypes fields read-permission. Use it
together with workflow to control ability to view fields based on
roles/permissions.
relation_implementation -- Sets the type of implementation is used for
an association: 'basic' (used as default) for classic style
archetypes references or 'relations' for use of the 'Relations'
Product.
searchable -- Per default a fields 'searchable' property is set to
False. Sometimes you want it for all fields True. This TGV let you
define the default for a class, package or model.
searchable_type -- Setting this boolean value adds the type to 'types
to be searched' in the portals search settings. Default is True
suppl_views -- The TemplateMixin class in Archetypes allows your class
to present several alternative view templates for a content type.
The suppl_views value sets the available views. Example:
'("my_view", "myother_view")'. Defaults to '()'. Only relevant if
you use TemplateMixin.
use_dynamic_view -- Controles wether CMFDynamicViewFTI is used for a
type/class. Boolean, default is True.
use_portal_factory -- This boolean value controls the registration of
the type for use with portal_factory. Default: True.
use_workflow -- Tie the class to the named workflow. A state diagram
(=workflow) attached to a class in the UML diagram is automatically
used as that class's workflow; this tagged value allows you to tie
the workflow to other classes.
version_info -- Add ArchGenXML version information to the generated
file (default is 1).
vocabulary:type -- Enables support for Products 'ATVocabularyManager'
by setting value to 'ATVocabularyManager'.
vocabulary:vocabulary_type -- For use with 'ATVocabularyManager'.
Defaults to 'Simplevocabulary'. Let you define the portal_type of
the vocabulary used as initial vocabulary at Product install time.
If VdexVocabulary is used, the install-script tries to install a
vocabulary from a vdex file names
'Products/PRODUCTNAME/data/VOCABULARYNAME.vdex'.
write_permission -- Defines archetypes fields write-permission. Use it
together with workflow to control ability to write data to a field
based on roles/permissions.
portlet
label -- Sets the readable name.
template_name -- Specify a template for the portlet (without .pt).
Default is the class name. (on classes with the stereotype
<<portlet_class>>)
state
access -- Shortcut for 'Access contents information'.
add -- Shortcut for 'Add portal content'.
delete -- Shortcut for 'Delete objects'.
description -- Sets the state description.
inactive -- Shortcut for 'Access inactive portal content'.
initial_state -- Sets this state to be the initial state. This allows
you to use a normal state in your UML diagram instead of the special
round starting-state symbol.
label -- Sets the readable name.
list -- Shortcut for 'List folder contents'.
modify -- Shortcut for 'Modify portal content'.
review -- Shortcut for 'Review portal content'.
role -- Shortcut for 'Change local roles'.
view -- Shortcut for 'View'.
worklist -- Attach objects in this state to the named worklist. An
example of a worklist is the to-review list.
worklist:guard_permissions -- Sets the permissions needed to be allowed
to view the worklist. Default value is 'Review portal content'. Set
to 'False' for no guard_permission.
worklist:guard_roles -- Sets the roles needed to be allowed to view the
worklist. No default value
state action
after:binding -- Interface to bind the after effect to.
before:binding -- Interface to bind the before effect to.
label -- Sets the readable name.
state machine
bindings -- List of portal-types this workflow should be bound to.
Comma-separated, i.e. 'Document, Image, File'.
default -- A workflow id to be set as the default workflow.
label -- Sets the readable name.
state transition
label -- Sets the readable name.
trigger_type -- Sets the trigger type, following what is defined by
DCWorkflow: automatic user action (default) workflow method
url -- Action URL, need 'PloneWorkflowTransitions' to see it in Plone.
tool
author -- You can set the author project-wide with the '--author'
commandline parameter (or in the config file). This TGV allows you
to use/ overwrite it on a tool level.
autoinstall -- Controls, wether the tool is automatically installed
when your product is installed. Boolean, default is True.
configlet -- Set to true (1) to set up a configlet in the Plone control
panel for your tool.
configlet:condition -- A TALES expression defining a condition which
will be evaluated to determine whether the configlet should be
displayed.
configlet:description -- A description of the configlet.
configlet:icon -- The name of an image file, which must be in your
product's skin directory, used as the configlet icon.
configlet:permission -- A permission which is required for the
configlet to be displayed.
configlet:section -- The section of the control panel where the
configlet should be displayed. One of 'Plone', 'Products' (default)
or 'Member'. **warning**: older documentation versions mentioned
'Members' here.
configlet:title -- The name of the configlet.
configlet:view -- The id of the view template to use when first opening
the configlet. By default, the 'view' action of the object is used
(which is usually base_view)
copyright -- You can set the copyright project-wide with the '--
copyright' commandline parameter (or in the config file). This TGV
allows you to use/ overwrite it on a tool level.
creation_permission -- Sets the creation permission for the class.
Example: 'Add portal content'.
creation_roles -- You can set an own role who should be able to add a
type. Use an Tuple of Strings. Default and example for this value:
'("Manager", "Owner", "Member")'.
default_view -- The TemplateMixin class in Archetypes allows your class
to present several alternative view templates for a content type.
The default_view value sets the default one. Defaults to
'base_view'. Only relevant if you use TemplateMixin.
email -- You can set the email project-wide with the '--email'
commandline parameter (or in the config file). This TGV allows you
to use/ overwrite it on a tool level.
immediate_view -- Set the immediate_view factory type information
value. This should be the name of a page template, and defaults to
'base_view'. Note that Plone at this time does not make use of
immediate_view, which in CMF core allows you to specify a different
template to be used when an object is first created from when it is
subsequently accessed.
imports -- A list of python import statements which will be placed at
the top of the generated file. Use this to make new field and widget
types available, for example. Note that in the generated code you
will be able to enter additional import statements in a preserved
code section near the top of the file. Prefer using the imports
tagged value when it imports something that is directly used by
another element in your model. You can have several import
statements, one per line, or by adding several tagged values with
the name 'imports'.
label -- Sets the readable name.
license -- You can set the license project-wide with the '--license'
commandline parameter (or in the config file). This TGV allows you
to use/ overwrite it on a tool level.
module -- Like 'module_name', it overwrites the name of the directory
it'd be normally placed in.
module_name -- Like 'module', it overwrites the name of the directory
it'd be normally placed in.
suppl_views -- The TemplateMixin class in Archetypes allows your class
to present several alternative view templates for a content type.
The suppl_views value sets the available views. Example:
'("my_view", "myother_view")'. Defaults to '()'. Only relevant if
you use TemplateMixin.
tool_instance_name -- The id to use for the tool. Defaults to
'portal_<name>', where <name> is the class name in lowercase.
toolicon -- The name of an image file, which must be found in the skins
directory of the product. This will be used to represent your tool
in the Zope Management Interface.
unknown
Modify --
access --
allow_empty_rows --
columns --
default:widget:Reference --
default_content_type --
default_output_type --
default_page_type --
i18ncontent --
index_method --
languageIndependent --
max_size --
mode --
pil_quality --
pil_resize_algo --
rename_after_creation --
storage --
swallowResizeExceptions --
widget:addable --
widget:allow_brightness --
widget:allow_browse --
widget:allow_file_upload --
widget:allow_search --
widget:allow_sorting --
widget:append_only --
widget:auto_insert --
widget:available_indexes --
widget:base_query --
widget:checkbox_bound --
widget:cols --
widget:columns --
widget:default_search_index --
widget:destination --
widget:destination_types --
widget:divider --
widget:dollars_and_cents --
widget:ending_year --
widget:force_close_on_insert --
widget:format --
widget:future_years --
widget:history_length --
widget:image_method --
widget:image_portal_types --
widget:maxlength --
widget:nullValueTitle --
widget:omitCountries --
widget:only_for_review_states --
widget:provideNullValue --
widget:restrict_browsing_to_startup_directory --
widget:rows --
widget:search_catalog --
widget:show_hm --
widget:show_indexes --
widget:show_path --
widget:show_review_state --
widget:show_ymd --
widget:size --
widget:starting_year --
widget:startup_directory --
widget:thousands_commas --
widget:visible --
widget:whole_dollars --
view
label -- Sets the readable name.
name -- Specify a name for the zope3 view.. Default is the class name.
(on classes with the stereotype <<view_class>>)
widget
description -- Sets a description for this widget. It's used for widget
documentation while registering inside Archetypes.
label -- Sets the readable name.
macro -- Sets the macro used by the widget. This will be used as the
name of the auto-created page template for the widget.
title -- Sets the widget title. It's used for widget documentation
while registering inside Archetypes.
used_for -- Sets the possible fields which can use this widget. It's
used for widget documentation while registering inside Archetypes.
The list has the form: '"Products.Archetypes.Field.Field1Name",
"Products.Archetypes.Field.FieldName2"'.
3.4.3. Stereotypes
All stereotypes available in its context.
*This file was generated 2009-05-12 with bin/agx_stereotypes 2.4.1. Don't forget to replace << by &lt;&lt; and >> by &gt;&gt; on this page.*
abstraction
adapts -- On a realization, specify a class (<<adapter>>,
<<named_adapter>>, <<extender>>) adapts another class (<<stub>>,
<<interface>>).
class
adapter -- Is a (non-named) adapter.
archetype -- Explicitly specify that a class represents an Archetypes
type. This may be necessary if you are including a class as a base
class for another class and ArchGenXML is unable to determine
whether the parent class is an Archetype or not. Without knowing
that the parent class in an Archetype, ArchGenXML cannot ensure that
the parent's schema is available in the derived class.
atblob -- Turns the class into an plone.app.blob.content.ATBlob
subclass.
atdocument -- Turns the class into an Atdocument subclass.
atevent -- Turns the class into an ATEvent subclass.
atfile -- Turns the class into an ATFile subclass.
atfolder -- Turns the class into an ATFolder subclass.
atimage -- Turns the class into an ATImage subclass.
atlink -- Turns the class into an ATLink subclass.
atnewsitem -- Turns the class into an ATNewsItem subclass.
btree -- Like '<<folder>>', it generates a folderish object. But it
uses a BTree folder for support of large amounts of content. The
same as '<<large>>'.
content_class -- TODO
doc_testcase -- Turns a class into a doctest class. It must subclass a
'<<plone_testcase>>'.
extender -- Is a schema extender supported by
archetypes.schemaextender.
field -- Class will target in a ObjectField or CompoundField (latter if
Attributes are provided)
flavor -- Generates a ContentFlavors flavor from this class.
folder -- Turns the class into a folderish object. When a UML class
contains or aggregates other classes, it is automatically turned
into a folder; this stereotype can be used to turn normal classes
into folders, too.
functional_doc_testcase -- Turns a class into a functional doctest
class. It must subclass a '<<plone_testcase>>'.
functional_testcase -- Turns a class into a functional testcase. It
must subclass a '<<functional_testcase>>'. Adding an interface arrow
to another class automatically adds that class's methods to the
testfile for testing.
hidden -- Generate the class, but turn off "global_allow", thereby
making it unavailable in the portal by default. Note that if you use
composition to specify that a type should be addable only inside
another (folderish) type, then "global_allow" will be turned off
automatically, and the type be made addable only inside the
designated parent. (You can use aggregation instead of composition
to make a type both globally addable and explicitly addable inside
another folderish type).
interface -- Is an interface.
interface_testcase -- Turns a class into a testcase for the interfaces.
large -- Like '<<folder>>', it generates a folderish object. But it
uses a BTree folder for support of large amounts of content. The
same as '<<large>>'.
mixin -- Don't inherit automatically from "BaseContent" and so. This
makes the class suitable as a mixin class. See also '<<archetype>>'.
named_adapter -- Is a named adapter.
odStub -- Prevents a class/package/model from being generated. Same as
'<<stub>>'.
ordered -- For folderish types, include folder ordering support. This
will allow the user to re-order items in the folder manually.
plone_testcase -- Turns a class into the (needed) base class for all
other '<<testcase>>' and '<<doc_testcase>>' classes inside a
'<<test>>' package.
plonefunctional_testcase -- Turns a class into the base class for all
other '<<functionaltestcase>>' classes inside a '<<test>>' package.
portal_tool -- Turns the class into a portal tool.
portlet_class -- Generate this class as a zope3 portlet class instead
of as an Archetypes class.
python_class -- Generate this class as a plain python class instead of
as an Archetypes class.
remember -- The class will be treated as a remember member type. It
will derive from remember's Member class and be installed as a
member data type. Note that you need to install the separate
remember product.
setup_testcase -- Turns a class into a testcase for the setup, with
pre-defined common checks.
stub -- Prevents a class/package/model from being generated.
testcase -- Turns a class into a testcase. It must subclass a
'<<plone_testcase>>'. Adding an interface arrow to another class
automatically adds that class's methods to the testfile for testing.
tool -- Turns the class into a portal tool. Similar to
'<<portal_tool>>'.
variable_schema -- Include variable schema support in a content type by
deriving from the VariableSchema mixin class.
view_class -- Generate this class as a zope3 view class instead of as
an Archetypes class.
vocabulary -- TODO
vocabulary_term -- TODO
widget -- A simple stub archetypes-widget class will be created.
zope_class -- Generate this class as a plain Zope class instead of as
an Archetypes class.
dependency
value_class -- Declares a class to be used as value class for a certain
field class (see '<<field>>' stereotype).
interface
stub -- Prevents a class/package/model from being generated.
z3 -- Generate this interface class as zope 3 interface. This will
inherit from zope.interface.Interface.
method
action -- Generate a CMF action which will be available on the object.
The tagged values "action" (defaults to method name), "id" (defaults
to method name), "category" (defaults to "object"), "label"
(defaults to method name), "condition" (defaults to empty), and
"permission" (defaults to empty) set on the method and mapped to the
equivalent fields of any CMF action can be used to control the
behaviour of the action.
form -- Generate an action like with the '<<action>>' stereotype, but
also copy an empty controller page template to the skins directory
with the same name as the method and set this up as the target of
the action. If the template already exists, it is not overwritten.
noaction -- Disables standard actions, applied to a method out of
'view', 'edit', 'metadata', 'references.
view -- Generate an action like with the '<<action>>' stereotype, but
also copy an empty page template to the skins directory with the
same name as the method and set this up as the target of the action.
If the template exists, it is not overwritten.
model
odStub -- Prevents a class/package/model from being generated. Same as
'<<stub>>'.
stub -- Prevents a class/package/model from being generated.
operation
action -- Generate a CMF action which will be available on the object.
The tagged values "action" (defaults to method name), "id" (defaults
to method name), "category" (defaults to "object"), "label"
(defaults to method name), "condition" (defaults to empty), and
"permission" (defaults to empty) set on the method and mapped to the
equivalent fields of any CMF action can be used to control the
behaviour of the action.
form -- Generate an action like with the '<<action>>' stereotype, but
also copy an empty controller page template to the skins directory
with the same name as the method and set this up as the target of
the action. If the template already exists, it is not overwritten.
noaction -- Disables standard actions, applied to a method out of
'view', 'edit', 'metadata', 'references.
view -- Generate an action like with the '<<action>>' stereotype, but
also copy an empty page template to the skins directory with the
same name as the method and set this up as the target of the action.
If the template exists, it is not overwritten.
package
odStub -- Prevents a class/package/model from being generated. Same as
'<<stub>>'.
stub -- Prevents a class/package/model from being generated.
tests -- Treats a package as test package. Inside such a test package,
you need at a '<<plone_testcase>>' and a '<<setup_testcase>>'.
3.5. Step by Step Instructions
Step by step instructions for common tasks while developing with ArchGenXML.
3.5.1. Setup of a Testcase Environment using UML and ArchGenXML
Environment for test-driven and architecture centric development.
TODO: Check if it works in 2.0. Add some information how to run the tests.
About Testing
Since development is going on at many places in the Plone system,
it is important to have a way ready to *test* if the software
you wrote is affected by the changes. And, more important the other
way around, if your work, in case it makes it way into the Plone
core or other add-on products, affects the work of *others*.
There are several documents available to read on why testing is
important, so feel free to have a look:
* "Testing in Plone - Introduction":http://plone.org/documentation/tutorial/testing/introduction
* "Best Practices for Plone development - Unit Testing":http://plone.org/documentation/tutorial/best-practices/unit-testing
* "RichDocument Tutorial - Unit testing":http://plone.org/documentation/tutorial/richdocument/unit-testing
and there are many more: Try the *testing*. Some of these documents
are describing in detail how to write the tests itself, which this
manual page is not intended for.
Testing and UML/ ArchGenXML
This document describes the few steps necessary to setup your testing
environment when using an UML diagram and ArchGenXML. It generates
your projects with the test infrastructure and you can focus on writing
the test itself
ArchGenXML provides a pre-configured testing environment - no more
hand-work to create it !
Steps:
1 Check if "PloneTestCase":http://plone.org/products/plonetestcase/
product was shipped with your Plone. If not install it in the version
for your Plone.
2 Create a package in your model and name it 'tests' and give it the
stereotype '<<tests>>'
3 Inside the new tests package create class 'MyFancyTestcase' (in the
uml below named 'testPlone') and give it the stereotype '<<plone_testcase>>'.
This is your main testcase class.
4 Create an additional class inside the test package, call it 'testSetup', and
give it the stereotype '<<setup_testcase>>'. Let it derive from the main testcase
class using the generalization arrow.
5 Testing methods/behaviour of archetypes classes: Create a class inside the
test package. Give it the stereotype '<<testcase>>' or '<<doc_testcase>>'.
Make them derive from main testcase class using the generalization arrow.
On a testcase class you can add methods starting with 'test' such
as 'testMyFancyFeature'. After code generation you just need to fill
in your test code. A doc_testcase class creates an empty
doctest text-file in the '/docs' directory of your product. There are some
tagged value available to control the testcase in detail. Please look at the
chapter *Reference - Tagged Values* for more information.
6 To generate all imports and some startup code, you can use the dependency arrow
from the testcase class to the archetypes class.
7 You can repeat 5 and 6 for every class you want to include in yout test. You
can organize your tests also different, like one integration test, as you like.
8 Generate and run the tests. (TODO: steps needed to get the test fly, such as
'zopectl test' or setting SOFTWAREHOME and INSTANCEHOME environment.
"sample UML":img:uml-testcase.png
These are the basic steps necessary to get it running.
3.6. Recipes and Tips
A collection of recipes for real world use cases and tips for making life easier.
3.6.1. Using the config file to get shorter tagged values
Some tagged values can get quite lengthy. If you use such a lengthy value a few times, you can store it in your project's config file.
An example of long tagged values are the permissions you set on workflow states. A key 'view' with value 'Manager, Member, Reviewer' for instance.
In tagged values, the text you type in is normally taken as a string. If you prefix your value with 'python:', it is copy-pasted literally into your code. So 'python:["a", "b"]' is put into your code as '["a", "b"]'.
The config file
ArchGenXML generates a 'config.py' in your Product's root directory, which in turn tries to import 'AppConfig.py'. So stuff you put in there is treated as if it is placed in the main config file.
Every ArchGenXML-generated file contains an import like 'from Products.YourProduct.config import *', so the variables defined in your AppConfig are directly available in all the files. This means that *you can specify shortcuts* for the tagged values.
Shorter tagged values
Example line in your 'AppConfig.py'::
EDITORS = 'Manager, Member, Reviewer'
Remember that we can use 'python:' to paste raw python code directly into the generated files. After adding above line, the original tagged value 'view' with value 'Manager, Member, Reviewer' can be shortened to the tagged value 'view' with value 'python:EDITORS'. Now that's handy :-) And if you need to change this definition you have it at a central place. No need to touch 2, 3 or more 'states' in UML where its used several times on each.
You can use this little *feature* almost everywhere, so its not reduced to workflow.
4. Portlets
An introduction to the portlets architecture of Plone 3, with practical examples.
4.1. What's a portlet?
This manual covers what a developer needs to know to create new portlet types or customise existing ones.
Portlets are chunks of information that can be shown outside the main area of a page. They are usually boxes of different kinds which content editors can add, set properties of and policies for showing.
(Screenshot).
Differences with viewlets
A portlet is like a viewlet but with persistent configuration (i.e. persistent in the ZODB) and run-time changeable assignments.
Use a viewlet for:
- General content which is always displayed, for example: breadcrumbs,
the logo, or the footer. This is not limited to only visible elements
but can also include CSS, javascript, etc. (actually, that's how ResourceRegistries work).
- Displaying elements based on the interface provided by the current context.
Use a portlet when:
- You need to specify the configuration data for an item. - i.e. number of entries to show.
- You want to give the content editors a choice about when and where to display it.
- You want to display it only from inside a specific folder.
- You'd like to show it only to some groups or users - e.g. Review portlet only is shown to users within the Reviewers group.
4.2. Basic plone.portlets architecture
This section describes the general architecture of a portlet through an example. You can checkout the example code from the collective .
The use case
As an example, we will develop a portlet to display the last n (where n is a positive integer ;) modified content items to logged-in users, which will be available to add it to any portlet manager (left or right column by default).
[screenshot follows]
The configuration data
When a portlet is first created, there are often customizations which can be made which tailor the portlet's behaviour to meet the user's needs: eg. which content type to display, how many items to list, etc... In our example, we want the person configuring the portlet to be able to specify how many of the most recent items will be displayed inside the portlet.
First, we have to describe the interface schema of the configuration data we want to store using zope.schema (see this page for more info on schemas). By convention, this interface derives from IPortletDataProvider, which is just a marker interface. In the package's interfaces.py file, type:
from plone.portlets.interfaces import IPortletDataProvider
from Products.CMFPlone import PloneMessageFactory as _
class IRecentPortlet(IPortletDataProvider):
count = schema.Int(title=_(u'Number of items to display'),
description=_(u'How many items to list.'),
required=True,
default=5)
The PloneMessageFactory makes our code ready to be localized using the Plone i18n machinery.
After defining the configuration schema interface, we implement it in a class called the Assignment class. This is a persistent "content" class which stores the persistent configuration data (if any) of the portlet. Even when a portlet is not configurable, it needs to have an Assignment class, because the presence of an Assignment instance in various places is what determines what portlets show up where.
The Assignment class has a title attribute that is used in the portlet management UI to distinguish different instances of the portlet.
from plone.app.portlets.portlets import base
from zope.interface import implements
from ploneexample.portlet.interfaces import IRecentPortlet
class Assignment(base.Assignment):
implements(IRecentPortlet)
def __init__(self, count=5):
self.count = count
@property
def title(self):
return _(u"Recent items")
The add and edit forms
To add the portlet and edit its configuration, we have to define appropiate add and edit forms.
This is typically done using zope.formlib and the portlet
schema, together with some base form classes to save us from designing
the forms template and logic ourselves. If the portlet is not
configurable, this can use the special base.NullAddForm, which is just a view that creates the portlet and then redirects back to the portlet management screen.
For more information about zope.formlib, check this tutorial.
The edit form can be omitted if the portlet configuration is not editable.
from zope.formlib import form
class AddForm(base.AddForm):
form_fields = form.Fields(IRecentPortlet)
label = _(u"Add Recent Portlet")
description = _(u"This portlet displays recently modified content.")
def create(self, data):
return Assignment(count=data.get('count', 5))
class EditForm(base.EditForm):
form_fields = form.Fields(IRecentPortlet)
label = _(u"Edit Recent Portlet")
description = _(u"This portlet displays recently modified content.")
As it can be seen above, the add form must return an Assignment instance of the portlet.
The portlet presentation
Next, we define how the portlet will be rendered.
The Portlet Renderer is the "view" of the portlet. This is just a
content provider (in the zope.contentprovider sense), in that it has an
update() and a render() method, which will be called upon the rendering of the portlet.
It's a multi-adapter that takes a number of parameters which makes it possible to vary the rendering of the portlet:
- context
- The current content object. Mind the type of content object that's being shown.
- request
- The current request. Mind the current theme/browser layer.
- view
- The current (full page) view. Mind the current view, and whether or not
this is the canonical view of the object (as indicated by the
IViewViewmarker interface) or a particular view, like the manage-portlets view. - manager
- The portlet manager where this portlet was rendered (for now, think of a portlet manager as a column). Mind where in the page the portlet was rendered.
- data
- The
portlet data, which is basically an instance of the portlet assignment
class. Mind the configuration of the portlet assignment.
The Renderer base class relieves us from having to remember all these parameters.
The Renderer class must have an available property,
which is used to determine whether this portlet should be shown or not.
Note you shouldn't include checks for the user id, group or
content-type here, since you can perform these assignments later by
registering the portlet under a certain category (more on this later).
from plone.memoize.instance import memoize
from zope.component import getMultiAdapter
from Acquisition import aq_inner
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
class Renderer(base.Renderer):
_template = ViewPageTemplateFile('recent.pt')
def __init__(self, *args):
base.Renderer.__init__(self, *args)
context = aq_inner(self.context)
portal_state = getMultiAdapter((context, self.request), name=u'plone_portal_state')
self.anonymous = portal_state.anonymous() # whether or not the current user is Anonymous
self.portal_url = portal_state.portal_url() # the URL of the portal object
# a list of portal types considered "end user" types
self.typesToShow = portal_state.friendly_types()
plone_tools = getMultiAdapter((context, self.request), name=u'plone_tools')
self.catalog = plone_tools.catalog()
def render(self):
return self._template()
@property
def available(self):
"""Show the portlet only if there are one or more elements."""
return not self.anonymous and len(self._data())
def recent_items(self):
return self._data()
def recently_modified_link(self):
return '%s/recently_modified' % self.portal_url
@memoize
def _data(self):
limit = self.data.count
return self.catalog(portal_type=self.typesToShow,
sort_on='modified',
sort_order='reverse',
sort_limit=limit)[:limit]
When reading the previous code, note that:
plone_portal_stateandplone_toolsare helper views providing some useful attributes to gather information from.- The
memoizedecorator is used here to cache the results of the catalog query to avoid the perfomance hit of re-generating them in each request. See the plone.memoize doctests for more information.
Registering the portlet
A convenient ZCML directive is provided to glue all components of the portlet in the Zope Component Architecture. In the package's configure.zcml file (or any other ZCML file included from it), write:
<configure
xmlns:five="http://namespaces.zope.org/five"
xmlns:plone="http://namespaces.plone.org/plone"
i18n_domain="ploneexample.portlet">
<five:registerPackage package="." initialize=".initialize" />
<include package="plone.app.portlets"/>
<plone:portlet
name="ploneexample.portlet.Recent"
interface=".recent.IRecentPortlet"
assignment=".recent.Assignment"
renderer=".recent.Renderer"
addview=".recent.AddForm"
editview=".recent.EditForm"
/>
</configure>
Note you have to define/reference the plone XML namespace for the directive to work. There is also a <plone:portletRenderer /> directive to override the renderer for a particular context/layer/view/manager.
You can see the descriptions of all these directives together with their arguments in the metadirectives.py file of the plone.app.portlets package.
This ZCML directive is read at the Zope startup, so to register each class appropiately into the Component Architecture, but you won't be able to add your new portlet yet. You first need to install its portlet type into your Plone site, as described in the section which follows.
Installing the portlet
The components and registration above make a new type of portlet available for installation. To install the portlet type into a particular Plone site, use GenericSetup.
First, register a new GenericSetup extension profile using a registerProfile ZCML directive:
<configure
xmlns:five="http://namespaces.zope.org/five"
xmlns:plone="http://namespaces.plone.org/plone"
xmlns:gs="http://namespaces.zope.org/genericsetup"
i18n_domain="ploneexample.portlet">
<five:registerPackage package="." initialize=".initialize" />
<include package="plone.app.portlets"/>
<gs:registerProfile
name="ploneexample.portlet"
title="Recent Items Example"
directory="profiles/default"
description="An example portlet"
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
<plone:portlet
name="ploneexample.portlet.Recent"
interface=".recent.IRecentPortlet"
assignment=".recent.Assignment"
renderer=".recent.Renderer"
addview=".recent.AddForm"
editview=".recent.EditForm"
/>
</configure>
Next, create the folder profiles/default and place a portlets.xml file inside with the following content:
<?xml version="1.0"?>
<portlets
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="plone">
<portlet
addview="ploneexample.portlet.Recent"
title="Recent items Example"
description="An example portlet which can render a listing of recently changed items."
i18n:attributes="title title_recent_portlet;
description description_recent_portlet">
<for interface="plone.app.portlets.interfaces.IColumn" />
<for interface="plone.app.portlets.interfaces.IDashboard" />
</portlet>
</portlets
When this is run, it will create a local utility in the Plone site of the IPortletType. This just holds some metadata about the portlet for UI purposes.
Title and description should be self-explanatory.
The addview is the name of the view used to add the
portlet, which helps the UI to invoke the right form when the user asks
to add the portlet. This should match the portlet name.
for is an interface or list of interfaces that describe
the type of portlet managers that this portlet is suitable for. This
means that we can install a portlet that's suitable for the dashboard,
say, but not for the general columns. In this case, we're making the
portlet suitable for the dashboard and for any (either left or right)
column. Current portlet manager interfaces include IColumn, ILeftColumn, IRightColumn and IDashboard, all of them defined inside the plone.app.portlets package.
Again, this is primarily about helping the UI construct appropriate menus.
4.3. Testing the portlet
Ensure everything's working as it should.
If the portlet was registered and installed correctly, it should now
show up in the list of portlets available for addition into the type of
portlet managers specified in the for argument of the portlet type (IColumn and IDashboard in our case), under the @@manage-portlets view (Manage Portlets link).
However, to ensure everything's working as it should without having to test it through the web, we can write some integration tests. This is recommended practice in the Plone universe. Moreover, once you've understood how the portlet infrastructure and its API work, you will be able to write tests first (you can copy&paste tests from other portlets products) and then start coding the portlet. More info on testing in the Testing in Plone tutorial.
Run them using bin/instance test -s ploneexample.portlet
4.4. How and where are Portlet Assignments stored?
When you choose a portlet to be displayed somewhere, for example, using the interface that appears when you hit the Manage Portlets button, what you're doing is storing a persistent instance of the Portlet Assignment class into your site, together with all its associated configuration data.
Portlet Assignments are stored in what's called an Assignment Mapping. This is an ordered container with a dict-like interface. The keys are unique string names, and the values are instances of the assignment class.
Assignment mappings can be stored in two different kinds of locations depending on their type: site-wide or contextual.
Site-wide
Site-wide assigned portlets are shown in the whole site, unless
blocked. They're stored in Portlet Managers. Portlet Managers define a
column or other area that can be filled with portlets, and are
analogous to the viewlet manager for viewlets. They are named
persistent local utilites providing the IPortletManager interface.
You can look up a portlet manager like this:
manager = getUtility(IPortletManager, name=u"plone.leftcolumn")
By default, there are two standard portlet managers, plone.leftcolumn and plone.rightcolumn, as well as four portlet managers for the four columns on the dashboard, from plone.dashboard1 to plone.dashboard4. You can create your own in portlets.xml like this:
<portletmanager name="my.package.myportletmanager" type="my.package.interfaces.IMyPortletManagerType" />
The "type" is a marker interface that can be used to install
particular portlets only for particular types of portlet managers, as
explained above. Example: plone.app.portlets.interfaces.IDashboard.
Portlets in global categories (site-wide) are stored directly inside the IPortletManager
utility, under a particular category - e.g. "group" - a
category-specific key - e.g. the group id - and finally a unique
portlet id. Putting this together, we could access a particular portlet
assignment like this:
from plone.portlet.constants import GROUP_CATEGORY manager = getUtility(IPortletManager, name=u"plone.leftcolumn") recent_assignment = manager[GROUP_CATEGORY][u"Administrators"][u"recent"]
Here we look up the left column portlet manager and get the portlet assignment named recent assigned to the Administrators group.
Each of the lookups here has a dict interface, so you can iterate, call keys()
and so on. You can store assignments under any string as category, but
the default portlet retriever is only aware of the three site-wide
assignment categories defined as constants in plone.portlet.constants, USER_CATEGORY, GROUP_CATEGORY and CONTENT_TYPE_CATEGORY, which should be enough for most use-cases. More on portlet retrievers later.
Contextual
Location-specific portlet assignments are stored on annotations on objects providing the ILocalPortletAssignable marker interface.
To get hold of the assignment in this case, we multi-adapt the content object and the manager instance to the IPortletAssignment interface, like so:
manager = getUtility(IPortletManager, name=u"plone.leftcolumn") assignment_mapping = getMultiAdapter((context, manager), IPortletAssignmentMapping) news_portlet = assignment_mapping[u"news"]
There are two functions in plone.app.portlets.utils to make it
easier to find the appropriate mapping for a portlet, or to get a
portlet assignment directly: assignment_mapping_from_key() and assignment_from_key().
We can use GenericSetup to assign portlets to particular portlet managers upon the installation of a product. Read the Theme Reference Manual for info about how to do that. Read the Generic Setup tutorial for further info about what's GenericSetup and how it works.
4.5. How are portlets rendered?
The process to find, update and render portlets from the main views is rather complex. Here we describe how does it all work, step by step.
Portlets are always rendered inside a portlet manager. From a template, we can ask a portlet manager to render itself and all its portlets. This is achieved using a zope.contentprovider 'provider:' expression. In Plone's main_template, for example, you will find:
<tal:block replace="structure provider:plone.leftcolumn" />
Behind the scenes, this will look up a local adapter on (context, request, view) with name plone.leftcolumn (this is just how the provider expression works).
As it happens, this local adapter factory was registered when the portlet manager was installed (via portlets.xml), and is a callable that returns an IPortletManagerRenderer. The portlet manager renderer is the "view" of the portlet manager.
The default implementation will simply output each portlet wrapped
in a div tag with some helpful attributes to support AJAX via KSS. You
can of course register your own portlet manager renderers. A portlet
manager renderer is a multi-adapter on (context, request, view,
manager). The @@manage-portlets view, for example, relies
on a portlet manager renderer override for this particular view that
renders the add/move/delete operations. For most people, the standard
renderer will suffice, though.
The portlet manager renderer asks an IPortletRetriever
to fetch and order the portlet assignments that it should render. This
is a multi-adapter on (context, manager), which means that the fetch
algorithm can be overridden either based on the type of content object
being viewed, or the particular manager. There are two default
implementations - one for "placeful" portlet managers (those which know
about contextual portlets, such as the standard left/right column ones)
and one for "placeless" ones that only deal in global categories. This
latter retriever is used by the dashboard, which stores its portlets in
a global "user" category.
The IPortletRetriever algorithm is reasonably complex,
especially when contextual blacklisting/blocking is taken into account
(see below). To make it possible to re-use this algorithm across
multiple configurations, it is written in terms of an IPortletContext. The context content object will be adapted to this interface. The portlet context provides:
- A universal identifier for the current context (usually just the physical path) - the
uidproperty. - A
way to obtain the parent object of the current context (for acquiring
portlets and blacklist information in a placeful retriever) - the
getParent()method. - A list of global portlet categories to look up, and the keys to look under (obtainable by using the
globalPortletCategories()method on the adapted context).
The last parameter is best described by an example. Let's say we're
logged in as "testuser", a member of both the "Administrators" and
"Reviewers" groups, and were looking at a Folder. The return value of globalPortletCategories() would then be:
>>> portlet_context.globalPortletCategories()
[("content_type", "Folder",),
("group", "Administrators",),
("group", "Reviewers",),
("user", "testuser",)]
This informs the retriever that it should first look up any portlets
in the current portlet manager in the "content_type" category under the
"Folder" key, and then portlets in the "group" category under the
"Administators" and "Reviewers" key, and finally portlets in the "user"
category under the "testuser" key, all in that order. Thus, if we
wanted to add a new category, or change the order of categories, we
could override the IPortletContext, either everywhere or just for one particular type of context.
Once the IPortletRetriever has retrieved the
assignments that should be shown for the current portlet manager, the
portlet manager renderer will look up the portlet renderer for each
assignment, ensure that it should indeed be rendered by checking its available property, and finally call update() and render(), placing the output in the reponse.
4.6. Appendix: Practicals
4.6.1. Subclassing new portlets
This how-to briefly explains how to create new portlets based on another existing portlet class. (Mikko Ohtama)
5. Indexing and searching
How to index and look for content into your Plone site using the Catalog Tool.
5.1. Introduction to ZCatalogs and the Catalog Tool
A brief introduction to ZCatalogs, the Catalog Tool and what they're used for.
Why ZCatalogs?
Plone is built on the CMF, which uses the ZODB to store content in a very free-form manner with arbitrary hierarchy and a lot of flexibility in general. For some content use cases, however, it is very useful to treat content as more ordered, or tabular. This is where ZCatalog comes in.
Searching, for example, requires being able to query content on structured data such as dates or workflow states. Additionally, query results often need to be sorted based on structured data of some sort. So when it comes to searching it is very valuable to treat our free-form persistent ZODB objects as if they were more tabular. ZCatalog indexes do exactly this.
Since the ZCatalog is in the business of treating content as tabular when it isn't necessarily so, it is very tolerant of any missing data or exceptions when indexing. For example, Plone includes "start" and "end" indexes to support querying events on their start and end dates. When a page is indexed, however, it doesn't have start or end dates. Since the ZCatalog is tolerant, it doesn't raise any exception when indexing the start or end dates on a page. Instead it simply doesn't include pages in those indexes. As such, it is appropriate to use indexes in the catalog to support querying or sorting when not all content provides the data indexed.
This manual is intended to be a brief start guide to ZCatalogs, specially aimed to tasks specific to Plone, and will not treat advanced ZCatalogs concepts in depth. If you want to learn more about ZCatalogs in the context of Zope, please refer to The Zope Book, Searching and Categorizing Content. If you want to perform advanced searches, AdvancedQuery, which is included with Plone since the 3.0 release, is what you're looking for. See Searching with AdvancedQuery for a brief introduction.
Quick start
Every ZCatalog is composed of indexes and metadata. Indexes are fields you can search by, and metadata are copies of the contents of certain fields which can be accessed without waking up the associated content object.
Most indexes are also metadata fields. For example, you can search objects by Title and then display the Title of each object found without fetching them, but note not all indexes need to be part of metadata.
When you search inside the catalog, what you get as a result is a list of elements known as brains. Brains have one attribute for each metadata field defined in the catalog, in addition to some methods to retrieve the underlying object and its location. Metadata values for each brain are saved in the metadata table of the catalog upon the (re)indexing of each object.
Brains are said to be lazy for two reasons; first, because they are only created 'just in time' as your code requests each result, and second, because retrieving a catalog brain doesn't wake up the objects themselves, avoiding a huge perfomance hit.
To see the ZCatalogs in action, fire up your favourite browser and open the ZMI. You'll see an object in the root of your Plone site named portal_catalog. This is the Catalog Tool, a Plone tool (like the Membership Tool or the Quickinstaller Tool) based on ZCatalogs created by default in every Plone site which indexes all the created content.
Open it and click the Catalog tab, at the top of the screen. There you can see the full list of currently indexed objects, filter them by path, and update and remove entries. If you click on any entry, a new tab (or window) will open showing the metadata and index values for the selected indexed object. Note that most fields are "duplicated" in the Index Contents and Metadata Contents tables, but its contents have different formats, because, as it was said earlier, indexes are meant to search by them, and metadata to retrieve certain attributes from the content object without waking it up.
Back to the management view of the Catalog Tool, if you click the Indexes or the Metadata tab you'll see the full list of currently available indexes and metadata fields, respectively, its types and more. There you can also add and remove indexes and metadata fields. If you're working on a test environment, you can use this manager view to play with the catalog, but beware indexes and metadata are usually added through GenericSetup and not using the ZMI.
5.2. Querying the catalog
How to search and list content by title, description, interface, location, etc.
The Catalog Tool has an easy and clean API to search for content. First of all, you need to acquire it from the current context. Here context can be any object in the site:
from Products.CMFCore.utils import getToolByName catalog = getToolByName(context, 'portal_catalog')
To search for something and get the resulting brains, write:
results = catalog.searchResults(**kwargs)
Where kwargs is a dictionary of index names and their associated query values. Only the indexes that you care about need to be included. This is really useful if you have variable searching criteria, for example, coming from a form where the users can select different fields to search for. For example:
results = catalog.searchResults({'portal_type': 'Event', 'review_state': 'pending'})
It is worth pointing out at this point that the indexes that you include are treated as a logical AND, rather than OR. In other words, the query above will find all the items that are both an Event, AND in the review state of pending.
Additionally, you can call the catalog tool directly, which is equivalent to calling catalog.searchResults():
results = catalog(portal_type='Event')
Available indexes
To see the full list of available indexes in your catalog, open the ZMI (what usually means navigating to http://yoursiteURL/manage) look for the portal_catalog object tool into the root of your Plone site and check the Indexes tab. Note that there are different types of indexes, and each one admits different types of search parameters, and behave differently. For example, FieldIndex and KeywordIndex support sorting, but ZCTextIndex doesn't. To learn more about indexes, see The Zope Book, Searching and Categorizing Content.
Some of the most commonly used ones are:
- Title
- The title of the content object.
- Description
- The description field of the content.
- Subject
- The keywords used to categorize the content. Example:
catalog.searchResults(Subject=('cats', 'dogs')) - portal_type
- As its name suggest, search for content whose portal type is indicated. For example:
catalog.searchResults(portal_type='News Item')
You can also specify several types using a list or tuple format:
catalog.searchResults(portal_type=('News Item', 'Event')) - review_state
- The current workflow review state of the content. For example:
catalog.searchResults(review_state='pending')
- object_provides
- From Plone 3, you can search by the interface provided by the content. Example:
from Products.MyProduct.path.to import IIsCauseForCelebration catalog(object_provides=IIsCauseForCelebration.__identifier__)
Searching for interfaces can have some benefits. Suppose you have several types, for example, event types like Birthday, Wedding and Graduation, in your portal which implement the same interface (for example,IIsCauseForCelebration). Suppose you want to get items of these types from the catalog by their interface. This is more exact than naming the types explicitly (like portal_type=['Birthday','Wedding','Graduation' ]), because you don't really care what the types' names really are: all you really care for is the interface.
This has the additional advantage that if products added or modified later add types which implement the interface, these new types will also show up in your query.
Sorting and limiting the number of results
To sort the results, use the sort_on and sort_order arguments. The sort_on argument accepts any available index, even if you're not searching by it. The sort_order can be either 'ascending' or 'descending', where 'ascending' means from A to Z for a text field. 'reverse' is an alias equivalent to 'descending'. For example:
results = catalog_searchResults(Description='Plone documentation',
sort_on='sortable_title', sort_order='ascending')
The catalog.searchResults() returns a list-like object, so to limit the number of results you can just use Python's slicing. For example, to get only the first 3 items:
results = catalog_searchResults(Description='Plone documentation')[:3]
In addition, ZCatalogs allow a sort_limit argument. The sort_limit is only a hint for the search algorhitms and can potentially return a few more items, so it's preferable to use both sort_limit and slicing simultaneously:
limit = 50
results = catalog_searchResults(Description='Plone documentation',
sort_limit=limit)[:limit]
Searching for content within a folder
Use the 'path' argument to specify the physical path to the folder you want to search into.
By default, this will match objects into the specified folder and all existing sub-folders. To change this behaviour, pass a dictionary with the keys 'query' and 'depth' to the 'path' argument, where
- 'query' is the physical path, and
- 'depth' can be either 0, which will return only the brain for the path queried against, or some number greater, which will query all items down to that depth (eg, 1 means searching just inside the specified folder, or 2, which means searching inside the folder, and inside all child folders, etc).
The most common use case is listing the contents of an existing folder, which we'll assume to be the context object in this example:
folder_path = '/'.join(context.getPhysicalPath())
results = catalog(path={'query': folder_path, 'depth': 1})
Getting the underlying object, its path, and its URL from a brain
As it was said earlier, searching inside the catalog returns catalog brains, not the object themselves. If you want to get the object associated with a brain, do:
brain.getObject()
To get the path of the object without fetching it:
brain.getPath()
which is equivalent to obj.getPhysicalPath().
And finally, to get the URL of the underlying object, usually to provide a link to it:
brain.getURL()
which is equivalent to obj.absolute_url().
5.3. Configuring Catalogs with GenericSetup
Adding, removing and changing indexes and metadata.
The Catalog Tool can be configured through the ZMI or programatically in Python but current best practice in the CMF world is to use GenericSetup to configure it using the declarative catalog.xml file. The GenericSetup profile for Plone, for example, uses the CMFPlone/profiles/default/catalog.xml XML data file to configure the Catalog Tool when a Plone site is created. It is fairly readable so taking a quick look through it can be very informative.
When using a GenericSetup extension profile to customize the Catalog Tool in your portal, you only need to include XML for the pieces of the catalog you are changing. To add an index for the Archetypes location field, as in the example above, a policy package could include the following profiles/default/catalog.xml:
<?xml version="1.0"?> <object name="portal_catalog" meta_type="Plone Catalog Tool"> <index name="location" meta_type="FieldIndex"> <indexed_attr value="location"/> </index> </object>
The GenericSetup import handler for the Catalog Tool also supports removing indexes from the catalog if present using the "remove" attribute of the <index> element. To remove the "start" and "end" indexes used for events, for example, a policy package could include the following profiles/default/catalog.xml:
<?xml version="1.0"?> <object name="portal_catalog" meta_type="Plone Catalog Tool"> <index name="start" remove="True" /> <index name="end" remove="True" /> </object>
Care must be taken when setting up indexes with GenericSetup - if the import step for a catalog.xml is run a second time (for example when you reinstall the product), the indexes specified will be destroyed, losing all currently indexed entries, and then re-created fresh (and empty!). If you want to workaround this behaviour, you can either update the catalog afterwards or add the indexes yourself in Python code using a custom import handler.
5.4. Custom indexing strategies
How to add special logic to indexing.
Sometimes you want to index "virtual" attributes of an object computed from existing ones, or just want to customize the way certain attributes are indexed, for example, saving only the 10 first characters of a field instead of its whole content.
To do so in an elegant and flexible way, Plone 3.3 includes a new package, plone.indexer, which provides a series of primitives to delegate indexing operations to adapters.
Let's say you have a content-type providing the interface IMyType. To define an indexer for your type which takes the first 10 characters from the body text, just type (assuming the attribute's name is 'text'):
from plone.indexer.decorator import indexer @indexer(IMyType) def mytype_description(object, **kw): return object.text[:10]
Finally, register this factory function as a named adapter using ZCML. Assuming you've put the code above into a file named indexers.py:
<adapter name="description" factory=".indexers.mytype_description" />
And that's all! Easy, wasn't it?
Note you can omit the for attribute because you passed this to the @indexer decorator, and you can omit the provides attribute because the thing returned by the decorator is actually a class providing the required IIndexer interface.
To learn more about the plone.indexer package, read its doctest.
For more info about how to create content-types, refer to the Archetypes Developer Manual.
Important note: If you want to adapt a out-of-the-box Archetypes content-type like Event or News Item, take into account you will have to feed the indexer decorator with the Zope 3 interfaces defined in Products.ATContentTypes.interface.* files, not with the deprecated Zope 2 ones into the Products.ATContentTypes.interfaces file.
5.5. Searching with AdvancedQuery
A brief primer on using AdvancedQuery to simplify searches that are otherwise hard with plain ZCatalog
AdvancedQuery is an excellent product that overcomes several of the more cumbersome limitations otherwise present with plain ZCatalog queries. The comprehensive documentation is available here.
If you want to install it, require it in your add-on product's setup.py:
install_requires=[
'setuptools',
'Products.AdvancedQuery',
AdvancedQuery is straightforward to use. In the simplest scenario, it can simply duplicate the action of running a normal ZCatalog query:
from Products.CMFCore.utils import getToolByName
cat = getToolByName(context, 'portal_catalog')
aq = cat.makeAdvancedQuery({'portal_type' : 'Event', 'review_state' : 'pending'})
brains = cat.evalAdvancedQuery(aq)
At this stage, all it looks like is a slightly more complicated way of doing things that you already know how to do. However, AdvancedQuery comes into its own by making possible things that are otherwise very hard to do with plain ZCatalog queries. For example, we want to get all published Documents sorted first by Creator, and sub-sorted by date of publication:
from Products.CMFCore.utils import getToolByName
cat = getToolByName(context, 'portal_catalog')
aq = cat.makeAdvancedQuery({'portal_type' : 'Document', 'review_state' : 'published'})
brains = cat.evalAdvancedQuery(aq, (('Creator', 'asc'), ('effective', 'asc')))
Or how about only those documents the same as above which have had related items noted against them?
from Products.AdvancedQuery import Ge
from Products.CMFCore.utils import getToolByName
cat = getToolByName(context, 'portal_catalog')
aq = cat.makeAdvancedQuery({'portal_type' : 'Document', 'review_state' : 'published'})
aq &= Ge('getRawRelatedItems', None)
brains = cat.evalAdvancedQuery(aq, (('Creator', 'asc'), ('effective', 'asc')))
As you can see, AdvancedQuery makes specifying exactly what you want from the catalog very easy, and the transition to using it is very straightforward, as it already accepts the same sort of query parameters and format that you are already familiar with. When you are ready, you can mix in more advanced criteria without disturbing your existing way of working with the Catalog Tool.
It's strongly recommended to read the AdvancedQuery documentation linked to above and playing with some of the more advanced options it details.
6. Forms
Creating forms for Plone
6.1. Using zope.formlib
zope.formlib is a zope 3 library to handle forms creation, validation, display and actions. It provides a series of base classes and methods to allow forms to be defined with just a little bit of meta data.
6.1.1. Why learn how to use a new framework?
You may be wondering why should you learn how to use a new forms framework if you already know how to use the CMF Form Controller Tool (Form Controller).
Why should you use a forms framework at all? You could always write your own HTML form snippets and use the request dictionary to retrieve and handle data.
The reason is simple: you'll end up writing a lot of boilerplate code to collect, validate and build the response. It would be better if you could just define the fields and metadata of the form and re-use a set of base classes to do the repetitive work behind the scenes, i.e., a forms framework.
One of these frameworks is the Form Controller Tool, which is not bad, but has some disadvantages over formlib:
- First, the Form Controller spreads the form logic across several files so it can be hard to follow it.
- Second, the From Controller doesn't handle the creation and display of the widgets, so you have to create them manually, what could become especially unmantainable when using choice-type fields.
- Last, the Form Controller doesn't work with Zope 3 schema interfaces nor views. Using a Zope 3 schema can help you creating add and edit forms.
However, the Form Controller can be useful and even preferable when you need to implement a complex page flow, or if you want to customize Plone forms that use it; e.g. the ''Send this page to someone'' form.
Beginning with Zope 2.9.3 (Plone 2.5) zope.formlib is being distributed with Zope 2. Five >= 1.4 is required to make use of this Zope 3 package.
Note: Where do I place the code?
You can place the code wherever you want: all in the same file, each class in a file, in several directories, utilities in a utilities.py file, etc. Just keep in mind two things:
- If you write several pieces of code (functions, classes) in separate files, remember to import them whenever you use them, as you would do in any other Python program.
- The ZCML statements have to be placed into a file called
configure.zcmlin the root of your package, or in any other file included from it.
Said that, the author reccommends putting all the Python code in a file named browser.py in this tutorial to avoid confusion.
6.1.2. Creating a simple feedback form
This section explains how to create a very basic feedback form.
The code for this example is available to checkout from the collective as the example.formlib package.
For all practical sense formlib based components are really regular Zope view components with some convenient base classes for auto-generating output based on schemas and other configuration info. You will see that in a moment.
First, define an interface class with the schema of the form:
from zope.interface import Interface
from zope.schema import TextLine, Text
class IFeedbackForm(Interface):
"""
A typical feedback schema
"""
customer = TextLine(title=u'Customer',
description=u'Customer email',
required=True)
subject = TextLine(title=u'Subject',
required=True)
message = Text(title=u'Message',
description=u'The message body',
required=True)
The purpose of this interface is to define the fields of the form. The type of each schema field determines the type of widget that will be used by default for that field, so choose it carefully. To see all the schema fields available, read the zope.schema package's interfaces.
Next, create a form instance, which is a class that groups an ordered collection of fields and actions. To do that, simply subclass Five's PageForm class, a wrapper to the formlib Form class to keep Zope 2 happy. Type the following code into a Python file inside your product:
from Products.Five.formlib.formbase import PageForm
You will also need to make use of Five's strange hybrid between Zope 2 and Zope 3 page templates:
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
The simplest way to define a collection of form fields is using the Fields constructor with the previous schema:
from zope.formlib import form
class FeedbackForm(PageForm):
"""
A typical feedback form
"""
form_fields = form.Fields(IFeedbackForm)
By inheriting from the PageForm class, the FeedbackForm class inherit functionality from
formlib itself. By default, PageForm knows how to generate all the HTML
that will make up of a finished form. But in order to do this, formlib
needs to know what fields are wanted. This is done by providing the
form_fields attribute. The Fields constructor is a formlib helper class that
generates the appropriate field items from any Zope 3 schema (in this case,
the schema interface defined above).
In order to provide a complete form, you need to specify the action to perform when the "submit" button of the form (or any other indicated) is activated. To define the action, use the form.action decorator with a handler function for the submitted data. More on actions later.
# use a dummy MailHost tool here to keep it simple
class MHost:
def __init__(self):
pass
def Send(self, sender, to, subject, body):
pass
class FeedbackForm(PageForm):
"""
A typical feedback form
"""
form_fields = form.Fields(IFeedbackForm)
result_template = ViewPageTemplateFile('feedback_result.pt')
@form.action("send")
def action_send(self, action, data):
mhost = MHost()
self.mFrom = data['customer']
self.mTo = "feedback@mycompany.com"
self.mSubject = data['subject']
self.mBody = data['message']
mhost.Send(self.mFrom, self.mTo, self.mSubject, self.mBody)
return self.result_template()
This is where the real work takes place. In this example, the feedback_result.pt page template is rendered and returned. All the view's attributes will be available inside this template, which will be introduced later.
An example result form is:
<html metal:use-macro="context/@@standard_macros/view">
<head>
</head>
<body>
<div metal:fill-slot="body">
<h1 tal:content="view/label">Form label</h1>
<p>Thank you for your request about
<span tal:replace="view/mSubject">subject</span>,
<span tal:replace="view/mFrom">customer@mail</span>.</p>
<p>We will reply to it shortly.</p>
</div>
</body>
</html>
zope.formlib already includes a default general page form template, with the fields labels, the widgets structures and the submit buttons, so you only have to register your form page with the appropiate ZCML snippet in order to make it accesible from a browser. Assuming you've placed your code into a file named browser.py:
<browser:page
name="feedback"
for="Products.CMFPlone.Portal.PloneSite"
class=".browser.FeedbackForm"
permission="zope.Public"
/>
Let's explain what this ZCML snippet means:
- The
forattribute indicates the class or interface this view will be available for; in this case, it will be shown only from the root of a Plone site. To see the interfaces provided by a certain object, fire up the ZMI, navigate up to your object and check the Interfaces tab. - The
nameattribute sets the name of the view, so the form will be available from a URL with the formhttp://<plone-site>/feedback. - The
classattribute indicates the view class responsible for displaying the page form, in this case, the FeedbackForm class inside thebrowser.pyfile. - The
permissionattribute specify the permission needed to access the page.
Among the most used permissions you can find:
zope.Public- no restrictions, available to everyone.zope.View- permission to view this component.zope.ManageContent- add, edit and delete content objects.
Note: Keen readers will notice the special name for configuring the new view component, browser:page. This XML tag actually employs an XML namespace prefix which needs to be defined. Normally this is added right onto the configure tag like this:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:five="http://namespaces.zope.org/five">
And that's all! Here's how the form and result pages will look like:
Contact form:

Result page:

6.1.3. Adding validation
Server-side form validation is vital to ensure data sanity and protect our site from malicious users.
Field validation
Once you've understood the "hello form", let's move onto a more advanced topic: validation.
The easiest way to manage validation in a formlib-based form is to specify the validation rules in our schema. Actually, you've already implemented some validation: the customer, subject and message fields are required. If you leave the subject field empty, for example, and click the send button, a pretty red error message will show up asking you to fill that field.
Let's add email validation to the customer field using the constraint keyword argument fot that attribute in our schema. For simplicity, the mail address checker that comes with the CMFDefault utilities toolbox will be used in this example, althought you could also use your own regular expression checking. The constraint argument must be a callable that returns True if the value submitted is valid, or raise an exception inheriting from zope.schema.ValidationError, whose docstring will be used in the error message.
from zope.schema import ValidationError
class InvalidEmailAddress(ValidationError):
"Invalid email address"
from Products.CMFDefault.utils import checkEmailAddress
from Products.CMFDefault.exceptions import EmailAddressInvalid
def validateaddress(value):
try:
checkEmailAddress(value)
except EmailAddressInvalid:
raise InvalidEmailAddress(value)
return True
class IFeedbackForm(Interface):
"""
A typical feedback schema
"""
customer = TextLine(title=u'Customer',
description=u'Customer email',
required=True,
constraint=validateaddress)
subject = TextLine(title=u'Subject',
required=True)
message = Text(title=u'Message',
description=u'The message body',
required=True)
Now, if you type an invalid address into the customer field and click send, a kind and colorful error message will be displayed:

That was too easy, wasn't it?
Invariants validation
zope.formlib also supports the validation of schema invariants, e.g. the min value entered must be smaller than the max value. In this example the form will be extended to provide a set of predefined subjects and a field named other which must be filled when selecting the the Other option in the subject select dropdown. It's easier to explain it in Python than in English:
from zope.schema import Choice
from zope.interface import invariant, Invalid
class IFeedbackForm(Interface):
"""
A typical feedback schema
"""
customer = TextLine(title=u'Customer',
description=u'Customer email',
required=True,
constraint=validateaddress)
subject = Choice(title=u'Subject',
vocabulary='Available Subjects',
required=True,
)
other = TextLine(title=u'Other',
description=u"""
If you've specified Other above,
please fill this this field too.""",
required=False)
message = Text(title=u'Message',
description=u'The message body',
required=True)
@invariant
def otherFilledIfSelected(feedback):
if feedback.subject == u'Other' and not feedback.other:
raise Invalid("Please specify the motivation of your request")
Here, the subject field type has been set to Choice, and the list of available values has been indicated to be obtained from the Available Subjects vocabulary, a named utility which will be defined shortly.
The form will call all the invariant-decorated functions of the schema upon validation and catch any raised Invalid exceptions.
You still need to define the Available Subjects vocabulary:
from zope.schema.vocabulary import SimpleVocabulary
def availableSubjects(context):
subjects = ('Comment',
'Feature Request',
'Technical Issue',
'Complaint',
'Other',
)
return SimpleVocabulary.fromValues(subjects)
and register it as a named utility using ZCML in the configure.zcml file:
<configure ... >
...
<utility
component=".browser.availableSubjects"
name="Available Subjects"
provides="zope.schema.interfaces.IVocabularyFactory"
/>
</configure>
Restart your Zope instance for the changes to take effect and test your new form. You'll see something similar to this:

Unfortunately, invariant errors descriptions are not shown in the default template.
6.1.4. Customizing the template and the widgets
Hack into the appearance of your form.
Customizing the template
plone.app.form provides a handy default template named pageform.pt which integrates well with the default Plone skin, but you might need to customize it or write your own one.
To do that, override the template attribute of the form class definition:
class FeedbackForm(PageForm):
"""
A typical feedback form
"""
label = u'Contact Us'
form_fields = form.Fields(IFeedbackForm)
template = ViewPageTemplateFile('feedback_form.pt')
result_template = ViewPageTemplateFile('feedback_result.pt')
@form.action("send")
def action_send(self, action, data):
mhost = MHost()
self.mFrom = data['customer']
self.mTo = "feedback@mycompany.com"
self.mSubject = data['subject']
self.mBody = data['message']
mhost.Send(self.mFrom, self.mTo, self.mSubject, self.mBody)
return self.result_template()
As already stated, all the view attributes will be available inside the page template, including:
- label - A label to display at the top of the form.
- prefix - A string added to all widget and action names.
- form_fields - The list of form's fields.
- widgets
- A list of views for the former fields. The widgets are looked up as
multiadapters for each schema field and the request providing
IDisplayWidgetorIInputWidget. - errors - A list of errors encountered during validation.
- error_views
- A list of views for the former errors. These views are looked up as
multiadapters for each error and the request providing
zope.app.form.browser.interfaces.IWidgetInputErrorView. - status - An update status message, normally generated by success or failure handlers.
- availableActions - The list of form's available actions.
- template - The template used to display the form.
It's reccommended to start with the default pageform.pt and customize it cutting, pasting, deleting and entering text and tags.
Using named templates
Another really zope3-ish method to choose the form template is using the zope.formlib named templates. Using named templates can be (and actually is) an overkill if you've designed your template to work with your form class as a single component. But if you write a form class and the template is just a visual customization of that form, you might want to be able to customize the template without having to reimplement the whole class, or let others do so. This is exactly how Plone overrides the default zope.formlib template with a more plone-ish one in the plone.app.form package.
Please note that this approach was not taken in the example product example.formlib.
Named templates are adapters for the form's view class to INamedTemplate, bound to the form class only by their names. This way, a third party product (e.g. a theme) can register a different template with the same name (usually in a different browser skin layer) to override the default one. Moreover, they're very easy to use. Modify and add the emphasized lines:
from zope.formlib.namedtemplate import NamedTemplate
# Five's ViewPageTemplateFile doesn't work correctly with formlib's NamedTemplateImplementation,
# so we use here the Plone implementation
from plone.app.form import named_template_adapter
class FeedbackForm(PageForm):
"""
A typical feedback form
"""
label = u'Contact Us'
form_fields = form.Fields(IFeedbackForm)
template = NamedTemplate('feedback.form')
result_template = ViewPageTemplateFile('feedback_result.pt')
# rest of the form class implementation...
feedback_template = named_template_adapter(
ViewPageTemplateFile('feedback_form.pt'))
In configure.zcml, add the following snippet to register the named template as an adapter for your form:
<adapter
factory=".browser.feedback_template"
for=".browser.FeedbackForm"
name="feedback.form"
/>
Name your page template feedback_form.pt and you're done.
Customizing the widgets
As we've already stated earlier, form widgets are views for schema fields, i.e. multiadapters for each schema field and the request providing IDisplayWidget or IInputWidget, depending on if they display field data or offer editing funcionality to the user.
To do so, override the custom_widget attribute of a field (which defaults to None). Remember how we set up the form's fields:
class FeedbackForm(PageForm):
"""
A typical feedback form
"""
label = u'Contact Us'
form_fields = form.Fields(IFeedbackForm)
# rest of the form class...
The form_fields fields are accessible throught a dict-like interface, with the schema field names as keys, so we write:
from zope.app.form.browser import RadioWidget as _RadioWidget
def RadioWidget(field, request):
vocabulary = field.vocabulary
widget = _RadioWidget(field, vocabulary, request)
return widget
class FeedbackForm(PageForm):
"""
A typical feedback form
"""
label = u'Contact Us'
form_fields = form.Fields(IFeedbackForm)
form_fields['subject'].custom_widget = RadioWidget
# rest of the form class...
Here, we're specifying a custom widget for the subject field: RadioWidget, which displays a radio box for every item from the field's vocabulary. The zope.app.form.browser and plone.app.form.widgets packages provide a reasonable set of widgets to use and customize, including dropdowns and Kupu/WYSIWYG. Unfortunately, creating new widgets is out of the scope of this tutorial for now.
The RadioWidget function deserves a little explanation. Believe it or not, zope.formlib doesn't handle custom widgets with vocabularies (called items widgets) properly, because it calls form_field.custom_widget(field, request) either the field has an associated vocabulary or not, and item widgets have to be initialized with a vocabulary argument too; so a wrapper function is needed to workaround this issue.
Here's how the improved form looks like:

6.2. Using CMFFormController
How to create and validate forms in Plone using its CMFFormController. Be sure to also read the CMFFormController tutorial in the Products/CMFFormController/documentation directory, included with your copy of Plone. This how-to is also available in Products/CMFFormController/www/ as the file docs.stx, included with Plone.
The CMFFormController package helps developers by simplifying the process of validating forms. It also makes it easier for site administrators to override some of the behavior of packages without modifying code, making it easier to upgrade packages without disturbing the modifications.
How it works:
- Developers associate a set of default variables for their Page Templates. These variables control the validation that takes place after the form is submitted and the actions that occur after validation. The variables are stored on the filesystem in the .metadata properties file.
- Site administrators can override the default validations and actions using the ZMI. Once a set of validations or actions has been specified in the ZMI, the default validations and actions will be ignored.
Forms
To take advantage of CMFFormController, you need to use Controller Page Templates rather than ordinary Page Templates. Controller Page Templates act just like ordinary Page Templates, but they do some extra work when they are viewed.
Here is a basic form that uses CMFFormController:
<form tal:define="errors options/state/getErrors"
tal:attributes="action string:${here/absolute_url}/${template/id};"
method="post">
<input type="hidden" name="form.submitted" value="1" />
<p tal:define="err errors/foo|nothing" tal:condition="err" tal:content="err" />
<input type="text"
name="foo"
tal:define="val request/foo|nothing"
tal:attributes="value val" />
<input type="submit" name="submit" value="submit" />
</form>
Let's take a look.
- First, we note that the form is set up to submit to itself. Forms must submit to themselves.
- Second, we see the special hidden variable
form.submitted. The controlled page template checks the REQUEST for form.submitted to see if the form has been submitted or if, instead, it has just been accessed, e.g. via a link. Forms must contain the hidden variableform.submitted - At the beginning of the form we set the variable errors. The
errors dictionary comes from the state object which is passed
in the template options. The state object lets validators and
scripts pass information to each other and to forms. For our
purposes, the most important information is the errors
dictionary, which has entries of the form
{field_name:error_message}.
Before we can use this form we need to specify the validators that will be used to check the form values, and we need to specify the action that will occur after validation.
Specifying Validators
There are two basic ways to specify a form's validators.
- You can specify the validators in the .metadata properties for filesystem-based Controller Page Templates.
- You can specify the validators via the ZMI (or
programmatically). These values will be stored in the
ZODB as attributes of the
portal_form_controllerobject.
If you specify validators in both places, the validators specified in the ZMI will take precedence over those specified in the .metadata file.
Specifying Validators on the Filesystem
You can specify validators on the filesystem using an objects .metadata properties file.
To create a .metadata file, simply create a file with the
same name as your page template, and then append .metadata to
the end of the name of the file. For instance, you might have
a Controller Page Template called
document_edit_form.cpt. The properties for that file would
be stored in a file called document_edit_form.cpt.metadata
The .metadata file uses the standard python ConfigParser syntax. The validator section of the .metadata file would look like:
[validators]
validators = validate_script1, validate_script2
The validation scripts validate_script1 and
validate_script2 will be called in order.
Type-Specific Validators
Suppose you want different validators to be called, depending on the type of context the form has.
You can do so as follows:
[validators]
validators = validate_script1
validators.Document = validate_script2
In the above example, if the context is a Document object,
validate_script2 will be called for validation; for
everything else, only validate_script1 will be called.
Note that the order in which the variables are specified does not matter; the type-specific validators override non-specific validators if both are applicable.
Button-Specific Validators
Suppose instead that you have two different buttons on your form, and you want different validation sequences to occur depending on which button is pressed. You can accomplish this as follows:
First, name your buttons button1 and button2:
<input type="submit"
name="form.button.button1"
value="First Button" />
<input type="submit"
name="form.button.button2"
value="Second Button" />
Next, specify validators in the .metadata file for button1 and for button2:
[validators]
validators..button1 = validate_script1, validate_script3
validators..button2 = validate_script2, validate_script4
Note the presence of the ... This is a placeholder for a
type specifier. You could further specify that
validate_script5 is called if button2 is pressed and the
context is a Document by adding:
[validators]
validators.Document.button2 = validate_script5
Remember that button specific validators take precedence over non-specific validators.
Specifying Validators in the ZMI
If you look at a Controller Page Template in the ZMI, you will see that it looks just like an ordinary Page Template with two extra tabs, Validation and Actions. Click on the Validation tab.
The Validation tab shows all the validators for the page template in question. You can specify validators with the same kind of specialization options as above via a web form.
The validator information for all forms is stored in the
portal_form_controller tool in your portal. This means
that you can specify validators for filesystem objects
with no problems, since the information is persisted in
the ZODB. Note that the validator information is bound to
the form's Id, so all forms with the same Id use the same
validators. This keeps things simple when you have
multiple skins:
Forms with the same Id use the same validators, no matter what skin they are in.
When a form is submitted, it first checks to see if there are any applicable validators that have been specified via the ZMI. If it finds one, it uses it. If it does not find a validator via the ZMI, it then checks the REQUEST object to see if validators have been specified in hidden variables. As a result, validators specified in the ZMI take precedence over those specified in forms.
Specifying Validators Programmatically
The portal's portal_form_controller tool has methods you
can use to specify the validators for a given
ControllerPageTemplate. The API is as follows:
portal_form_controller.addFormValidators(id,
context_type,
button,
validators)
Here id is the Id of the ControllerPageTemplate,
context_type is the class name for the class of the
context object, button is the name of the button
pressed, and validators is a comma-delimited string or a
list of strings. If you want a validator to act for any
class, set context_type to None.
Similarly, you want a validator to act for any button, set
button to None.
Specifying Actions
The sequence of validators that is executed returns a status
in the state object.
The default status is success, i.e. if no validators are
executed, the status will be success. If a validator
encounters an error, it will typically set the status to
failure. The next thing we need to do in your form is to
specify what happens when a given status is returned.
As with validators, there are two basic ways to specify a form's actions.
- You can specify the actions in the .metadata properties for filesystem-based Controller Page Templates and Controller Python Scripts.
- You can specify the actions via the ZMI (or
programmatically). These values will be stored in the
ZODB as attributes of the
portal_form_controllerobject.
If you specify actions in both places, the actions specified in the ZMI will take precedence over those specified in the form.
Specifying Actions on the Filesystem
You can specify actions on the filesystem using an objects .metadata properties file.
Actions are stored in the same .metadata file as the validators. The syntax for the actions section of your file would look like:
[actions]
action.success = traverse_to:string:script1
In the above example, when the form is submitted and the
validation scripts return a status of success, the
traverse_to action is called with the argument
string:script1, i.e. if the form data is valid, we run the
script script1. Alternatively, we could specify
action.success = redirect_to:string:http://my_url_here,
which would cause the browser to be redirected to
http://my_url_here.
The default action for the failure status is to reload the
current form. The form will have access to all the error
messages, via the state object in its options.
Type-Specific Actions
Suppose you want different actions to occur depending on the type of context the form has.
You can do so as follows:
[actions]
action.success = traverse_to:string:script1
action.success.Document = traverse_to:string:document_script
In the above example, if the context is a Document object, document_script will be executed upon successful validation; for everything else, script1 will be executed. Note that the order in which the variables are specified does not matter; the type-specific actions will override non-specific actions if both are applicable.
Button-Specific Actions
Suppose instead that you have two different buttons on your form, and you want different actions to occur depending on which button is pressed. You can accomplish this as follows:
First, name your buttons button1 and button2:
<input type="submit"
name="form.button.button1"
value="First Button" />
<input type="submit"
name="form.button.button2"
value="Second Button" />
Next, specify actionss for button1 and for button2:
[actions]
action.success..button1 = traverse_to:string:script1
action.success..button2 = traverse_to:string:script2
Note the presence of the ... This is a placeholder for
a type specifier. You could further specify that
document_script2 is called if button2 is pressed and the
context is a Document by adding:
[actions]
action.success.Documnet.button2 = traverse_to:string:document_script2
Specifying Actions in the ZMI
If you look at a Controller Page Template in the ZMI, you will see that it looks just like an ordinary Page Template with two extra tabs, Validation and Actions. Click on the Actions tab.
The Actions tab shows all the actions for the page template in question. You can specify actions with the same kind of specialization options as above via a web form.
The action information for all forms is stored in the
portal_form_controller tool in your portal. This means
that you can specify actions for filesystem objects with
no problems, since the information is persisted in the
ZODB. Note that the action information is bound to the
form's Id, so all forms with the same Id use the same
actions. This keeps things simple when you have multiple
skins: forms with the same Id use the same actions, no
matter what skin they are in.
When a form is submitted, it first checks to see if there are any applicable actions that have been specified via the ZMI. If it finds one, it uses it. If it does not find an action via the ZMI, it then checks the REQUEST object to see if actions have been specified in hidden variables. As a result, actions specified in the ZMI take precedence over those specified in forms.
Specifying Actions Programmatically
The portal's portal_form_controller tool has methods you
can use to specify the actions for a given
ControllerPageTemplate. The API is as follows:
portal_form_controller.addFormAction(id,
status,
context_type,
button,
action_type,
args)
Here id is the Id of the ControllerPageTemplate,
status is the status for which the action will be
executed, context_type is the class name for the class
of the context object, button is the name of the button
pressed, action_type is the type of action that will
occur, and args is a string (typically a TALES
expression) that will be passed to the action. If you want
an action to be executed for any class, set context_type
to None. Similarly, you want an action to be executed for
any button, set button to None.
Validation Scripts
When writing validation scripts, use Controller Validators instead of Python Scripts. Controller Validators are just like ordinary Scripts with the addition of a ZMI Actions tab. On the file system, Controller Validators use the extension .vpy rather than .py.
Let's take a look at a basic validation script that tests the
REQUEST value n to see if it is an integer:
n = context.REQUEST.get('n', None)
if not n:
state.setError('n', 'Please enter a value', new_status='failure')
else:
try:
int(n)
except ValueError:
state.setError('n', 'Please enter an integer',
new_status='failure')
if state.getErrors():
state.set(portal_status_message='Please correct the errors shown.')
return state
The first thing to note is that Controller Validators have a
built-in state object called state. This state object (of
class ControllerState) contains basic information about what has
happened during the validation chain.
The state object has a status attribute which contains the
current validation status. The initial status is success. If
errors are detected by validators, they set the status to
something else, typically failure.
The state object also stores errors that have been detected. The
setError method is used to set an error message for a
particular variable. The setError method has the optional
new_status argument that can be used to both set an error
message as well as to update the status. You can see if an error
message has already been stored for a particular variable by
calling state.getError(variable_name).
The set method lets you set multiple attributes of the state object all at once, e.g.:
state.set(status='my_new_status')
You can also pass keyword arguments to the state object via the
set method. These arguments will get passed along by the
action. The traverse_to action places these keyword arguments
in the REQUEST. The redirect_to action adds them to the query
string of the URL to which it is redirecting.
Finally, we return the state object.
Another interesting example is email validation:
from Products.CMFDefault.utils import checkEmailAddress
from Products.CMFDefault.exceptions import EmailAddressInvalid
email = context.REQUEST.get('email', None)
if not email:
state.setError('email', 'No e-mail address')
else:
# Do try-catch here because checkEmailAddress will throw an exception
# instead of saying "no, not valid".
try:
checkEmailAddress(email)
email_ok = True
except EmailAddressInvalid:
email_ok = False
if not email_ok:
state.setError('email', 'Invalid e-mail address.')
Scripts
When writing scripts that do some processing after a validated form, you can use Controller Python Scripts instead of ordinary Python Scripts to let site managers override their actions via the ZMI. On the file system, Controller Python Scripts use the extension .cpy rather than .py. Note that Controller Validators and Controller Python Scripts differ in signficant ways. Be sure to use the appropriate script type (Controller Validator or Controller Python Script) and/or the appropriate file extension (.cpy or .vpy).
Let's take a look at a basic script that sets a context attribute
to the value n that is passed in via the 'REQUEST':
context.n = context.REQUEST.get('n')
# Optionally set the default next action (this can be overridden
# in the ZMI)
state.setNextAction('redirect_to:string:view')
# Optionally pass a message to display to the user
state.setKwargs({'portal_status_message':'You set context.n to %s.' % str(context.n)})
return state
Note that you will usually want to use the traverse_to action to
call your script. This will ensure that form variables set in the
REQUEST object are available to your script.
This script sets its action to redirect to the relative url view
for the current context object. The status has not been set, so it
is the default status, success.
The state.setNextAction directive above is analogous to having
the following line in your .metadata file:
[actions]
action.success = redirect_to:string:view
As with the .metadata file, the default action specified in the script can be overridden via the ZMI. This allows site managers to override post-script actions without having to customize your code.
Finally, we return the state object.
Validation for Scripts
Having separate validation scripts typically means that validation is moved out of scripts. This simplifies scripts, but means that it is possible to call them directly with invalid data. We can prevent this problem by adding validators to scripts. Controller Python Scripts use the same ZMI and/or .metadata file mechanisms for adding validators as do Controller Page Templates.
Each time a validator is called, it logs the call in the state object. Validation is smart enough that if a validator is called by a form, it will not be called again by the script.
Note that if you associate validators with a script, you will
need to set a sensible failure status action, since scripts do
not set such an action by default. You may wish to define a
different failure status for failures that occur within your
script, e.g. script_failure. Then you can specify a behavior
for failures that occur as a result of invalid parameters coming
in and for failures that occur within the script.
7. Testing
This tutorial will explain how to write safer, better code that makes you look more professional. That's right - it's time to write tests, for everything you do. Don't worry, it's not boring or complicated, you just need to learn how.
7.1. Introduction
What is this thing called testing anyway?
"I know I should write tests, but ...
- ... they take time to write
- ... Iâm a good developer
- ... my customer / the community does the testing"
Sound familiar? No matter how good you think you are, you will make mistakes. Your code will contain bugs and someone will come after you demanding an explanation. Without some methodical way of testing, you are guaranteeing your code with nothing more than guesswork and arrogance. Clicking around the Plone interface for a few minutes before you ship your code off to the customer or user is simply not enough.
Testing is an art, it needs to be built into your development cycle from the very beginning - it is not something you do only after all the other work is finished, it is something you do continuously. Unfortunately, testing often evokes emotions of dread in developers. It's slow, it's boring, it's not what they signed up to do. But the art of testing has evolved beyond that - there is considerable elegance and fun to be found in well-conceived test strategies.
This tutorial aims to give you the tools you need to write tests and testable software in Plone. If you are writing software for Plone core itself, don't even think about commiting any bug fix or feature without test coverage. If you are writing an add-on product or doing a customisation, holding yourself to the same high standards that the Plone core team do will give you better confidence in your software and will likely save you considerable pain down the road.
Examples
This tutorial contains several examples of the various types of tests. They are available in the example.tests package, which you can install as a develop egg in a Plone 3 buildout. The examples of running tests use the standard commands for buildouts, since this is the only way that works reliably on Windows (that is, plain zopectl test will not work on Windows).
Take a look at the buildout tutorial for more information.
7.2. A brief example
Just so that you know what we're talking about
Try to find the bug in the following piece of code:
class Employee(object):
def __init__(self, name, position, employee_no=None):
self.name = name
self.position = position
self.employee_no = employee_no
salaries = {0: 12000,
1: 4000,
2: 8000,
3: 4000}
def print_salary(employee):
if employee.employee_no:
salary = salaries.get(employee.employee_no, 0)
print "You make EUR %s." % salary
else:
print "You're not an employee currently."
Found it yet? Did you have to spend more than a few seconds thinking about it? Any developer could have written that code and not seen the problem. Furthermore, the bug is an edge case that you may not have tested using manual/through-the-web testing.
Let us write a test (actually, a doc/unit test) for this code. Don't worry too much about how this is set up and executed just yet.
Employee w/o an employee number is ignored:
>>> print_salary(Employee('Adam', 'Developer'))
You're not an employee currently
Employee w/o a known employee number earns nothing:
>>> print_salary(Employee('Berta', 'Designer', 100))
You make EUR 0.
Employee w/ a valid employee number is found properly:
>>> print_salary(Employee('Chris', 'CTO', 2))
You make EUR 8000.
Zero is a valid employee number:
>>> print_salary(Employee('Devon', 'CEO', 0))
You make EUR 12000
As it happens, the last test would fail. It would print You are not an employee currently., unless we fixed the code:
class Employee(object):
def __init__(self, name, position, employee_no=None):
self.name = name
self.position = position
self.employee_no = employee_no
salaries = {0: 12000,
1: 4000,
2: 8000,
3: 4000}
def print_salary(employee):
if employee.employee_no is not None:
salary = salaries.get(employee.employee_no, 0)
print "You make EUR %s." % salary
else:
print "You're not an employee currently."
The moral of the story?
- you rarely catch problems like these with manual testing
- put the time you waste catching silly bugs and typos into writing tests
- with decent test coverage, you end up saving lots of time when you refactor
7.3. Types of tests
Some terminology you should be familiar with
Broadly speaking, there are four main types of tests:
- Unit tests
- These are written from the programmer's perspective. A unit test should test a single method or function in isolation, to ensure that it behaves correctly. For example, testing that a given calculation is performed correctly given a variety of input is a good unit test for that one method.
- Integration tests
- Whereas unit tests try to remove or abstract away as many dependencies as possible to ensure that they are truly only concerned with the method under test, integration tests exercise the integration points between a method or component and the other components it relies on. For example, testing that a method performs some calculation and then correctly stores the result in the ZODB is an integration test in that it tests the integration between that component and the ZODB.
- Functional tests
- A functional test is typically demonstrating a use case, exercising a "vertical" of functionality. For example, testing that filling in a form and clicking "Save" then makes the resulting object available for future use, is a functional test for the use case of using that form to create content objects.
- System tests
- These are written from the user's perspective, and treat the system as a black box. A system test may be simulating a user interacting with the system according to expected usage patterns. By their nature, they are typically less systematic than the other types of tests.
Furthermore, functional tests may be white box, in which case they can make assertions about things like the underlying data storage (but only if this is specified clearly; implementation details should never affect functional tests). Such tests are also called functional integration tests (you can see where the lines start to blur, but don't worry too much about the naming). Alternatively, functional tests can be black box in which case they only perceive the system from the point of view of an actor (usually the end user) and make assertions only on what is presented in the (user) interface to that actor. Such tests, also known as acceptance tests would not make assumptions about the underlying architecture at all.
Tests and documentation
In a post to the Zope 3 mailing list, Jim Fulton explains the importance of tests and documentation, and how they go hand-in-hand:
One of the important things about this is that most doctests
should be written as documentation. When you write new software
components and you need to write tests for the main functionality
of your software you need to:
- Get your head into the mode of writing documentation.
This is very very very important.
- You need to document how to use the software. Include examples,
which are tests
We will learn more about doctests, and how they are used for unit testing and functional testing later. The important thing to note is that good tests often serve as documentation describing how your component is supposed to be used. Thinking about the story they tell is just as important as thinking about the number of input and output states they cover.
7.4. Telling stories with doctests
Doctests bring code and test closer together, and makes it easier to describe what a test does, and why.
By their nature, tests should exercise an API and demonstrate how it is used. Thus, for other developers trying to understand how a module or library should be used, tests can be the best form of documentation. Python supports the notion of doctests, otherwise known as executable documentation.
Doctests look like Python interpreter sessions. They contain plain text (normally in reStructedText, which can be rendered to HTML or PDF easily) as well as examples. The idea is to show something that could have been typed in an interpreter session and what the expected outcome should be. In the Zope 3 world, doctests are extremely prevalent and are used for most unit and integration testing.
Doctests come in two main flavours: You can write a simple text file, such as a README.txt, that explains your code along with verifiable examples, or you can add doctests for a given method or class into the docstring of that method or class.
The full-file approach - sometimes known as documentation-driven development - is the most common. This type of test is very well suited for explaining how an API should be used and ensuring that it works as expected at the same time. However, note that these are not technically proper unit tests, because there is no guarantee of isolation between the steps of the "script" that the doctest describes. The docstring version uses the same basic syntax, but each docstring is executed as its own test fixture, guaranteeing full isolation between tests.
Here is a trivial example of a doctest. We will learn how to set up such a test shortly.
Interfaces are defined using Python class statements::
>>> import zope.interface
>>> class IFoo(zope.interface.Interface):
... """Foo blah blah"""
...
... x = zope.interface.Attribute("""X blah blah""")
...
... def bar(q, r=None):
... """bar blah blah"""
In the example above, we've created an interface::
>>> type(IFoo)
<class 'zope.interface.interface.InterfaceClass'>
We can ask for the interface's documentation::
>>> IFoo.__doc__
'Foo blah blah'
We could create an arbitrary object - this will of course not provide
the interface.
>>> o = object()
>>> o # doctest: +ELLIPSIS
<object at ....>
>>> IFoo.providedBy(o)
False
>>> o.bar() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
AttributeError: 'object' object has no attribute 'bar'
Each time the doctest runner encounters a line starting with >>>, the prompt of the Python interpreter (i.e. what you get by running python without any arguments in a terminal), it will execute that line of code. If that statement is then immediately followed by a line with the same level of indentation as the >>> that is not a blank line and does not start with >>>, this is taken to be the expected output of the statement. The test runner will compare the output it got by executing the Python statement with the output specified in the doctest, and flag up an error if they don't match.
Note that not writing an output value is equivalent to stating that the method has no output. Thus, this is a failure:
>>> foo = 'hello'
>>> foo
>>> # do something else
The reference to foo on its own will print the value of foo. The correct DocTest would read:
>>> foo = 'hello'
>>> foo
'hello'
>>> # do something else
Notice also the ... (ellipsis) element in the expected otuput. These mean "any number of characters" (anologus to a .* statement in a regular expression, if you are familiar with those). They are usually convenient shorthand, but they can sometimes be necessary. For example:
>>> class Foo: ... pass >>> Foo() <__main__.Foo instance at ...>
Here, the ... in the expected output replaces a hexadecimal memory address (0x0x4523a0 on the author's computer at the time of writing), which cannot be predicted in advance. When writing doctests in particular (but also when writing regular unit tests), you need to be careful about values you cannot predict, such as auto-generated ids based on the current time or a random number. The ellipsis operator can help you work around those.
Do not confuse the ellipsis operator in the expected output with the syntax of using ... underneath a >>> line. This is the standard Python interpreter syntax used to designate statments that run over multiple lines, normally as the result of indentation. You can, for example, write:
>>> if a == b: ... foo = bar
if that is necessary in your test.
Doctest tips and tricks
As with all testing, you will get better at doctests over time. Below are a few tips that may help you get started.
- Read the documentation
- doctests have been in Python for a long time. The doctest module comes with more documentation on how they work.
- A test is just a bunch of python statements!
- Never forget this. You can, for example, reference helper methods in your own product, for example, imagine you have a method in Products.MyProduct.tests.utils that has a method setUpSite() to pre-populate your site with a few directories and users. Your doctest could contain:
>>> from Products.MyProduct.tests.utils import setUpSite >>> setUpSite()
- The test suite can perform additional initialisation
- A test suite can have setUp() and/or tearDown() handlers that perform additional set-up or clean-up. We will see further examples of this later.
- PDB is still your friend
- You can put the standard import pdb; pdb.set_trace() on a line in doctest. Unfortunately, you can't step through a doctest line by line, but you can print variables and examine the state of the test fixture.
- You can catch exceptions
- If you need to debug a doctest that is throwing an exception, this statement is often useful:
>>> try: ... someOperation() ... except: ... import pdb; pdb.set_trace() >>> # continue as normal
7.5. Running tests
It is not much good writing a test or relying on someone else's tests if you don't know how to run them.
The easiest way to run tests in Zope is to use zopectl or the equivalent control script.
./bin/zopectl test -s Products.RichDocument
This would run all tests in the Products.RichDocument module. If you are using a buildout with an instance control script called instance, this would be:
./bin/instance test -s Products.RichDocument
Using buildout is probably a good idea - see the buildout tutorial - not at least because this is the only way that works reliably on Windows. We will use this syntax from now on.
To execute a single test or a set of tests matched by regular expression, you can use:
./bin/instance test -s Products.RichDocument -t setup
This would run tests in files like test_setup.py. To run all doctests in README.txt (presuming there was a test suite for this file) you would write:
./bin/instance test -s Products.RichDocument -t README.txt
The new test runner also includes a few debugging options. For example:
./bin/instance test -m Products.RichDocument -D
This will stop execution at the first failing test and drop into a PDB post-mortem.
To see the other options that are available, run:
./bin/instance test --help
When the tests you think are relevant all pass, it's time to run all tests and make sure nothing else broke. (No, we don't care that you are writing your code in a totally different python module than what those other tests are supposed to test, and that they were all fine and good and all you changed was a docstring. Run the tests when you think you're done.)
When tests finish running, you will see a report like:
...
Ran 18 tests in 6.463s
OK
(it may look slightly different, depending on which test runner you are using)
Rehearse a satisfied sigh as you read the line "OK", as opposed to seeing a count of failed tests. With time, this will be the little notifier that lets you go to bed, see your friends again or generally get back to real life with an svn commit.
If you're not so lucky, you may see:
Ran 18 tests in 7.009s
FAILED (failures=1, errors=1)
(again, the output may look slightly different depending on your test runner, but the same information should always be there)
This means that there were 1 python error and 1 failed test during test execution.
A python error means that some of your test code, or some code that was called by a test, raised an exception. This is bad, and you should fix it right away.
A failed test means that your test was trying to assert something that turned out not to be true. This could be OK. It could mean you haven't written the code the test is testing yet (well done, you wrote the test first!), or that you don't yet know why it's failing. Sometimes you may be radically refactoring or rewriting parts of your code, and the tests will keep on failing until you're done. Incidentally, this is part of the reason why unit tests are so good - you can do that kind of stuff.
It's sometimes (not always - don't try this on Plone core unless you've been told it's OK by the release manager) acceptable to go to bed and check in a failing test if you are not in a position to know how to fix it. At least other developers will be aware of the problem and may be able to fix it.
7.6. Writing unit tests
Now that you understand the principle of tests and how to run them, it's time to write some. We will start with simple unit tests using doctest syntax.
We will start by showing how to create a simple unit test with doctest syntax. There is nothing Zope- or Plone-specific about this test. This type of test is ideal for methods and classes that perform some kind of well-defined operation on primitives or simple objects. The doctest syntax is well-suited for explaining the inputs and outputs. Since the tests are relatively few and/or descriptive, keeping the tests, documentation and code close together makes sense.
Tests are usually found in a tests/ sub-package. In the example.tests package, we have created a file called tests/test_simple_doctest.py. This sets up a test suite to run doctests in the doc strings in the module example.tests.context. Let's look at the test setup first:
"""This is the setup for a doctest where the actual test examples are held in
docstrings in a module.
Here, we are not using anything Zope-specific at all. We could of course
use the Zope 3 Component Architecture in the setup if we wanted. For that,
take a look at test_zope3_doctest.py.
However, we *do* use the zope.testing package, which provides improved
version of Python's standard DocTestSuite, DocFileSuite and so on. If you
don't want this dependency, just use doctest.DocTestSuite.
"""
import unittest
import zope.testing
import example.tests.context
def setUp(test):
"""We can use this to set up anything that needs to be available for
each test. It is run before each test, i.e. for each docstring that
contains doctests.
Look at the Python unittest and doctest module documentation to learn
more about how to prepare state and pass it into various tests.
"""
def tearDown(test):
"""This is the companion to setUp - it can be used to clean up the
test environment after each test.
"""
def test_suite():
return unittest.TestSuite((
# Here, we tell the test runner to execute the tests in the given
# module. The setUp and tearDown methods can be used to perform
# test-specific setup and tear-down.
zope.testing.doctest.DocTestSuite(example.tests.context,
setUp=setUp, # setUp and tearDown are optional!
tearDown=tearDown),
))
There are a lot of comments here, and we show how to use setUp() and tearDown() methods for additional initialisation and clean-up, if necessary. The test runner will call the test_suite() method and expect a TestSuite object back. If desired, we could have put multiple test suites referring to multiple modules into the TestSuite that is being returned.
Here is the actual code under test, in context.py:
from zope.interface import implements
from example.tests.interfaces import IContext
class Context(object):
"""An object used for testing. We will register an adapter from this
interface to IUpperCaser in the test setup.
Here's how you use it. First, import the class.
>>> from example.tests.context import Context
Then in-stan-ti-ate it (with me so far?):
>>> my_context = Context()
Okay, here's the tricky bit ... now we need to set the title:
>>> my_context.title = u"Some string!"
Phew ... did that work?
>>> my_context.title
u'Some string!'
Yeah!
"""
implements(IContext)
def __init__(self, title=u""):
self.title = title
Here is how we may run the tests from a buildout:
./bin/instance test -s example.tests -t context Running unit tests: Running: .... Ran 4 tests with 0 failures and 0 errors in 0.071 seconds.
7.7. Testing a Zope 3 component with a separate doctest file
Sometimes, we may need to perform additional set-up for our tests to run properly.
In the previous example, we wrote a doctest in a docstring. As tests become more complex or require more involved configuration, it is usually better to separate the actual test into a text file. Sometimes, this can be the README.txt file of a package. This is the approach favoured by Zope 3 components.
In this example, we will register an adapter that is used in a doctest. This doctest also serves to illustrate how this particular adapter should be used. This style of test is great when the emphasis is on the documentation as well as the test. Note that we do not load the package's ZCML in its entirely. Instead, we register the required components explicitly. This means that we retain control over what is executed in the test. We use the zope.component.testing.tearDown method to ensure that our test environment is properly cleaned up.
In the example.tests package, we have the following test setup in tests/test_zope3_doctest.py:
"""This is the setup for a doctest that tests a Zope 3 component.
There is really nothing too different from a "plain Python" test. We are not
parsing ZCML, for example. However, we use some of the helpers from Zope 3
to ensure that the Component Architecture is properly set up and torn down.
"""
import unittest
import zope.testing
import zope.component
def setUp(test):
"""This method is used to set up the test environment. We pass it to the
DocFileSuite initialiser. We also pass a tear-down, but in this case,
we use the tear-down from zope.component.testing, which takes care of
cleaning up Component Architecture registrations.
"""
# Register the adapter. See zope.component.interfaces for more
from example.tests.context import UpperCaser
zope.component.provideAdapter(UpperCaser)
def test_suite():
return unittest.TestSuite((
# Here, we tell the test runner to execute the tests in the given
# file. The setUp and tearDown methods employed make use of the Zope 3
# Component Architecture, but really there is nothing Zope-specific
# about this. If you want to test "plain-Python" this way, the setup
# is the same.
zope.testing.doctest.DocFileSuite('tests/zope3.txt',
package='example.tests',
setUp=setUp,
tearDown=zope.component.testing.tearDown),
))
Notice how we use a custom setUp() method to register the custom adapter, and then reference zope.component.testing.tearDown for the tear-down method.
This refers to the file zope3.txt, which looks like this:
==========================
A Zope 3 component doctest
==========================
This is the type of test found most commonly in Zope 3. We have a custom
setup method (in test_zope3_doctest.py) which registers the components we
need for the test. We can then use those here. ZCML is not processed directly,
nor do we have a full Zope 2/Plone environment available. This makes the test
more isolated (and faster!). Often, we may choose to use mock implementations
of certain components in order to make the test properly isolated.
Of course, we should still tell a story with this documentation.
Let's say we had one of our really exciting context objects:
>>> from example.tests.context import Context
>>> context = Context()
>>> context.title = u"Some puny title"
Of course, that's nice, but what if we wanted to make a bit more of an impact?
We can use our handy upper-caser adapter!
>>> from example.tests.interfaces import IUpperCaser
>>> shout = IUpperCaser(context)
>>> shout.title
u'SOME PUNY TITLE'
Wow!
To run just this test, we may do:
./bin/instance test -s example.tests -t zope3.txt
Running unit tests:
Running:
..
Ran 2 tests with 0 failures and 0 errors in 0.010 seconds.
7.8. Writing a PloneTestCase unit/integration test
Sometimes, we need access to a full-blown Plone instance in order to effectively write tests
PloneTestCase, which in turn uses ZopeTestCase, is used to set up a full Zope environment, including a Plone instance, for testing. This type of test is very convenient and often necessary because content types, tools and other parts of Plone have hard dependencies on various underlying Zope, CMF and Plone components. It is generally better to write simpler tests, however, both because they provide better isolation (thus testing the component more directly and under better controlled circumstances) and because they execute faster.
PloneTestCase-tests are often referred to as "unit tests", but in truth they are integration tests, since they depend on a "live" Zope instance and thus test the integration between your code and the underlying framework. We can use the PloneTestCase setup to run doctests, as we will see in the next section.
Here, however, we will demonstrate how to use unittest.TestCase classes, where each test is a method on a class (with a name beginning with test) This type of test is not as good for documentation, but can be very useful for systematically executing many variations on the same test. Some developers also find this type of test easier to debug, since it is plain Python code which can be stepped through using the debugger.
In the example.tests package, we have tests/base.py. This does not contain any tests, but performs the necessary configuration to set up the test fixture:
"""Test setup for integration and functional tests.
When we import PloneTestCase and then call setupPloneSite(), all of Plone's
products are loaded, and a Plone site will be created. This happens at module
level, which makes it faster to run each test, but slows down test runner
startup.
"""
from Products.Five import zcml
from Products.Five import fiveconfigure
from Testing import ZopeTestCase as ztc
from Products.PloneTestCase import PloneTestCase as ptc
from Products.PloneTestCase.layer import onsetup
#
# When ZopeTestCase configures Zope, it will *not* auto-load products in
# Products/. Instead, we have to use a statement such as:
#
# ztc.installProduct('SimpleAttachment')
#
# This does *not* apply to products in eggs and Python packages (i.e. not in
# the Products.*) namespace. For that, see below.
#
# All of Plone's products are already set up by PloneTestCase.
#
@onsetup
def setup_product():
"""Set up the package and its dependencies.
The @onsetup decorator causes the execution of this body to be deferred
until the setup of the Plone site testing layer. We could have created our
own layer, but this is the easiest way for Plone integration tests.
"""
# Load the ZCML configuration for the example.tests package.
# This can of course use <include /> to include other packages.
fiveconfigure.debug_mode = True
import example.tests
zcml.load_config('configure.zcml', example.tests)
fiveconfigure.debug_mode = False
# We need to tell the testing framework that these products
# should be available. This can't happen until after we have loaded
# the ZCML. Thus, we do it here. Note the use of installPackage() instead
# of installProduct().
#
# This is *only* necessary for packages outside the Products.* namespace
# which are also declared as Zope 2 products, using
# <five:registerPackage /> in ZCML.
# We may also need to load dependencies, e.g.:
#
# ztc.installPackage('borg.localrole')
#
ztc.installPackage('example.tests')
# The order here is important: We first call the (deferred) function which
# installs the products we need for this product. Then, we let PloneTestCase
# set up this product on installation.
setup_product()
ptc.setupPloneSite(products=['example.tests'])
class ExampleTestCase(ptc.PloneTestCase):
"""We use this base class for all the tests in this package. If necessary,
we can put common utility or setup code in here. This applies to unit
test cases.
"""
class ExampleFunctionalTestCase(ptc.FunctionalTestCase):
"""We use this class for functional integration tests that use doctest
syntax. Again, we can put basic common utility or setup code in here.
"""
Notice how we can explicitly install third party products (and egg-based packages which use product semantics) and then tell PloneTestCase to quick-install these into the test fixture site. The test runner will not automatically load all products in the Products.* namespace, nor will it execute ZCML for packages outside Products.* automatically.
The test class which uses this environment is found in tests/test_integration_unit.py:
"""This is an integration "unit" test. It uses PloneTestCase, but does not
use doctest syntax.
You will find lots of examples of this type of test in CMFPlone/tests, for
example.
"""
import unittest
from example.tests.tests.base import ExampleTestCase
from Products.CMFCore.utils import getToolByName
class TestSetup(ExampleTestCase):
"""The name of the class should be meaningful. This may be a class that
tests the installation of a particular product.
"""
def afterSetUp(self):
"""This method is called before each single test. It can be used to
set up common state. Setup that is specific to a particular test
should be done in that test method.
"""
self.workflow = getToolByName(self.portal, 'portal_workflow')
def beforeTearDown(self):
"""This method is called after each single test. It can be used for
cleanup, if you need it. Note that the test framework will roll back
the Zope transaction at the end of each test, so tests are generally
independent of one another. However, if you are modifying external
resources (say a database) or globals (such as registering a new
adapter in the Component Architecture during a test), you may want to
tear things down here.
"""
def test_portal_title(self):
# This is a simple test. The method needs to start with the name
# 'test'.
# Look at the Python unittest documentation to learn more about hte
# kinds of assertion methods which are available.
# PloneTestCase has some methods and attributes to help with Plone.
# Look at the PloneTestCase documentation, but briefly:
#
# - self.portal is the portal root
# - self.folder is the current user's folder
# - self.logout() "logs out" so that the user is Anonymous
# - self.setRoles(['Manager', 'Member']) adjusts the roles of the current user
self.assertEquals("Plone site", self.portal.getProperty('title'))
def test_able_to_add_document(self):
new_id = self.folder.invokeFactory('Document', 'my-page')
self.assertEquals('my-page', new_id)
# Keep adding methods here, or break it into multiple classes or
# multiple files as appropriate. Having tests in multiple files makes
# it possible to run tests from just one package:
#
# ./bin/instance test -s example.tests -t test_integration_unit
def test_suite():
"""This sets up a test suite that actually runs the tests in the class
above
"""
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestSetup))
return suite
Here, we have a test suite with one test class - we could have added more classes if necessary. The afterSetUp() and beforeTearDown() methods - if present - are called immediately before and after each test. After a test is run, the transaction is rolled back, causing tests to run in isolation. You only really need explicit teardown if your tests make permantent changes that are not covered by the ZODB transaction machinery.
You are free to add whatever helper methods you wish to your unit test class, but any method with a name starting with test will be executed as a test. Tests are usually written to be as concise (not to be confused with "obfuscated") as possible.
Notice the calls to methods like self.assertEqual() or self.failUnless(). These are the assertion methods that do the actual testing. If any of these fail, that test is counted as a failure and you'll get an ugly F in your test output.
To run the test, we would do:
./bin/instance test -s example.tests -t test_integration_unit
Running:
..
Ran 2 tests with 0 failures and 0 errors in 0.178 seconds.
There is actually more output than this, as PloneTestCase installs a number of products and processes ZCML.
Rules of thumb
There are some basic rules of thumb for writing unit tests with PloneTestCase you should be aware of:
- Write test first, don't put it off, and don't be lazy (did we say this enough already?)
- Write one test (i.e. one method) for each thing you want to test
- Keep related tests together (i.e. in the same test case class)
- Be pragmatic. If you want to test every combination of inputs and outputs you will probably go blue in the face, and the additional tests are unlikely to be of much value. Similarly, if a method is complicated, don't just test the basic case. This comes with experience, but in general, you should test common cases, edge cases and preferably cases in which the method or component is expected to fail (i.e. test that it fails as expected - you still shouldn't get any F's in your test output!).
- Keep tests simple. Don't try to be clever, don't over-generalise. When a test fails, you need to easily determine whether it is because the test itself is wrong, or the thing it is testing has a bug.
Assertion and utility methods in the unit testing framework
There are quite a few assertion methods, most of which do basically the same thing - check if something is True or False. Having a variety of names allows you to make your tests read the way you want. The list of assertion methods can be found in the Python documentation for unittest.TestCase. The most common ones are:
- failUnless(expr)
- Ensure expr is true
- assertEqual(expr1, expr2)
- Ensure expr1 is equal to expr2
- assertRaises(exception, callable, ...)
- Make sure exception is raised by the callable. Note that callable here should be the name of a method or callable object, not an actual call, so you write e.g. self.assertRaises(AttributeError, myObject.myMethod, someParameter). Note lack of () after myMethod. If you included it, you'd get the exception raised in your test method, which is probably not what you want. Instead, the statement above will cause the unit testing framework to call myMethod(someParameter) (you can pass along any parameters you want after the calalble) and check for an AttributeError.
- fail()
- Simply fail. This is useful if a test has not yet been completed, or in an if statement inside a test where you know the test has failed.
In addition to the unit testing framework assertion methods, ZopeTestCase and PloneTestCase include some helper methods and variables to help you interact with Zope. It's instructive to read the source code for these two products, but briefly, the key variables you can use in unit tests are:
- self.portal
- The Plone portal the test is executing in
- self.folder
- The member folder of the member you are executing as
And the key methods are:
- self.logout()
- Log out, i.e. become anonymous
- self.login()
- Log in again. Pass a username to log in as a different user.
- self.setRoles(roles)
- Pass in a list of roles you want to have. For example, self.setRoles(('Manager',)) lets you be manager for a while. How nice.
- self.setPermissions(permissions)
- Similarly, grant a number of permissions to the current user in self.folder.
- self.setGroups(groups)
- Set which groups the test user is in.
Tips & Tricks
Good unit testing comes with experience. It's always useful to read the unit tests of code with which you are fairly familiar, to see how other people unit test. We'll cover a few hints here to get you thinking about how you approach your own tests:
- Don't be timid! Python, being a dynamic scripting language, lets you do all kinds of crazy things. You can rip a function right out from the Plone core and replace it with your own implementation in afterSetUp() or a test if that serves your testing purposes.
- Similarly, replacing things like the MailHost with dummy implementations may be the only way to test certain features. Look at CMFPlone/tests/dummy.py for some examples of dummy objects.
- Use tests to try things out. They are a safe environment. If you need to try something a bit out of the ordinary, writing them in a test is often the easiest way of seeing how something works.
- During debugging, you can insert print statements in tests to get traces in your terminal when you execute the tests. Don't check in code with printing tests, though. :)
- Similarly, the python debugger is very valuable inside tests. Putting import pdb; pdb.set_trace() inside your test methods lets you step through testing code and step into the code it calls. If you're not familiar with the python debugger, your life is incomplete. More about using pdb with Plone.
7.9. Integration doctests using PloneTestCase
The PloneTestCase integration test setup can also be used in doctests
The choice of test case classes over doctest is purely one of syntactic preference. We can use the test setup from the previous section (in base.py) in a doctest as well. This type of test is more useful for documenting the integration of your code with Zope/Plone in a narrative fashion.
There is no change to tests/base.py for this type of setup. However, we must be careful to use a test class that derives from FunctionalTestCase, since this performs the initialisation necessary for doctests. The test setup is found in tests/test_integration_doctest.py:
"""This is an integration doctest test. It uses PloneTestCase and doctest
syntax.
"""
import unittest
import doctest
from zope.testing import doctestunit
from Testing import ZopeTestCase as ztc
from example.tests.tests import base
def test_suite():
"""This sets up a test suite that actually runs the tests in the class
above
"""
return unittest.TestSuite([
# Here, we create a test suite passing the name of a file relative
# to the package home, the name of the package, and the test base
# class to use. Here, the base class is a full PloneTestCase, which
# means that we get a full Plone site set up.
# The actual test is in integration.txt
ztc.ZopeDocFileSuite(
'tests/integration.txt', package='example.tests',
test_class=base.ExampleFunctionalTestCase,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE |
doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS),
# We could add more doctest files here as well, by copying the file
# block above.
])
Here, we set ExampleFunctionalTestCase from base.py as the test_class, which means that self in the doctest will be the same as self in the test class we saw in the previous section. In particular, we can access variables such as self.portal and self.folder. We also set some common doctest option flags - reporting only the first failure (to avoid overly long error output when an example early on in the doctest fails), normalising whitespace (so that we can use newlines freely) and allowing the ellipsis operator everywhere (as opposed to having to turn it on each time we want to use it). Look at the doctest module documentation for more information.
The test itself, in tests/integration.txt, is written much like the other doctests we have seen:
======================
An integration doctest
======================
This test is an integration test that uses PloneTestCase. Here, 'self' is
the test class, so we can use 'self.folder', 'self.portal' and so on. The
setup is done in teststest_integration_doctest.py
Being a doctest, we can tell a story here.
For example, let's say a user had a dying wish: to add a news item. We'll do
that using the standard Plone API.
>>> self.folder.invokeFactory('News Item', 'news-item')
'news-item'
That's great, but really, he wanted to add it to the portal root:
>>> self.portal.invokeFactory('News Item', 'news-item')
Traceback (most recent call last):
...
Unauthorized: Cannot create News Item
Whoops! Too bad!
At least we got to demonstrate the ellipsis operator, which
matches arbitrary text. We enabled this in test_integration_doctest.py. It
is also possible to enable (or disable) this flag on a single statement.
See the Python doctest documentation for more information.
To run this test on its own, we would do:
./bin/instance test -s example.tests -t integration.txt
Running:
..
Ran 2 tests with 0 failures and 0 errors in 0.384 seconds.
Again, we have cut out some of the output from PloneTestCase.
7.10. Functional and system tests with zope.testbrowser
Whilst unit tests and doctests verify the correctness of individual methods and modules, functional tests test portions of the application as a whole, often from the point of view of the user, and typically aligned with use cases. System tests, in comparison, test the entire application as a black box.
No developer likes to click around the browser to check if that button that was only supposed to show up in some cases really did show up. Unfortunately, these are also the types of problems that most often suffer from regressions, because templates are difficult (and slow) to test.
Zope 3 has an elegant library called zope.testbrowser which lets you write doctests that behave like a real web browser (almost... it cannot yet handle JavaScript, which means that testing dynamic UIs that depend on JavaScript is not possible, although Selenium may be a viable alternative here). You can open URLs, click links, fill in form fields and test the HTTP headers, URLs and page contents that are returned from Plone. In fact, you could test any website, not just Zope or Plone ones.
Functional tests are no replacement for unit tests. They test a slice of functionality, typically as the user sees it. Thus, they may not systematically include every aspect of the application. For example, a functional test may check whether a "Delete" button is present, and even that it works as expected, but should not be used to exhaustively test whether the delete operation works in every possible edge case. Where they excel, however, is in testing things like which options appear to which users depending on roles and permissions, or simply to exercise all the various templates used in a given product to make sure they don't break.
Here is an example from the example.tests package. The test setup is in tests/test_functional_doctest.py:
"""This is a a functional doctest test. It uses PloneTestCase and doctest
syntax. In the test itself, we use zope.testbrowser to test end-to-end
functionality, including the UI.
One important thing to note: zope.testbrowser is not JavaScript aware! For
that, you need a real browser. Look at zope.testbrowser.real and Selenium
if you require "real" browser testing.
"""
import unittest
import doctest
from Testing import ZopeTestCase as ztc
from example.tests.tests import base
def test_suite():
"""This sets up a test suite that actually runs the tests in the class
above
"""
return unittest.TestSuite([
# Here, we create a test suite passing the name of a file relative
# to the package home, the name of the package, and the test base
# class to use. Here, the base class is a full PloneTestCase, which
# means that we get a full Plone site set up.
# The actual test is in functional.txt
ztc.ZopeDocFileSuite(
'tests/functional.txt', package='example.tests',
test_class=base.ExampleFunctionalTestCase,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE |
doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS),
# We could add more doctest files here as well, by copying the file
# block above.
])
This code is actually identical to the test setup for the integration doctest in the previous section. The differences are found in the actual test itself, which uses Products.Five.testbrowser.Browser, a Zope 2 compatability wrapper around zope.testbrowser.Browser:
====================
A functional doctest
====================
This is a full-blown functional test. The emphasis here is on testing what
the user may input and see, and the system is largely tested as a black box.
We use PloneTestCase to set up this test as well, so we have a full Plone site
to play with. We *can* inspect the state of the portal, e.g. using
self.portal and self.folder, but it is often frowned upon since you are not
treating the system as a black box. Also, if you, for example, log in or set
roles using calls like self.setRoles(), these are not reflected in the test
browser, which runs as a separate session.
Being a doctest, we can tell a story here.
First, we must perform some setup. We use the testbrowser that is shipped
with Five, as this provides proper Zope 2 integration. Most of the
documentation, though, is in the underlying zope.testbrower package.
>>> from Products.Five.testbrowser import Browser
>>> browser = Browser()
>>> portal_url = self.portal.absolute_url()
The following is useful when writing and debugging testbrowser tests. It lets
us see all error messages in the error_log.
>>> self.portal.error_log._ignored_exceptions = ()
With that in place, we can go to the portal front page and log in. We will
do this using the default user from PloneTestCase:
>>> from Products.PloneTestCase.setup import portal_owner, default_password
>>> browser.open(portal_url)
We have the login portlet, so let's use that.
>>> browser.getControl(name='__ac_name').value = portal_owner
>>> browser.getControl(name='__ac_password').value = default_password
>>> browser.getControl(name='submit').click()
Here, we set the value of the fields on the login form and then simulate a
submit click.
We then test that we are still on the portal front page:
>>> browser.url == portal_url
True
And we ensure that we get the friendly logged-in message:
>>> "You are now logged in" in browser.contents
True
To learn more, look at the zope.testbrowser documentation and interfaces.
There are also a few examples of testbrowser tests in Plone itself.
All the action happens with the browser object. This simulates a web browser (though as stated above, one that does not support JavaScript), and has a pleasant API for finding form controls and links and clicking on them. The variables browser.url and browser.contents represent what would've been in the URL bar and the rendered view of the page, respectively, and can be examined like any other variable.
zope.testbrowser has pretty comprehensive documentation in its README.txt file - which is, of course, a runnable doctest. In brief, the most important methods of the IBrowser interface (and thus the Browser class) are:
- open(url)
- Open a given URL.
- reload()
- Reload the current page, much as the Refresh button in your browser would do.
- goBack(count=1)
- Simulate pressing the Back button count times.
- getLink(text=None, url=None, id=None)
- Get an ILink (which you can then call click() on), either by the text inside the <a> tags, by the URL in the href attribute, or the id of the link.
- getControl(label=None, name=None, index=None)
- Get an IControl, representing a form control, by label (either the value of a submit button or the contents of an associated <label> tag) or form name. The index argument is used to disambiguate if there is more than one control (e.g. index=0 gets the first one). Again, you can call click() on the control object to simulate clicking on it.
The IBrowser interface also provides some properties that can be used to examine the state of the current page. The most important ones are:
- url
- The full URL to the current page.
- contents
- The full contents of the current page, as a string (usually containing HTML tags)
- headers
- A dict of HTTP headers
Please refer to the interfaces and the README file for details on the other methods and attributes, the interfaces for various types of links and controls, and further examples.
Debugging functional tests
Sometimes you will get errors from Zope resulting from some command executed using the testbrowser. In this case, it can sometimes be difficult to know what the underlying cause is. Two debugging aids exist to make this a bit easier.
First of all, make sure you see all errors in full by setting:
>>> browser.handleErrors = False
If handleErrors is True (the default) you will get errors like HTTPError: HTTP Error 404: Not Found or HTTPError: HTTP Error 500: Internal Server Error. Those are probably not very useful to you. Setting handleErrors to False will show the full exceptions Zope (or possibly the HTML rendering of the error page, depending on the type of error).
Secondly, if you are using PloneTestCase, you can use Plone's error log. At the top of the example, we do:
>>> self.portal.error_log._ignored_exceptions = ()
This means that errors such as NotFound and Unauthorized will be shown in the error log. It may also be useful to enable Verbose Security in zope.conf (see the comments in that file for details). Now, when a line appears that is throwing an error you can't debug, you can do:
>>> try:
... browser.getControl('Save').click()
... except:
... print self.portal.error_log.getLogEntries()[0]['tb_text']
... import pdb; pdb.set_trace()
>>> # continue as normal
This will print the most recent entry in the error log, and set a PDB break point.
Using a real browser to render the results of your tests
Sometimes you would like to see the output of browser.contents in a browser to easily debug what's happening in your functional tests. To do so, place a PDB break point in your tests as described above (import pdb; pdb.set_trace()) and type the following when you get to the PDB prompt while running the tests:
>>> from Testing.ZopeTestCase.utils import startZServer >>> startZServer()
This will print a tuple like
('127.0.0.1', 55143)
containing an IP address and port where you can access the same test site that the testbrowser is working with, in a real browser.
Functional tests vs. system tests
A system test is one which treats the entire system as a black box, interacting with it as a user would. A functional test is more focused on a single "vertical" of functionality, typically linked to a particular use case.
For a functional test, it may be acceptable to examine the internal state of the portal (using self.portal and the PloneTestCase.FunctionalTestCase class to build a test suite) to provide assertions. A system test, by contrast, makes no such assumptions. Ideally, you should be able to point a zope.testbrowser test at a remote site running a fresh installation of your system, and have the tests pass.
Beyond that, the tools used to write a system test are the same. It is only the approach to testing that changes. Whether you need one, or the other, or both, will depend on the level of rigour you need in your tests, and how your system is constructed. In general, though, true system tests are more rare than functional (integration) tests and unit tests.
7.11. Using zope.testrecorder to record functional tests
The zope.testrecorder product brings us full-circle: functional tests are recorded from within the browser, and saved to a runnable test.
Functional tests using zope.testbrowser save us from clicking around the browser to regression test UI, but writing them could still be easier. With complex templates, it can sometimes be difficult to find out what actual links and form fields the testbrowser test should be looking for, and what text to use in assertions.
This is where zope.testrecorder comes in. The theory is that you click around the UI only once, and then render the history of what you did to a runnable testbrowser test. zope.testrecorder can even create Selenium tests - an alternative form of functional tests which runs in the browser (i.e. it automates your browser right before your eyes) and thus supports JavaScript, but which cannot be run as part of an automated test run without a browser.
Installing zope.testrecorder is simple. First, check it out from Zope's subversion repository:
svn co svn://svn.zope.org/repos/main/zope.testrecorder/trunk zope.testrecorder
See INSTALL.txt for further instructions, but the easiest way to install it in a Zope 2 instance is just to put it in your Products directory: Copy zope.testrecorder/src/zope/testrecorder as a product into Products/testrecorder and restart Zope. Then, go to the ZMI and add a Test Recorder object in the root of your Zope instance. Call it e.g. test-recorder.
Presuming you run Zope on localhost:8080, you should now be able to go to http://localhost:8080/test-recorder/index.html. You should see a page something like this:

NOTE: Like most things, zope.testrecorder seems to work better in Firefox than in other browsers.
Now, enter the address of your Plone site (or indeed any web site), e.g. http://localhost:8080/Plone and click Go. You can perform any number of operations, e.g. logging in and clicking around the UI. If you wish to add a comment to your test run, as you would add free text inside a doctest, click the Add comment button. If you wish to verify that some text appears on the page, highlight that text, shift-click on it, and select "Check text appears on page":

When you are done, click Stop recording. You can then choose to render the test as a Python doctest and you will get something like:
Create the browser object we'll be using.
>>> from zope.testbrowser import Browser
>>> browser = Browser()
>>> browser.open('http://localhost/test')
A test comment.
>>> 'start writing' in browser.contents
True
You can then paste this into a doctest file, and perform any post-processing or make any changes that may be necessary to make the test more generally valid.
Tips for using zope.testrecorder
- Plan, plan, plan
- It's best if you have a rough script in front of you before you start recording tests, or you may get lost afterwards. Make good use of the Add comment button to state what you are testing before you test it, so that the final doctest will make sense.
- Careful where you click
- Some parts of the Plone UI are more ephemeral than others. It may not be a good idea to rely on links in the Recent portlet, for example. Think about what operations will provide the most general and valid test. It will save you time in the long run.
- Set up your site beforehand
- Recall from the section on zope.testbrowser that we set up users and basic site structure with calls to the Python APIs instead of using testbrowser to manipulate the "site setup" screents. When using zope.testrecorder you may want to set up the same users with the same user names and passwords, and the same site structure before you start recording to test. Otherwise, you may need to change some of the values of the test.
- Check the doctest
- zope.testrecorder is a time-saving tool. Sometimes, it may end up referring to parts of the page that can't be guaranteed to be consistent (such as randomly generated ids of content objects), and sometimes you may have gone on a detour and ended up with a test that contains irrelevant or duplicate sections. Always fix up your test (and run it!) afterwards, to make sure that the test remains valid for the future - otherwise, you will end up clicking around the UI in anger again before you know it.
7.12. Determining the Code Coverage of your Test Suite
Explanation for how to use the Zope test runner's built in code coverage features to prove the quality of your test suite.
The better your test suite's coverage, the lower the likelihood that some modification to your code will break another piece of functionality in some unanticipated way. But, how do you know the quality of your test coverage? Zope's test runner comes with several features to help you do just that.
But first, let's say you've written some code with a Python conditional like the following:
if value % 2 == 0:
print "This is an even number"
else:
# we need to do some more complex
# computation to handle odd numbers
_someComplexCodeDealingWithOddNumbers(value)
The comments and function call in the else clause are symbolic of some advanced coding that's required to handle all odd numbers.
Now, as you've no doubt learned while reading this tutorial, testing is important. But what if for one reason or another, all the test cases you've come up with during testing amount to even numbers when you get to the aforementioned block of code. If this were the case, you'd have a big risk of unanticipated code breakage to the way that you handle odd numbers. This is something that you'd ideally cover in your test suite.
Discovering the untested sections of your code
You've learned how to run your test suite in this tutorial. Zope's test runner accepts an optional parameter called --coverage. When passed a path to a directory, Zope will generate some high-level output and produce a coverage file for each of the Python modules in your product or package.
In full, running your test suite with the coverage option enabled looks like:
./bin/instance test -s Products.productname --coverage=$HOME/coverage
Note: Running your tests with the coverage option enabled takes significantly longer (as in ~10 times or more) than without, so this is something to be done occasionally to gauge your work, rather than each time you run your tests.
At the end of running your test suite, you'll get some immediate output like the following, which includes lines of code and your coverage percentage:
lines cov% module (path)
104 100% $INSTANCE_HOME.parts.salesforce-integration-products.salesforcepfgadapter.Extensions.Install
($INSTANCE_HOME/parts/salesforce-integration-products/salesforcepfgadapter/Extensions/Install.py)
39 41% $INSTANCE_HOME.parts.salesforce-integration-products.salesforcepfgadapter.__init__
($INSTANCE_HOME/parts/salesforce-integration-products/salesforcepfgadapter/__init__.py)
2 100% $INSTANCE_HOME.parts.salesforce-integration-products.salesforcepfgadapter.content.__init__
($INSTANCE_HOME/parts/salesforce-integration-products/salesforcepfgadapter/content/__init__.py)
168 91% $INSTANCE_HOME.parts.salesforce-integration-products.salesforcepfgadapter.content.salesforcepfgadapter
($INSTANCE_HOME/parts/salesforce-integration-products/salesforcepfgadapter/content/salesforcepfgadapter.py)
21 100% $INSTANCE_HOME.parts.salesforce-integration-products.salesforcepfgadapter.migrations.migrateUpTo10rc1
($INSTANCE_HOME/parts/salesforce-integration-products/salesforcepfgadapter/migrations/migrateUpTo10rc1.py)
If all you're looking for is a quick status report, this should suffice.
However, if you want to dig deeper, head to the directory you listed in the --coverage option. Note: The files may be preceded with dots, thus requiring an ls -a in order to reach the coverage files.
A sample file may look like the following:
1: def initializeArchetype(self, **kwargs):
"""Initialize Private instance variables
"""
15: FormActionAdapter.initializeArchetype(self, **kwargs)
15: self._fieldsForSFObjectType = {}
1: security.declareProtected(View, 'onSuccess')
1: def onSuccess(self, fields, REQUEST=None):
""" The essential method of a PloneFormGen Adapter
"""
>>>>>> logger.debug('Calling onSuccess()')
>>>>>> sObject = self._buildSObjectFromForm(fields, REQUEST)
>>>>>> if len(sObject.keys()) > 1:
It's really just your file with some meaningful data proceeding each line. Anything with a 1: signifies that your code was at least touched during the running of the test suite. The higher the number, the more often your code was touched. Perhaps this is intentional and signifies really good coverage in other cases, it's may be either unavoidable or could even signify that the high level of coverage wouldn't actually be required. The >>>>>> means that you've missed a line and you should consider coming up with a test scenario or more that will touch the line of code in question. The number of untested lines divided by total lines gives you your coverage percentage.
If what you really want is eye-candy
If you want pretty graphs to provide for you boss to include in a report or to make a client feel better about the quality of code they are receiving, z3c.coverage takes the contents of the output files and creates pretty summaries. Get z3c.coverage from subversion via the following:svn co svn://svn.zope.org/repos/main/z3c.coverage/trunk z3c.coverage
Create a directory within your previously created coverage directory. We call it reports. Run the coveragereport.py module with the source being you coverage output and the destination, your newly created reports directory. See the following:
mkdir $HOME/coverage/reports python z3c.coverage/src/z3c/coverage/coveragereport.py $HOME/coverage $HOME/coverage/reports
You should now be able to open $HOME/coverage/reports/all.html within your browser for a pretty output like the one below.

With this information available, you can start to make conclusions about how you may work your way towards better coverage of your product.
7.13. Testing examples
Here, we list a few packages and projects that demonstrate good test coverage
Testing is best learned by example. It can be very instructive to read through the tests written by other developers and learn what they test, what they don't test and how they write their tests.
- example.tests, which we have already mentioned, contains an example of each of the different types of tests covered in this tutorial. The test setup code is well-commented, with the intention that this package should provide good boilerplate for developers setting up a new project.
- Plone itself has more than 1,600 tests at the time of writing. Most of these are integration tests using unit-test syntax with PloneTestCase.
- RichDocument has a basic test_setup.py integration test. This is a good example of the kind of testing you may want to do to ensure that your package installs cleanly.
- borg.project contains a README.txt file with an integration doctest demonstrating how it is used. It has only a single test module, tests.py, which performs the same setup as base.py and test_integration_doctest.py from example.tests.
- Many of the tests in the plone.app.controlpanel package use basic test-browser functional tests to verify that the Plone control panels work as expected.
Feel free edit or comment on this page if you have more examples to add!
8. Generic Setup
GenericSetup gives developers a way to easily import and export site configuration
8.1. What is GenericSetup?
A brief overview of what GenericSetup is
GenericSetup is a product that allows developers to import and export site configuration. This product was created to counteract Seaver's Law, which states the following: "Persistence means always having to say you're sorry". This means that any time you configure your Plone site purely through the Plone interface or ZMI you run the risk of losing your configuration. Since your configuration was only persisted into the ZODB, if you lose your data, you also lose your configuration.
This is where GenericSetup comes in to save the day. You can still make all your configuration changes in the Plone site or through the ZMI. Then when you are ready you can export these settings using the portal_setup tool.
GenericSetup was introduced in Plone 2.5 to help replace the use of customization policies and the quick installer. The old methods relied on the user to write Python code to set up their product or site. When the API of the tools and objects changed the install and uninstall methods would also have to be changed. GenericSetup gives us a way to deal with the settings that we want to import and export and not have to worry about the API changing over time.
8.2. GenericSetup Profiles
How to setup a GenericSetup profile
8.2.1. What is a profile?
Information on what a GenericSetup profile is for
A GenericSetup profile is a preset configuration of a site, defined on the filesystem. This configuration can consist on a broad variety of settings, including roles and permissions mappings, skin layers order, site properties...
8.2.2. Profile types
Information about the different types of GenericSetup profiles
GenericSetup has two kinds of profiles that you can create. A base profile is a profile that defines the basis for the site configuration. In Plone the base profile is the "plone" profile from the CMFPlone product. A base profile is not typically something that product developers needs to deal with. There can only be one base profile in use, when you create a new Plone site the base profile is applied for you. The base profile will setup a default set of import and export steps along with a default configuration for each.
The second type of profile is an extension profile. An extension profile is what allows us to build on top of the base profile. When you create a new product, theme or policy package you will register extension profiles to build on top of the existing configuration. Extension profiles also allow you to define new import and export steps that other extension profiles can use.
8.2.3. Registering a profile
Learn how to register a profile
GenericSetup profiles are registered via ZCML. The registration is typically done in the configure.zcml of your package. Let's register a default profile for our example my.package
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="my.package">
<genericsetup:registerProfile
name="default"
title="My Package"
directory="profiles/default"
description="Default profile for My Package"
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
</configure>
This is what we would see in the configure.zcml of my.package. Let's examine what each of the attributes of the registerProfile directive mean.
- name
- The name is how GenericSetup will identify your profile. The full profile id is "profile-<package_name>:<profile_name>", in our case profile-my.package:default
- title
- The title of the profile will show up in the portal_setup tool when you select what profile you want to import. The title may also be used in the portal_quickinstaller if it picks up the profile as the install profile for your package.
- directory
- The directory attribute is the relative path to the folder containing the profile information. The directory name is conventionally the same as the profile name.
- description
- The description gives a brief summary of what the profile is used for.
- provides
- The provides attribute tells GenericSetup what kind of profile you are registering. For almost all cases you will be registering an extension profile to extend Plone's base profile.
8.3. Using GenericSetup Efficiently
Tips about getting the most out of GenericSetup
Once you have your GenericSetup profile set up you'll need to start adding import files into it. It is common for people to think that they have to write all the XML by hand. This should almost never be the case.
Change and export
The easiest way to to make changes with GenericSetup is to make the changes in your site and then export the corresponding step. Let's start with a very basic example, changing the MailHost settings so that we can send out mail.
Go to the ZMI (http://yourplonesite:8080/manage) and find the MailHost object in the Plone site root. Enter the configuration data for your mail provider. Now go to portal_setup and click on the Export tab. Click the checkbox next to the MailHost import step and then on the Export selected steps button.
A targz bundle with the XML declarations of this exported import step will be generated and downloaded. Note you can select as many import steps as necessary or even export all steps directly so you have a complete snapshot of the configuration of the your site.
Trimming down the export
When you export settings from from GenericSetup, you will get more than what you changed. It's a very good idea to trim down the export file to only have the specific items that you changed. If you don't trim down the file then you will inadvertently override a change that is made in another profile.
A common example is the propertiestool.xml export. This exports the properties in the portal_properties tool. Let's say you have modified the default_language property in portal_properties/site_properties and you want to save it for your policy product. If you go to portal_setup and export the Plone Properties, the generated propertiestool.xml will more than 100 lines long.
Since you have only modified a property and you're using a extension profile, you only have to keep the configuration data for this property.
<?xml version="1.0"?> <object name="portal_properties" meta_type="Plone Properties Tool"> <object name="site_properties" meta_type="Plone Property Sheet"> <property name="default_language" type="string">en</property> </object> </object>
Play well with others
When writing Plone add-ons you need to be aware of how your profile is going to interact with other add-ons and custom packages.
By default existing settings are purged before applying settings from base profiles. Extension profiles are applied in update mode.
However, you can use the purge="True" and purge="False" directives to override the default behavior. If True the existing settings of the current object are always purged, if False they are not purged.
For example, the declarations in the propertiestool.xml in an extension profile:
<?xml version="1.0"?>
<object name="portal_properties">
<object name="navtree_properties">
<property name="metaTypesNotToList" type="lines" purge="False">
<element value="Promotion"/>
</property>
</object>
</object>
will append the Promotion type to the list of content types not to list in the navigation trees, leaving the rest of them intact, whereas, in the same extension profile
<?xml version="1.0"?>
<object name="portal_properties">
<object name="navtree_properties">
<property name="metaTypesNotToList" type="lines" purge="True">
<element value="Promotion"/>
</property>
</object>
</object>
would remove all previous elements from the mentioned list and let Promotion alone.
User editable settings
Another thing to take into consideration is the fact that some of the properties or settings that you are importing can be changed by the end user. The most common of these properties would be the site title. The end user can easily change this setting by going to the Plone control panel.
Be careful not to place a setting like this in a profile that runs when your product is re-installed. This could lead to the end users' changes being wiped out each time. You can place these settings in another profile that can be run on the initial creation of the site.
8.4. GenericSetup Reference
Information about how each import/export step handles the data given to it
8.4.1. Actions
Information about action import and export
GenericSetup can export the actions that are set up in each of the action providers. When you export the actions step then you will be given an actions.xml file. Here is an excerpt from the actions.xml file.
<?xml version="1.0"?>
<object name="portal_actions" meta_type="Plone Actions Tool"
xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<action-provider name="portal_workflow"/>
<action-provider name="portal_types"/>
<action-provider name="portal_actions"/>
<object name="document_actions" meta_type="CMF Action Category">
<object name="print" meta_type="CMF Action" i18n:domain="plone">
<property name="title" i18n:translate="">Print this</property>
<property name="description" i18n:translate=""></property>
<property name="url_expr">string:javascript:this.print();</property>
<property name="icon_expr">string:$portal_url/print_icon.png</property>
<property name="available_expr"></property>
<property name="permissions">
<element value="View"/>
</property>
<property name="visible">True</property>
</object>
<!-- more actions here -->
</object>
</object>
Here we can see that there are three different things being defined here. First is the list of action providers as defined in the portal_actions tool. Each action-provider element provides the name of the tool or object that provides the actions. In the following example the types tool is being set up.
<action-provider name="portal_types"/>
The next element that you see is the document_actions. This is an action category in the portal_actions tool. Each action category is a folder of actions. You can see that the category is defined as follows.
<object name="document_actions" meta_type="CMF Action Category">
<!-- contained actions here -->
</object>
Action Objects
Inside the action category are the actions. In our example above we were looking at the print action. Here is the action.
<object name="print" meta_type="CMF Action" i18n:domain="plone">
<property name="title" i18n:translate="">Print this</property>
<property name="description" i18n:translate=""></property>
<property name="url_expr">string:javascript:this.print();</property>
<property name="icon_expr">string:$portal_url/print_icon.png</property>
<property name="available_expr"></property>
<property name="permissions">
<element value="View"/>
</property>
<property name="visible">True</property>
</object>
The object element has a name of print, this is the id of the action. We know that this object is an action because its meta_type is CMF Action. Inside of the action object we can see the properties for that action. These correspond to the form that we see in the ZMI.
Each property consists of the name and the value inside the element. Let's say we wanted to add a description to the print action. We would modify the description property as follows.
<property name="description">Print the current page</property>
Permissions
The permissions property accepts multiple elements. If you want multiple permissions you could add multiple elements to the permissions property as follows.
<property name="permissions">
<element name="Modify portal content"/>
<element name="Manage portal"/>
</property>
The name of each element is the name of the permission as seen on the security tab or on the action form in the ZMI. Of course, the easiest way to modify these settings is to change them in the ZMI and then export. This way there is no guess work involved.
Visible
The visible property is a boolean. This means it has a slightly different syntax. If you want the action to show up then you would set it to True.
<property name="visible">True</property>
And of course, if you want to hide it then you would set the value to False.
<property name="visible">False</property>
Please note that hiding an action affects only the UI and won't disable it. It will still be accessible using the associated URL.
Removing Action Objects
You can remove actions and action categories via the actions.xml. First let's see how to remove a particular action. In the following example we will remove the print action.
<object name="print" remove="True"/>
Since we are removing the action, all we need to do is define the name and add the remove attribute. Here is what our actions.xml would look like if we were to remove that action.
<?xml version="1.0"?>
<object name="portal_actions">
<object name="document_actions">
<object name="print" remove="True"/>
</object>
</object>
We could do the same for the whole category with the following.
<?xml version="1.0"?>
<object name="portal_actions">
<object name="document_actions" remove="True"/>
</object>
8.4.2. Catalog
Information about the import and export of catalog indexes, metadata and settings.
The Catalog Tool is the tool that Plone uses for indexing and searching content. Check the Indexing and Searching section of the Developer Manual for more information about how the Catalog Tool works.
Imagine you've created an 'Apartment' content-type using Archetypes with a field called 'address'. To index the address of each apartment so to be able to search by this field, create a catalog_tool.xml file in your extension profile with the following content:
<?xml version="1.0"?> <object name="portal_catalog" meta_type="Plone Catalog Tool"> <index name="address" meta_type="FieldIndex"> <indexed_attr value="getAddress" /> </index> <column value="a_field" /> </object>
The first line after the xml declaration specifies the type of object GenericSetup is dealing with: the Plone Catalog Tool, with id portal_catalog.
The 'index' node will create a new index of the type 'meta_type' with the name specified. You can find a list of the available meta-types, as well as the differences between them in the Searching and Categorizing Content section of The Zope Book.
The 'indexed_attr' node specifies the name of the attribute or method that will be called in each content object to index its values. This is useful, for example, if you want to create an index named 'Address' from the values returned by the getAddress() method.
The 'column' node will register a new column in the metadata table of the Plone Catalog Tool, so to include it in the returned brains.
Some index types has different properties which can be set up using 'property' and 'extra' nodes. For example, the default 'modified' index in Plone is registered with the following code:
<index name="modified" meta_type="DateIndex"> <property name="index_naive_time_as_local">True</property> </index>
Here, the property specifies that the time stored will be the local time instead of the UTC time.
Another example is the default 'SearchableText' field:
<index name="SearchableText" meta_type="ZCTextIndex"> <indexed_attr value="SearchableText"/> <extra name="index_type" value="Okapi BM25 Rank"/> <extra name="lexicon_id" value="plone_lexicon"/> </index>
Here, the index_type attribute specifies the ranking strategy for the index. For searches in text indexes, the results are returned in order of relevancy - the algorithm used to order the results is the ranking strategy. The available ranking stragegies are Okapi BM25 Rank and Cosine Measure. See The sorting subsection of the Searching and Categorizing Content section of The Zope Book for details on this.
The lexicon_id specifies the lexicon used for the text index. Lexicons process and store the words from the text and help in processing queries, for example, turning the text into lowercase or removing very common words in a language like "the" or "and". See The Lexicons subsection of the Searching and Categorizing Content section of The Zope Book for details on this.
8.4.3. Control Panel
Information about the import and export of the Control Panel settings
The controlpanel.xml file keeps the information about the different configuration dialogs available for the different users, including members and site managers.
An excerpt of the the Plone controlpanel.xml file showing the general syntax follows:
<?xml version="1.0"?>
<object name="portal_controlpanel" meta_type="Plone Control Panel Tool"
xmlns:i18n="http://xml.zope.org/namespaces/i18n" i18n:domain="plone">
...
<configlet title="Change Password" action_id="MemberPassword" appId="Plone"
category="Member" condition_expr="python:member.canPasswordSet()"
icon_expr="string:$portal_url/lock_icon.png"
url_expr="string:${portal_url}/password_form" visible="True"
i18n:attributes="title">
<permission>Set own password</permission>
</configlet>
...
</object>
Each configuration dialog is called a configlet. The different attributes available are:
- title
- The human readable text to be shown in the Plone interface.
- action_id
- A unique identifier for the configlet.
- appId
- An application identifier for the configlet. This application id can be shared by more than one configlet and can be useful, for example, to remove all configlets related to a certain application in one go.
- category
- A category id to specify the area of the Plone interface where the configlet should be shown. Using the Plone category will cause it to be shown in the main site setup configuration panel, while the Member category will make it show in the list of configuration options for the account of the user logged in.
- condition_expr
- A TALES expression to control if the configlet is available or not. In the example above, the Change Password configlet will only be available if the current user can actually change her password.
- icon_expr
- The URL of the icon for the configlet in TALES, to be shown in the Plone interface where applicable. Leave empty if there isn't any icon associated.
- url_expr
- The URL of the configlet dialog, i.e. the view or form the user should use to perform the configuration changes.
- visible
- Controls if the configlet will be shown or not. Useful if you want to disable a configlet temporarily for any reason.
The <permission> nodes inside the <configlet> one specify the neccessary permissions to use the configlet.
8.4.4. Component Registry
Information about component registration with GenericSetup.
To register an (Zope3-like) utility local to your Plone site you can use a componentregistry.xml file in your profile.
Imagine you write an utility that connects to an external Internet video streaming service like YouTube, sending and receiving data using the API provided by them. Since you can only have a single simultaneus connection, you need an singleton utility to look up and use. Each site (you have more than one in the same instance) has its own login account, so you need to register the utility locally instead of globally.
Let's say that your utility provides the yourcompany.video.interfaces.IExternalVideo interface, and it's implemented in the yourcompany.video.ExternalVideo.ExternalVideo class. Write the following code into your componentregistry.xml file:
<?xml version="1.0"?> <componentregistry> <utilities> <utility interface="yourcompany.video.interfaces.IExternalVideo" factory="yourcompany.video.ExternalVideo.ExternalVideo" /> </utilities> </componentregistry>
The utility will be registered upon the installation of the product.
8.4.5. Factory Tool
Information about the Factory tool settings import and export.
Use the factorytool.xml import steps to include new types into the Plone Factory Tool when creating new Archetypes content-types. This is needed because Archetypes needs the content to be already created before validating it. In order to avoid broken objects in the site, they're first created in a place called portal factory and moved to the desired place later after they've been validated.
Imagine you've created the Cat content-type and want to register a factory for it. Just create a factorytool.xml file for your extension profile with the following contents:
<?xml version="1.0"?> <object name="portal_factory" meta_type="Plone Factory Tool"> <factorytypes> <type portal_type="Cat"/> </factorytypes> </object>
Check the Archetypes Developer Manual for more info about how to create content-types.
8.4.6. MailHost
Information about the import and export of MailHost settings.
Use this step to configure the MailHost tool to match your SMTP server settings. This example should speak for himself:
<?xml version="1.0"?>
<object name="MailHost" meta_type="Secure Mail Host"
smtp_host="smtp.mydomain.tld"
smtp_port="25"
smtp_pwd="secret123" smtp_uid="myusername@mydomain.tld"/>
8.4.7. Metadata
Info about GenericSetup metadata
GenericSetup profiles can define a metadata.xml file with some metadata about that particular profile. The contents are straight forward and don't need much explanation. Here is an example.
<?xml version="1.0"?>
<metadata>
<description>This is the profile description</description>
<version>1</version>
<dependencies>
<dependency>profile-my.package:default</dependency>
<dependency>profile-collective.foo:default</dependency>
</dependencies>
</metadata>
There are three pieces to the metadata.xml file. We will go into more detail for each piece here.
Description
First there is the description element. This is where you can give a brief explanation of the profile.
<description>My package's default installation profile</description>
Version
Next is the version element. This tells GenericSetup what the version of the profile is. The version number will be used to run upgrade steps. Upgrade steps only run between the given version numbers. The version will be a whole number that is incremented when the profile has been changed. Here is an example.
<version>42</version>
In Plone 3.1 the practice of using the GenericSetup profile version as a fall back for the QuickInstaller version was deprecated. The QuickInstaller version will be taken from your product's version instead.
Dependencies
You can define profile dependencies via the metadata.xml file. When your profile is run, the dependencies that are listed will be run first. You can define as many dependencies as you like. If you wanted your profile to depend on the fictitious my.package profile named default then you would have the following in your metadata.xml.
<dependencies>
<dependency>profile-my.package:default</dependency>
</dependencies>
The format for the profile entries are profile-<package_name>:<profile_name>. Here is an example of what an egg in the Products namespace would look like.
<dependencies>
<dependency>profile-Products.MyPloneProduct:default</dependency>
</dependencies>
See the profile reference page for more details about GenericSetup profiles.
8.4.8. Portlets
Information about the import and export of portlets
The portlets.xml file lets you register portlets and portlet managers. For more info about the portlets infrastructure, check the Portlets chapter of the Developer Manual.
Let's see an excerpt of the Plone portlets.xml file:
<?xml version="1.0"?>
<portlets
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="plone">
...
<portletmanager
name="plone.leftcolumn"
type="plone.app.portlets.interfaces.ILeftColumn"
/>
...
<portlet
addview="portlets.News"
title="News"
description="A portlet which can render a listing of recent news"
i18n:attributes="title;
description"
>
<for interface="plone.app.portlets.interfaces.IColumn" />
<for interface="plone.app.portlets.interfaces.IDashboard" />
</portlet>
</portlets>
The <portletmanager /> node registers a portlet manager. The available attributes are:
- name
- The name of the utility providing the IPortletManager interface which will be instantiated.
- type
- A marker interface that can be used to install
particular portlets only for particular types of portlet managers later.
The <portlet> node registers a portlet. The available attributes are:
- addview
- Somewhat confusing, should match the portlet name as registered in ZCML.
- title and description
- Human friendly text to be shown in the Plone user interface.
The <for /> nodes inside the <portlet> one specify the interfaces of the type of portlet managers that this portlet is suitable for.
The
<portlet addview="portlets.BBB"
title="Foo"
description="Foo"
for="plone.app.portlets.interfaces.IColumn" />
syntax to specify the allowed portlet manager interfaces was deprecated in Plone 3.1 and won't work in Plone 4.
Assigning portlets
You can assign portlets to certain parts of the site upon product installation using Generic Setup too. For example:
<assignment
manager="plone.rightcolumn"
category="context"
key="/"
type="portlets.News"
name="news"
visible="1"
/>
The visible attribute was introduced in Plone 4. If not present or equal to "1", the portlet will be shown. If it's equal to "0", it will be hidden.
You can use the insert-after and insert-before attributes matching an existing assignment or all them with "*". For example:
<assignment
insert-before="events"
manager="plone.rightcolumn"
category="context"
key="/"
type="portlets.News"
name="news"
/>
will insert the news portlet just before the events one.
You can also remove a certain assignment:
<assignment
remove="True"
manager="plone.rightcolumn"
category="context"
key="/"
type="portlets.News"
name="news"
/>
Or remove (purge)all the assignments from a certain manager:
<assignment
purge="True"
manager="plone.rightcolumn"
category="context"
key="/"
/>
The key attribute matches "/" for site-wide portlets and an absolute Zope path for context-wide ones. For example, if you want to remove all assignments from Plone/news, use:
<assignment
purge="True"
manager="plone.rightcolumn"
category="context"
key="/Plone/news"
/>
8.4.9. Properties
Information about the import and export of property sheets.
GenericSetup exports two kinds of property files by default. The properties.xml contains the properties set at the root of your Plone site. The propertiestool.xml contains all of the property sheets that are in the properties_tool. Both of these use the same syntax that will be described here. See the Properties Reference for more detailed information about what these properties do.
The propertiestool.xml is split into a section for each property sheet.
<?xml version="1.0"?> <object name="portal_properties" meta_type="Plone Properties Tool"> <object name="navtree_properties" meta_type="Plone Property Sheet"> <!-- nav properties here --> </object> <object name="site_properties" meta_type="Plone Property Sheet"> <!-- site properties here --> </object> <object name="custom_property_sheet" meta_type="Plone Property Sheet"> <property name="title" type="string">Custom Properties</property> <!-- custom_property_sheet props here --> </object> </object>
The property export handles all of the property sheet types. Single value properties are in the following format.
<property name="property_id" type="property_type">property_value</property>
Where the property_id is the id that the property will have in the property sheet and the property_value is the value that the property gets set to. Here are a couple of examples.
<property name="special_name" type="string">This is special</property> <property name="magic_number" type="int">3</property>
However, there are a couple of exceptions to this behavior.
Boolean
Boolean properties can be set as follows:
<property name="has_truth" type="boolean">true</property> <property name="has_truth" type="boolean">True</property> <property name="has_truth" type="boolean">TRUE</property> <property name="has_truth" type="boolean">Yes</property> <property name="has_truth" type="boolean">1</property>
The values True, Yes and 1 will end up being true, these are case insensitive. Any other value will be taken as False.
<property name="has_truth" type="boolean">anything besides true yes or 1 here</property>
Lines
Multi valued properties are known as lines properties. Here is an example of a grocery_list property with three values.
<property name="grocery_list" type="lines"> <element value="beer"/> <element value="chips"/> <element value="salsa"/> </property>
Each <element> will become an item in the lines property.
If you want to update existing lines properties then you need to understand the purge option. If you want to add to the list then you will put purge="false" as follows:
<property name="grocery_list" type="lines" purge="false"> <element value="wine"/> <element value="steak"/> </property>
Now the property will contain the following:
beer chips salsa wine steak
If the purge option is not explicitly set (it defaults to true) or if it is set to true, then you would get this:
wine steak
Date
A date property is similar to the string property but interprets the value as a DateTime.
<property name="day_of_infamy" type="date">December 7, 1941</property>
8.4.10. Resource Registries
Information about the import and export of resource registry settings.
This step let's you register CSS, KSS and JavaScript resources into the the corresponding registries, defining them in cssregistry.xml, kssregistry.xml and jsregistry.xml, respectively.
Let's see an example with the CSS registry. This step lets you register stylesheets in the portal_css tool. This tool is a site-wide registry providing interesting features like conditional inclusion, caching, merging or compression. Check the Resource Registries section in the Theming Manual for more info about them.
An example registration in the cssregistry.xml file would be:
<?xml version="1.0"?> <object name="portal_css" meta_type="Stylesheets Registry"> <stylesheet title="" cacheable="True" compression="safe" cookable="True" enabled="1" id="pro_members.css" media="screen" expression="not: portal/portal_membership/isAnonymousUser" rel="stylesheet" rendering="import" insert-before="ploneCustom.css" /> </object>
This code will slide in the pro_members.css stylesheet just before the ploneCustom.css one (so overriding its values if there are any collisions) when the user is not anonymous.
Check the Resource Registries section of the Theming Manual for more info about the different options (XML attributes) available.
8.4.11. Roles and Permissions
Information about the import and export of roles and permissions.
You can create roles and modify permissions using the rolemap.xml GenericSetup
import step. For more information on roles and permissions see the Permissions and roles tutorial.
Here is an example of what you will see when you export the rolemap step.
<?xml version="1.0"?>
<rolemap>
<roles>
<role name="Anonymous"/>
<role name="Authenticated"/>
<role name="Contributor"/>
<role name="Editor"/>
<role name="Manager"/>
<role name="Member"/>
<role name="Owner"/>
<role name="Reader"/>
<role name="Reviewer"/>
</roles>
<permissions>
<permission name="View" acquire="True">
<role name="Contributor"/>
<role name="Editor"/>
<role name="Reader"/>
</permission>
<!-- more permissions here -->
</permissions>
</rolemap>
The rolemap.xml is split into two sections, roles and permissions. Imagine you want to create a new ProUser role in your site. Here is what the rolemap.xml would look like.
NOTE: The rolemap.xml must include both the <roles> and <permissions> tags, even if they have empty contents - otherwise the step will error out.
<?xml version="1.0"?>
<rolemap>
<roles>
<role name="ProUser"/>
</roles>
<permissions/>
</rolemap>
You can assign the newly created role to existing permissions in the system. Here we add the ProUser role to the View permission.
<permission name="View" acquire="True">
<role name="Reader"/>
<role name="Editor"/>
<role name="Contributor"/>
<role name="ProUser"/>
</permission>
Note that if you assign roles to a permission using the <permission>
directive, it will overwrite existing assignments, so remember to include
all the already assigned roles when doing so. The following would change the View permission to only be applied to the ProUser role.
<permission name="View" acquire="True">
<role name="ProUser"/>
</permission>
The acquire attribute tells Zope wether or not to inherit the permissions from above. The default is False.
Removing roles and permissions
The rolemap handler does not handle the removal of roles or permissions. You would need to write Python code in order to remove a role from the system via your GenericSetup profile.
Permissions are typically created at startup via the package's __init__.py or being defined in zcml.
8.4.12. Sharing
Info about the sharing tab GenericSetup handler
The GenericSetup handler for the sharing tab, new in Plone 4, allows you to define new local role labels. When you add a new role to your site you may want to assign that role from the sharing tab. The sharing.xml allows us to do this.
The sharing export handler does not export the globally registered utilities. This means that by default there will be no entries in the sharing.xml when it is exported.
As an example, let's say you've added a new role named CopyEditor (see the rolemap reference for more details on adding roles) to your site. This would not be present on the sharing page. To make it show up you would add the following to your sharing.xml.
<?xml version="1.0"?>
<sharing xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="my.package">
<role
id="CopyEditor"
title="Can edit copy"
permission="Manage portal"
i18n:attributes="title"
/>
</sharing>
The sharing.xml file has a container named sharing. Inside of the sharing tag there are role tags. Here is an explanation of what the attributes of the role tag will do.
- id
- The
idis the name of the role that you have defined - title
- The
titleis what will show on the sharing tab to represent this local role - permission
- The
permissionis the Zope permission needed to assign the local role
Modify existing sharing tab entries
You can change existing sharing tab entries by redefining them. All you need to define is the id, then you can change the title and the permission to your liking. In this example we change the title to something funny and probably confusing to the end user. The permission can also be changed, here we changed it to View. If you had access to the sharing page and had the View permission on the item then you could assign the local role.
NOTE: When changing the stock local role entries you are creating a local utility that overrides the globally registered utility.
<?xml version="1.0"?>
<sharing>
<role
id="Reviewer"
title="Content Overlord"
permission="View"
/>
</sharing>
Removing entries
To remove an entry, just use the remove attribute on a <role /> node:
<role
remove="True"
id="Reviewer"
/>
8.4.13. Skins
Information about the import and export of skin paths
For some background about skin layers, read the Skin Layers section of the Theming Manual.
Let's take the skins.xml file of plonetheme.sunburst skins.xml as example:
<?xml version="1.0"?>
<object name="portal_skins" default_skin="Sunburst Theme">
<object name="sunburst_images"
meta_type="Filesystem Directory View"
directory="plonetheme.sunburst:skins/sunburst_images"/>
<object name="sunburst_templates"
meta_type="Filesystem Directory View"
directory="plonetheme.sunburst:skins/sunburst_templates"/>
<object name="sunburst_styles"
meta_type="Filesystem Directory View"
directory="plonetheme.sunburst:skins/sunburst_styles"/>
<object name="sunburst_js"
meta_type="Filesystem Directory View"
directory="plonetheme.sunburst:skins/sunburst_js"/>
<skin-path name="Sunburst Theme" based-on="Plone Default">
<layer name="sunburst_images"
insert-after="custom"/>
<layer name="sunburst_templates"
insert-after="sunburst_images"/>
<layer name="sunburst_styles"
insert-after="sunburst_templates"/>
<layer name="sunburst_js"
insert-after="sunburst_styles"/>
</skin-path>
</object>
Here, the line
<object name="portal_skins" default_skin="Sunburst Theme">
indicates we're dealing with the portal_skins object and sets Sunburst Theme as the default active theme. The later has the same effect of selecting Sunburst Theme in the Themes control panel dialog or in the portal_skins properties in the ZMI.
The <object /> nodes define a new Filesystem Directory View (FSDV):
- name
- The name of the skin layer as shown in the contents of portal_skins in the ZMI. It usually matches the name of the directory in the filesystem, but doesn't neccessarily have to.
- meta_type
- Will always be Filesystem Directory View in this context.
- directory
- The path to the directory in the filesystem in the format
[yournamespace].[your theme name]:skins/[Your Skin Directory Name]
Then, we insert the the previously defined skin layers in a theme. First, we specify we're dealing with the skin named Sunburst Theme, which will inherit the list and order of skins layers from Plone Default:
<skin-path name="Sunburst Theme" based-on="Plone Default">
Next, we insert each skin layer in the appropiate place.
<layer name="sunburst_images" insert-after="custom"/>
You can use the insert-after and insert-before attributes with the name of an already present skin layer to control the placement. If you want to insert a layer at the top of the stack, use
<layer name="layer_name" insert-before="*"/>
If you don't specify any insert-after nor insert-before attributes, the specified layer will be inserted at the bottom of the stack.
8.4.14. Structure
Information about the import of site structure
Despite this import step have been available since long time ago, the syntax is complex and underdocumented, so it's not recommended for use anymore. Use a custom import step with a Python handler and invokeFactory, getters and setters, etc. instead.
If you're still interested on this, you can check the article
8.4.15. Tools
Information about tools import
This import step registers a set of tools in the site, instantiating them at the root if they're missing or their class doesn't match the existing one.
The syntax of the toolset.xml file is very simple:
<?xml version="1.0"?>
<tool-setup>
...
<required tool_id="portal_catalog"
class="Products.CMFPlone.CatalogTool.CatalogTool"/>
...
</tool-setup>
This snippet would instantiate the famous portal_catalog object of the Products.CMFPlone.CatalogTool.CatalogTool class.
The most noticeable difference between a tool and an utility (in the Zope sense) is that tools are persistent objects that exist as content in the ZODB, while utilities are registered either site-wide or locally, but they are not listed as normal content and you won't be able to find them as such using the ZMI.
8.4.16. Types
Information about the import and export of content-types information.
8.4.17. Viewlets
Information about the import and export of viewlet settings
You can insert, sort and hide viewlets inside viewlet managers using a viewlets.xml file.
You will find detailed information about this in the tutorial Customizing the viewlets in main_template.
8.4.18. Workflow
Information about the workflow importer and exporter
You can export and import workflows and their settings this import step. The syntax is rather complex and verbose, so we recommend you to create and adjust the workflow settings manually, export them from the portal_setup tool as described in Using GenericSetup Efficiently and editing the resulting XML later.
You can declare and bind/unbind content-types to a certain workflow chain in the workflow.xml file. For example, the following snippet would register a workflow named my_workflow, set it as the default one, and unbind the My Type content-type from any workflow (including the default one):
<?xml version="1.0"?> <object name="portal_workflow" meta_type="Plone Workflow Tool"> <object name="my_workflow" meta_type="Workflow"/> <bindings> <default> <bound-workflow workflow_id="my_workflow" /> </default> <type type_id="My Type" /> </bindings> </object>
If you want to bind a particular content-type to a particular workflow, use the syntax:
<bindings>
...
<type type_id="My Type">
<bound-workflow workflow_id="my_workflow"/>
</type>
...
</bindings>
Once you've declared a workflow, you need to define it. Inside a folder named workflows in your profile, add a folder matching the name of your declared workflow and then a definition.xml file inside it. In our example above, profiles/default/workflows/my_workflow/definition.xml.
An short and simple definition.xml file looks like:
<?xml version="1.0"?>
<dc-workflow workflow_id="myt_workflow"
title="My Content"
state_variable="review_state"
initial_state="draft">
<!-- Permissions being managed -->
<permission>View</permission>
<!-- Workflow states, available transitions and permissions mapping -->
<state state_id="draft" title="Draft">
<exit-transition transition_id="publish"/>
<permission-map name="View" acquired="False">
<permission-role>Editor</permission-role>
<permission-role>Contributor</permission-role>
</permission-map>
</state>
<state state_id="published" title="Published">
<exit-transition transition_id="retract"/>
<permission-map name="View" acquired="False">
<permission-role>Anonymous</permission-role>
<permission-role>Member</permission-role>
</permission-map>
</state>
<!-- Transitions between states, including guard conditions -->
<transition transition_id="publish"
title="Contributor publishes"
new_state="published" trigger="USER"
before_script="" after_script="">
<action url="%(content_url)s/content_status_
modify?workflow_action=publish"
category="workflow">Publish</action>
<guard>
<guard-permission>Publish content</guard-permission>
</guard>
</transition>
<transition transition_id="retract"
title="Contributor retracts"
new_state="draft" trigger="USER"
before_script="" after_script="">
<action url="%(content_url)s/content_status_
modify?workflow_action=retract"
category="workflow">Retract</action>
<guard>
<guard-permission>Retract content</guard-permission>
</guard>
</transition>
</dc-workflow>
First, we define the permissions we're going to manage (View in this case).
Later, we define the different available states, together with their role mapping and available transitions.
Last, we define what each transition should perform. An user must have the indicated guard permission in the context to trigger the associated transition.
9. Properties Reference
Code snippets and info for using the Plone API
9.1. Site Root Properties
Properties managed by the site root object.
Although Plone uses other property managers for configuration, the Plone portal object (site root) is a property manager itself, including a set of important properties for site setup.
They can be set using GenericSetup throught the properties.xml file. Check the CMFPlone/profiles/default/properties.xml file for an example.
To learn how to access properties programatically, see The Zope Book, Appendix B: API Reference.
| Name | Default Value |
Type |
description |
|---|---|---|---|
| title |
string |
The title of the Plone site. This title will be used in the <title> HTML tag. |
|
| description | text |
The description of the site. It will be used to include the description in the HTML header using the tag |
|
| default_page |
front-page | string |
Sets the default page of the Plone site. |
| selectable_views | folder_listing, news_listing |
lines |
The views available to be used as default pages for the Plone site. |
| email_from_address | string |
The email used as the sender for messages sent by Plone, including account activation and password resetting emails. | |
| email_from_name | Site Administrator |
string |
The name included in email messages sent by Plone. |
| validate_email | True | boolean |
Activates email validation for user registration. If enabled, users receive an email upon registration, with a URL to a temporary form, requesting them to enter the new password. |
| email_charset | utf-8 | string |
The charset used when sending email messages. |
| enable_permalink | False | boolean |
A CMF property not used in Plone. |
9.2. Site-wide Properties
Site-wide properties available at the properties tool.
Plone uses the properties tool's sheet site_properties to store some site-wide settings.
They can be set using GenericSetup throught the propertiestool.xml file. Check the CMFPlone/profiles/default/propertiestool.xml file for an example.
| Title | Default Value | Type | Desccription |
|---|---|---|---|
| allowAnonymousViewAbout |
False | boolean | If False, only logged-in users will be able to view information about who created an item and when it was modified. |
| localTimeFormat | %b %d, %Y | string | Format to be used when displaying dates in short notation (e.g. without hours and minutes). It should follow the syntax as specified by Python's time.strftime. To know more, read the Python documentation for time.strftime. |
| localLongTimeFormat | %b %d, %Y %I:%M %p |
string | Similar to localTimeFormat but including more information (e.g. with hours and minutes). |
| default_language | en |
string | The default language for the site's content. |
| default_charset | utf-8 | string | The default charset in which the site's content is encoded. |
| use_folder_tabs | |
lines | List of content types that should include the 'contents' view. Not used in Plone 3. |
| ext_editor | False | boolean | If True, an icon will be made visible on each page, to allow the user to edit content using an external editor. This requires an additional application called ExternalEditor to be installed on the client workstation. |
| available_editors | TinyMCE None |
lines | List of available editors that the users are able to select. |
| allowRolesToAddKeywords | Manager Reviewer |
lines | Roles allowed to add new keywords/categories when editing content. |
| auth_cookie_length | 0 | int | Length of the authorization cookie. |
| calendar_starting_year | 2001 | int | The starting year to show in the calendar widget. |
| calendar_future_years_available | |
int | The number of future years, after the current year, to show in the calendar widget. |
| invalid_ids | actions | lines | Invalid ids for content created in Plone. |
| default_page | index_html index.html index.htm FrontPage |
lines | Attributes or object ids that, if available, should be used as default page. |
| search_results_description_length | int | Length of the description to be included in search results listings. For items with a longer description, text will be cropped. | |
| ellipsis | ... | string | The ellipsis that will be added at the end of the cropped description in search results listings. |
| typesLinkToFolderContentsInFC | Large Plone Folder Folder |
lines | Content types that should link to its 'contents' view, when listed in a 'contents' view. |
| visible_ids | False | boolean | If True, users will be able to set the ids for content items. If False, the ids will be generated automatically based on the normalized version of the title. |
| exposeDCMetaTags | False | boolean | If enabled, Plone will include Dublin Core metadata information at the HTML header. |
| types_not_searched | ATBooleanCriterion ATDateCriteria ... |
lines | List of content types whose instances won't be retrieved by search results. |
| disable_folder_sections | False | boolean |
If True, the automatic site sections (portal tabs) generation will be disabled, so only actions in category |
| disable_nonfolderish_sections | False | boolean | If True, only folderish content items will be listed as site sections. |
| typesUseViewActionInListings | Image File |
lines | List of content types that should link to its 'view' action in content listings. |
| verify_login_name | True | boolean | If True, when login fails, users will get a specific answer, telling their login name field wasn't found or the password was incorrect. If False, users will only be told that the login failed. |
| many_users | False | boolean | If True, the Plone user management screen will be optimized for a large amount of users, allowing only to search instead of listing them all. |
| many_groups | False | boolean | If True, the Plone group management screen will be optimized for a large amount of groups, allowing only to search instead of listing them all. |
| enable_livesearch | True | boolean | When set to True, the global search box will show live results. Only for browsers supporting javascript. |
| default_page_types | Topic | lines | The list of content types that can be used as default pages, so the user is able to selected them using the 'display' menu. |
| use_folder_contents | ... |
lines | Complementary to use_folder_tabs. Not used in Plone 3. |
| forbidden_contenttypes | text/structured text/restructured ... |
lines | List of forbidden mimetypes that are not available in text areas. Mimetypes removed from this list will be available in text areas (e.g. page body). Only if the user isn't using Kupu, since it only supports HTML. |
| default_contenttype | text/html |
string | Default mimetype for text areas. |
| enable_sitemap | False |
boolean | Expose sitemap.xml.gz in the portal root according to the sitemaps.org standard. |
| number_of_days_to_keep | 7 | int | Indicates how many days of undo history should be kept. |
| enable_inline_editing | True | boolean | If True, enables the inline editing feature, allowing users to click directly in the field value and change it without accessing the edit form. Only for browsers supporting Javascript. |
| enable_link_integrity_checks | True | boolean | If True, users will get warnings when they delete or move content that is linked from other content available in the site. |
| webstats_js | |
string | Intended for code snippets from external providers. It will be included in the rendered HTML, as entered, near the end of the page. It can be used to enable web statistics support from external providers (e.g. Google Analytics). |
| external_links_open_new_window | False | string | If True, all external links in the content region will open in a new window. Only for browsers supporting Javascript. |
| icon_visibility | enabled | string | Possible values are 'disabled', 'authenticated' or 'enabled'. If 'disabled', content icons won't be shown in listings; if 'authenticated', content icons will be visible only for authenticated users; if 'enabled' content icons will be visible for all users. |
9.3. Navigation Properties
Navigation properties available at the properties tool.
Plone site-wide navigation properties are stored at property tool's sheet navtree_properties.
They can be set using GenericSetup throught the propertiestool.xml file. Check the CMFPlone/profiles/default/propertiestool.xml file for an example.
| sortAttribute | getObjPositionInParent | string | Catalog index that will be used to sort navigation results. |
| sortOrder | asc | string | Sort order for navigation results. |
| sitemapDepth | 3 | int | Depth of the generated sitemap. |
| name | string | Name for the navigation portlet. Not used in Plone 3, since navigation portlets have their own configuration for each assignment. | |
| root | / | string | Root of the navigation. |
| currentFolderOnlyInNavtree | False | boolean | If True, the navigation tree will only show the current folder and its children at all times. Not used in Plone 3. |
| includeTop | True | boolean | If True, the top (root) node will be included in the navigation tree. This is affected by the topLevel setting. Not used in Plone 3. |
| topLevel | 0 | int | An integer value that specifies the number of folder levels below the site root that must be exceeded before the navigation tree will display. Not used in Plone 3. |
| bottomLevel | 0 | int | Specifies how many folders should be included before the navigation tree stops. 0 means no limit. 1 only includes the root folder. Not used in Plone 3. |
| showAllParents | True | boolean | If True, the navigation tree will display all the parents of the current item. Not used in Plone 3. |
| idsNotToList | lines | Content ids that should be ignored by the navigation. | |
| parentMetaTypesNotToQuery | Temp Folder Large Plone Folder |
lines | Meta types of parents that should be ignored by the navigation. |
| metaTypesNotToList | lines | Meta types that should be ignored by the navigation. | |
| enable_wf_state_filtering | False | boolean | If set to True, the navigation results will be filtered by workflow state. |
| wf_states_to_show | lines | The list of workflow states to be shown in navigation. Works in conjunction with enable_wf_state_filtering. |
9.4. portal_actions reference
How to use the portal_actions tool programmatically
Overview
The portal_actions tool
is an object in the root directory of all Plone sites that provides
access to the various actions in the site. An action usually
manifests itself as link or a menu-item somewhere in the site.Â
For example, the tabs along the top of the Plone site ('Home',
'Products', etc..) are actions that are defined in the portal_actions
tool.
Class
ActionsTool
API Docs
http://api.plone.org/Plone/2.1.1/public/CMFCore.ActionsTool.ActionsTool-class.html for the API docs of this class.
Usage examples:
Obtain actions available to current user:
from Products.CMFCore.utils import getToolByName
actions = getToolByName(context, 'portal_actions').listFilteredActions()
In the above example actions would contain a dictionary of lists of
dictionaries representing the actions available to the current
user. E.g.:
{'site_actions':
[
{'category': 'site_actions',
'available': True,
'title': 'Small Text',
'url': <bound method ActionInfo._getURL of {...}>,
'name': 'Small Text',
'visible': True,
'allowed': True,
'id': 'small_text',
'permissions': ('View',)
},
{'category': 'site_actions',
'available': True,
'title': 'Normal Text',
'url': <bound method ActionInfo._getURL of {...}>,
'name': 'Normal Text',
'visible': True,
'allowed': True,
'id': 'normal_text',
'permissions': ('View',)
},
...
],
...
'portal_tabs':
[
{
'category': 'portal_tabs',
'available': True,
'title': 'Home',
'url': <bound method ActionInfo._getURL of {...}>,
'name': 'Home',
'visible': True,
'allowed': True,
'id': 'index_html',
'permissions': ('View',)
},
...
],
...
}
And given this example, it is trivial to obtain a list of actions in a particular category.
Obtain a list of the actions in a particular category:
from Products.CMFCore.utils import getToolByName
portal_tabs = getToolByName(context, 'portal_actions').listFilteredActions()['portal_tabs']
In the above snippet, portal_tabs would contain a list of
dictionaries representing all of the actions with category
'portal_tabs'. E.g.:
[
{
'category': 'portal_tabs',
'available': True,
'title': 'Home',
'url': <bound method ActionInfo._getURL of {...}>,
'name': 'Home',
'visible': True,
'allowed': True,
'id': 'index_html',
'permissions': ('View',)
},
{
'category': 'portal_tabs',
'available': True,
'title': 'Products',
'url': <bound method ActionInfo._getURL of {...}>,
'name': 'Home',
'visible': True,
'allowed': True,
'id': 'products',
'permissions': ('View',)
},
...
],
Iterating through actions in a TAL template:
<ul>
<tal:actions repeat="action python:context.portal_actions.listFilteredActionsFor(context)['portal_tabs']">
<li>
<a tal:attributes="href action/url; title action/title;" tal:content="action/title">
Action title
</a>
</li>
</tal:actions>
</ul>
The above snippet creates an unordered list of links for the actions with category portal_tabs. Its output would be something like:
- Home
- Products
- About
- ...
Where each item is linked to its associated URL.
10. Internationalization (i18n) and localization (l10n)
How to make your Plone product behave with different languages and cultures
10.1. Introduction
Introduction to various i18n subsystems in Plone
Plone has three different internationalization subsystems
- Translating user interface text strings: zope.i18nmessageid package. This is translation work the developers do. Internally GNU gettext machinery is used.
- Adapting locale specific settings for the site, like time format - plone.i18n package. This is translation work site integrators usually do.
- Managing multilingual content - Products.Linguaplone add-on product. This is translation work site editors usually do.
10.2. Language negotiation
How the current language of the viewed page is defined
Each page view has a language associated with which is negotiated based on various factors. Negotiation is handled by PlacelessTranslationService and plone.i18n modules.
Several factors determine what is the current language:
- Cookies (user chosen setting from the language selector)
- Domain name: Top level domain name (TLD) like plone.fi for Finnish, plone.se for Swedish or subdomain name like fi.plone.org for Finnish.
- The language of the context: what function context.Language() returns. The user can set this on content metadata edit page.
- Language headers send by the web browser
Language is negotiated at the beginning of the page view.
Getting the current language
How to get the active language for the current context and HTTP request
from Acquisition import aq_inner from zope.component import getMultiAdapter context = aq_inner(self.context) portal_state = getMultiAdapter((context, self.request), name=u'plone_portal_state') current_language = portal_state.language()
10.3. Translating text in code
How to use message ids, gettext and context.translate() method
10.3.1. Introduction
Internationalization is a process to make your code locale and language aware. Usually this means supplying translation files for text strings used in the code.
Plone internally uses UNIX standard gettext tool to perform i18n.
Plone has two separate gettext subsystems for historical reasons
- zope.i18n based using gettext standard locales/ folder structure
- PlaceslessTranslationService and i18n/ folder based. Please do not use this for new products.
Both use .po file format to describe translations.
Note that this chapter concerns only code-level translations. Content translations are managed by Products.LinguaPlone add-on product.
10.3.2. zope.i18n package
The basics of zope.i18n and locales/ folders
zope.i18n package is Zope 3 way of doing gettext
- Follows gettext best practices
- Translations are stored in locales folder of your application. Example: i18n/LC_MESSAGES/fi/your.app.po
- Has zope.i18nmessageid package which is string-like class allowing storing translation domain with translatable text strings easily
- .po files must be manually converted to .mo binary files every time the translations are updated. See more about this on i18ndude page.
Plone uses only file path and name to search the translation files. Information about translation domain and language in the .po file headers is ignored.
10.3.3. Marking translatable strings in code
Each module declares its own MessageFactory which is a callable and marks strings with translation domain. MessageFactory is declared at main __init__.py file of your package.
from zope.i18nmessageid import MessageFactory
# your.app.package must match domain declaration in .po files
yourAppMessageFactory = MessageFactory('your.app.package')
You also need to have the following ZCML entry:
<configure
xmlns:i18n="http://namespaces.zope.org/i18n">
<i18n:registerTranslations directory="locales" />
</configure>
After the set-up above you can use message factory to mark strings with translation domains.
i18ndude translation utilities seek underscore _ function to mark translatable strings (gettext message ids). So i18ndude will harvest all text from Python files which is inside _() function, no matter from where the function is actually imported. Message ids must be unicode strings.
from your.app.package import yourAppMessageFactory as _ my_translatable_text = _(u"My text") # my_text is zope.
The object will still look like a string:
>>> my_translatable_text u'Test'
But in reality it is zope.i18nmessageid.message.Message object:
>>> my_translatable_text.__class__ <type 'zope.i18nmessageid.message.Message'> >>> my_translatable_text.domain 'your.app.package'
10.3.4. Automatically translated message ids
When messages are translated automatically and you don't need to call context.translate()
Plone will automatically perform translation for message id objects which are outputted in page templates.
The following code would translate my_translateable_text to the native language activated for the current page.
<span tal:content="view/my_translateable_text">
Note that since my_translateable_text is zope.i18nmessageid.message.Message containing its own gettext domain information, the i18n:domain attribute in page templates does not affect message ids declared through message factories.
10.3.5. Manually translating message ids
If you are outputting text outside page templates you need to make sure it runs through translate() function yourself
If you need to manipulate translated text outside page templates, you need to perform the final translation manually.
Translation always needs context (i.e. under which site translation happens), as the active language and other preferences are read from the HTTP request object and site settings.
Translation can be performed using context.translate() method which all content objects inherit:
# Create a message id which will be translated msgid = _(u"My text") # Use translate() function to get the final text string translated = self.context.translate(msgid) # translated value is now u"Käännetty teksti" (in Finnish)
10.3.6. Message ids in other files than Python source code
Some other file formats support i18ndude directly
There are also other message id markers in code outside Python domain
- ZCML entries have their own mechanism (*.zcml files)
- GenericSetup XML have their own mechanism (profiles/default/*.xml files)
- TAL page templates have their own mechanism (*.pt files)
10.3.7. String substitution in message ids
How to translate text which contains parameters
Translation string substitutions must be used when the final translated message contains variable strings in it.
Plone content classes inherit translate() function which can be used to get the final translated string. It will use the currently activate language. Translation domain will be taken from the msgid object itself, which is string-like zope.i18nmessageid instance.
Message ids are immutable (read-only) objects so you need to always create a new message id if you use different variable substituion mappings.
Python code:
from yourcompany.app import appMessageFactory as _
class SomeView(BrowserView):
def do_stuff(self):
msgid = _(u"search_results_found_msg", default=u"Found ${results} results", mapping={ u"results" : len(self.contents)})
# Use inherited translate() function to get the final text string
translated = self.context.translate(msgid)
# Show the final result count to the user as a portal status message
messages = IStatusMessage(self.request)
messages.addStatusMessage(translated, type="info")
Corresponding .po file entry:
#. Default: "Found ${results} results"
#: ./browser/accommondationsummaryview.py:429
msgid "search_results_found_msg"
msgstr "Löytyi ${results} majoituskohdetta"
For more information, see
10.3.8. PlacessTranslationService
PlacelessTranslationService is another of two Plone translation subsystems
PlacelessTranslationService is
- Historic, being phased out
- Stores .po files in i18n folder of your add-on product
- Used for main "plone" translation gettext domain
- Translation files are processed when Plone is restarted. Example: i18n/yourapp-fi.po.
10.3.9. i18ndude
i18ndude is a set of scripts to manage gettext translations with Zope
i18ndude is developer command-line utility to manage .po and .mo files.
Usually you build our own shell script wrapper around i18ndude to automatize generation of .mo files of your product .po files.
Examples
Setting up folder structure for Finnish and English
Example:
mkdir locales mkdir locales/fi mkdir locales/en mkdir locales/fi/LC_MESSAGES mkdir locales/en/LC_MESSAGES
Creating .pot base file
Example:
i18ndude rebuild-pot --pot locales/mydomain.pot --create your.app.package .
Managing .po files
Example shell script to manage i18n files. Change CATALOGNAME to reflect the actual package of your product:
#!/bin/sh
#
# Shell script to manage .po files.
#
# Run this file in the folder main __init__.py of product
#
# E.g. if your product is yourproduct.name
# you run this file in yourproduct.name/yourproduct/name
#
#
# Copyright 2009 Twinapex Research http://www.twinapex.com
#
# Assume the product name is the current folder name
CURRENT_PATH=`pwd`
CATALOGNAME="yourproduct.app"
# List of languages
LANGUAGES="en fi"
# Create locales folder structure for languages
install -d locales
for lang in $LANGUAGES; do
install -d locales/$lang/LC_MESSAGES
done
# Rebuild .pot
i18ndude rebuild-pot --pot locales/$CATALOGNAME.pot --create $CATALOGNAME .
# Compile po files
for lang in $(find locales -mindepth 1 -maxdepth 1 -type d); do
if test -d $lang/LC_MESSAGES; then
PO=$lang/LC_MESSAGES/${CATALOGNAME}.po
# Create po file if not exists
touch $PO
# Sync po file
echo "Syncing $PO"
i18ndude sync --pot locales/$CATALOGNAME.pot $PO
# Compile .po to .mo
MO=$lang/LC_MESSAGES/${CATALOGNAME}.mo
echo "Compiling $MO"
msgfmt -o $MO $lang/LC_MESSAGES/${CATALOGNAME}.po
fi
done
10.3.10. Other resources
Links to other material related to translating text in Plone code
10.4. Translating and creating multilingual content
How to translate and handle multilingual content programmatically
Content translations are handled by well-established LinguaPlone add-on product.
Each translation is its own Archetypes content object. Translations are linked together through LinguaPlone catalogs. The base text is called canonical and all other translations are linked into this.
The base class for multilingual content is LinguaItem: http://svn.plone.org/svn/plone/LinguaPlone/tags/2.1.1/Products/LinguaPlone/examples/LinguaItem.py
Translating content
LinguaPlone contains some unit test code which shows how to create translations. You can use context.addTranslation(language_code) and context.getTranslation(language_code) methods.
Example:
from Products.LinguaPlone.I18NBaseObject import AlreadyTranslated
try:
object.addTranslation(lang)
except AlreadyTranslated:
# Note: AlreadyTranslated is always risen if Products.Linguaplone is not installed
pass
translated = object.getTranslation(lang)
For more information see
Querying multi-lingual content
By default, LinguaPlone monkey-patches portal_catalog to return only content queried in the current language of the page view. If you need to refer to other language content in portal_catalog queries, you need to pass a special parameter Language="all" to portal_catalog queries.
Example:
for translation_search in self.portal_catalog.searchResults(Language='all')::
pass
10.5. Site language settings
How to change site language settings programmatically
You can set site language in code:
# Setup site langauge settings portal = context.getSite() ltool = portal.portal_languages defaultLanguage = 'en' supportedLanguages = ['en','es'] ltool.manage_setLanguageSettings(defaultLanguage, supportedLanguages, setUseCombinedLanguageCodes=False)
For unit testing, you need to run this in afterSetUp() after setting up the languages:
# THIS IS FOR UNIT TESTING ONLY # Normally called by pretraverse hook, # but must be called manually for the unit tests # Goes only for the current request ltool.setLanguageBindings()
On vanilla Plone site (no LinguaPlone installed) using profiles/default/propertiestool.xml:
<object name="portal_properties" meta_type="Plone Properties Tool">
<object name="site_properties" meta_type="Plone Property Sheet">
<property name="default_language" type="string">en</property>
</object>
</object>
On Linguaplone enabled sites, your add-on product can use GenericSetup XML profiles/default/portal_languages.xml
<?xml version="1.0"?> <object> <default_language value="fi"/> <use_path_negotiation value="False"/> <use_cookie_negotiation value="True"/> <use_request_negotiation value="False"/> <use_cctld_negotiation value="False"/> <use_combined_language_codes value="False"/> <display_flags value="True"/> <start_neutral value="False"/> <supported_langs> <element value="en"/> <element value="fi"/> </supported_langs> </object>
10.6. Language selector
Plone comes with a language selector viewlet which you often want to customize for your theme or your special language configuration
There exist two different language selector viewlet bases for Plone
- Viewlet from vanilla Plone
- Viewlet from LinguaPlone - LinguaPlone has its own language selector which replaces the default Plone selector if LinguaPlone is installed
Source code references
- https://svn.plone.org/svn/plone/plone.app.i18n/trunk/plone/app/i18n/locales/browser/selector.py
- https://svn.plone.org/svn/plone/plone.app.i18n/trunk/plone/app/i18n/locales/browser/languageselector.pt
- http://svn.plone.org/svn/plone/Products.LinguaPlone/tags/2.4/Products/LinguaPlone/browser/selector.py
Making language flags point to different domains
It is often desirable to make the language selector point to a different domains for different languages. For example, search engines get confused by dynamic language switcher and will index switching links, messing up your site search results.
Below is an example where different top level domain links are hard coded to the language selector template:
<tal:language
tal:define="available view/available;
languages view/languages;
showFlags view/showFlags;">
<ul id="portal-languageselector"
tal:condition="python:available and len(languages)>=2">
<tal:language repeat="lang languages">
<li tal:define="code lang/code;
selected lang/selected"
tal:attributes="class python: selected and 'currentLanguage' or '';">
<a href=""
tal:condition="python:code =='fi'"
tal:define="flag lang/flag|nothing;
name lang/name"
tal:attributes="href string:http://www.twinapex.fi;
title name">
<tal:flag condition="python:showFlags and flag">
<img
width="14"
height="11"
alt=""
tal:attributes="src string:${view/portal_url}${flag};
title python: name;
class python: selected and 'currentItem' or '';" />
</tal:flag>
<tal:nonflag condition="python:not showFlags or not flag"
replace="name">language name</tal:nonflag>
</a>
<a href=""
tal:condition="python:code =='en'"
tal:define="flag lang/flag|nothing;
name lang/name"
tal:attributes="href string:http://www.twinapex.com;
title name">
<tal:flag condition="python:showFlags and flag">
<img
width="14"
height="11"
alt=""
tal:attributes="src string:${view/portal_url}${flag};
title python: name;
class python: selected and 'currentItem' or '';" />
</tal:flag>
<tal:nonflag condition="python:not showFlags or not flag"
replace="name">language name</tal:nonflag>
</a>
</li>
</tal:language>
</ul>
</tal:language>
11. Users and Security
How to manage who and how can access to different parts of your site.
11.1. Pluggable Authentication Service
The Pluggable Authentication Service (PAS), which is used by Plone 2.5 and later for user management. This document is aimed towards administrators who need to configure user management in a Plone site and towards developers who are interested in PAS plugins.
11.1.1. Introduction
The Pluggable Authentication Service (PAS) is an alternative to the standard Zope User Folder or the popular Group User Folder (GRUF). PAS has a highly modular design, which is very powerful, but also a lot harder to understand.
PAS is built around the concepts of interfaces and plugins: all possible tasks related to user and group management and authentication are described in separate interfaces. These interfaces are implemented by plugins, which can be selectively enabled per interface.
Plone uses PlonePAS, which extends PAS with a couple of extra plugin types and which adds GRUF compatibility. Since PlonePAS extensions are rarely needed and are subject to change in the next Plone releases this tutorial will only focus on pure PAS features.
11.1.2. Features and interfaces
A user folder such as PAS provides a number of different services: it takes care of user authentication, it asks users to login if needed, it allows you to search for users and groups.
In order to make both configuration and implementation simpler and more powerful all these different tasks have been divided into different interfaces. Each interface describes how a specific feature, such as authenticating a user, has to be implemented.
Within PAS plugins are used to provide those features. Plugins are small pieces of logic which implement one or more functions as defined by these interfaces.
This separation is useful for different reasons:
- it makes it possible to configure different aspects of the system separately. For example how users authenticate (cookies, login forms, etc.) can be configured separately from where user information is stored (ZODB, LDAP, RADIUS, SQL, etc.). This flexibility makes it very easy to tune the system to specific needs.
- it makes it possible for developers to write small pieces of code that only perform a single task. This leads to code that is easier to understand, more testable and better maintainable.
11.1.3. The important interfaces
PAS has a number of interfaces that are important for everyone.
The most important interfaces that you may want to configure are:
- Authentication
- Authentication plugins are responsible for authenticating a set of credentials. Usually that will mean verifying if a login name and password are correct by comparing them with a user record in a database such as the ZODB or an SQL database.
- Extraction
- Extraction plugins determine the credentials for a request. Credentials can take different forms such as a HTTP cookie, HTTP form data or the users IP address.
- Groups
- These plugins determine of which group(s) a user (or group) is a member.
- Properties
- Property plugins manage all properties for users. This includes the standard information such as the user's name and email address but can also be any other piece of data that you want to store for a user. Multiple properties plugins can be used in parallel, making it possible for example to use some information from a central system such as active directory while storing data specific for your Plone site in the ZODB
- User Enumeration
- User enumeration plugins implement the searching logic for users.
11.1.4. Configuring PAS
There is no Plone interface to configure PAS: you will need to use the Zope Management Interface (ZMI). In the ZMI you will see a *acl_users* folder in the site root. This is your PAS.
If you open the acl_users folder you will see a number of different items. Each item is a PAS plugin, which implements some PAS functionality.
There is one special item: the plugins objects manages all administrative bookkeeping within PAS. It remembers which interfaces are active for each plugin and in what order the plugins should be called.
Let's take a look to see how this works. If you open the plugins object you will see a list of all the PAS interfaces, along with a short description of what they do.
We will take a look at the extraction plugins. These plugins take care of extracting the credentials such as your username and password from a request. These credentials can then be used to authenticate the user. If you click on the Extraction Plugins header you will see a screen which shows the plugins which implement this interface and allows you to configure which plugins will be used and in what order.
In the default Plone configuration there are two plugins enabled for this interface:
- the credentials_cookie_auth plugin can extract the login name and password from an HTTP cookie and HTTP form values from the login form or portlet
- the credentials_basic_auth plugin can extract the login name and password from standard HTTP authentication headers.
In the default configuration the cookie plugin takes preference over the basic authentication plugin. This means that credentials from a HTTP cookie will be preferred over credentials form HTTP authentication headers if both are present You can try this by first logging in using standard HTTP authentication in the Zope root and then visiting your Plone site and logging in with a different user there: you will see that the new user is now the active user.
You can change the order of the plugins by clicking on a plugin and moving it up or down with the arrows. Using the left and right arrows you can enable and disable a plugin for this interface.
11.1.5. Configuring an individual PAS plugin
In addition to enabling and disabling plugins via the plugins object each plugin can also have its own configuration. You can access this by opening a plugin in the ZMI.
Taking the credentials_cookie_auth as example again you will see the screen for the Activate tab. This tab is mandatory and allows you to enable and disable PAS interfaces for a plugin. This corresponds to the plugin configuration we saw earlier, but does not allow you to change the ordering of different plugins for an interface. If you enable a new interface for a particular plugin, it will be activated and placed last in the list of plugins for a particular interface.
You can also go to the properties tab to edit settings specific for this plugin:
What you can configure will differ per plugin. Some plugins do not have any configurations options, others can be very complex.
11.1.6. Concepts
PAS has a few basic concepts that you must understand in order to develop PAS related code.
There are a few basic concepts used in PAS:
- credentials
- Credentials are a set of information which can be used to authenticate a user. This can be a login name and password, an IP address, a session cookie or something else.
- user name
- The user name is the name used by the user to log into the system. To avoid confusion between user id and user name this tutorial will use the term login name instead.
- user id
- All users must be uniquely identified by their user id. A users id can be different than the login name.
- principal
- A principal is an identifier for any entity within the authentication system. This can be either a user or a group. This implies that it is not legal to have a user and a group with the same id!
11.1.7. The user object
Contrary to other user folders, a user does not have a single source in a PAS environment. Various aspects of a user (properties, groups, roles, etc.) are managed by different plugins. To accommodate this, PAS features a user object which provides a single interface to all different aspects.
There are two basic user types: a normal user (as defined by the IBasicUser interface) and a user with member properties (defined by the IPropertiedUser interface). Since basic users are not used within Plone we will only consider IPropertiedUser users.
- getId()
- returns the user id. This is a unique identifier for a user.
- getUserName()
- Return the login name used by the user to log into the system.
- getRoles()
- Return the roles assigned to a user "globally".
- getRolesInContext(context)
- Return the roles assigned to the user within a specific context. This includes the global roles as returned by getRoles().
11.1.8. User creation
PAS uses a multi-phase algorithm to create a user object
- An IUserFactoryPlugin plugin is used to create a new user object.
- All IPropertiesPlugin plugins are queried to get the property sheets.
- All IGroupsPlugin plugins are queried to get the groups.
- All IRolesPlugin plugins are queried to get the global roles
11.1.9. User factory plugin
PAS supports multiple user types. PAS contains two default user types: IBasicUser and IPropertiesUser. IBasicUser is a simple user type which supports a user id, login name, roles and domain restrictions. IPropertiedUser extends this type and adds user properties.
A user factory plugin creates a new user instance. PAS will add properties, groups and roles to this instance as part of its user creation process.
If no user factory plugin is able to create a user PAS will fall back to creating a standard PropertiedUser instance.
The IUserFactoryPlugin interface is a simple one containing a single method:
def createUser( user_id, name ):
""" Return a user, if possible.
o Return None to allow another plugin, or the default, to fire.
"""
The default PAS behaviour is demonstrated by this code::
def createUser(self, user_id, name):
return ProperiedUser(user_id, name)
11.1.10. Properties plugins
Properties are stored in property sheets: mapping-like objecst, such as a standard python dictionary, which contain the properties for a principal. The property sheets are ordered: if a property is present in multiple property sheets only the property in the sheet with the highest priority is visible.
Property sheets are created by plugins implementing the IPropertiesPlugin interface. This interface contains only a single method:
def getPropertiesForUser( user, request=None ):
""" user -> {}
o User will implement IPropertiedUser.
o Plugin may scribble on the user, if needed (but must still
return a mapping, even if empty).
o May assign properties based on values in the REQUEST object, if
present
"""
Here is a simple example:
def getPropertiesForUser(self, user, request=None):
return { "email" : user.getId() + "@ourcompany.com" }
this adds an email property to a user which is hardcoded to the user id followed by a companies domain name.
11.1.11. Group plugins
Group plugins return the identifiers for the groups a principal is a member of. Since a principal can be either a user or a group this implies that PAS can support nested group members. The default PAS configuration does not support this though.
Like other PAS interfaces the IGroupsPlugin interface is simple and only specifies a single method:
def getGroupsForPrincipal( principal, request=None ):
""" principal -> ( group_1, ... group_N )
o Return a sequence of group names to which the principal
(either a user or another group) belongs.
o May assign groups based on values in the REQUEST object, if present
"""
Here is a simple example:
def getGroupsForPrincipal(self, principal, request=None):
# Manager can not be itself
if principal=="Manager":
return ()
# Only act on the current user
if getSecurityManager().getUser().getId()!=principal:
return ()
# Only act if the request originates from the local host
if request is not None:
ip=request.get("HTTP_X_FORWARDED_FOR", request.get("REMOTE_ADDR", ""))
if ip!="127.0.0.1":
return ()
return ("Manager",)
This puts the current user in the Manager group if the site is being accessed from the Zope server itself.
11.1.12. Roles plugin
The IRolesPlugin plugins determine the global roles for a principal. Like the other interfaces the IRolesPlugin interface contains only a single method:
def getRolesForPrincipal( principal, request=None ):
""" principal -> ( role_1, ... role_N )
o Return a sequence of role names which the principal has.
o May assign roles based on values in the REQUEST object, if present.
"""
Here is a simple example:
def getRolesForPrincipal(self, principal, request=None):
# Only act on the current user
if getSecurityManager().getUser().getId()!=principal:
return ()
# Only act if the request originates from the local host
if request is not None:
ip=request.get("HTTP_X_FORWARDED_FOR", request.get("REMOTE_ADDR", ""))
if ip!="127.0.0.1":
return ()
return ("Manager",)
This gives the current user in Manager role if the site is being accessed from the Zope server itself.
11.1.13. Authorisation algorithm
These are the steps the PAS user folder follows in its validate method:
- extract all credentials. This looks for any possible form of authentication information in a request: HTTP cookies, HTTP form parameters, HTTP authentication headers, originating IP address, etc. A request can have multiple (or no) sets of credentials.
- for each set of credentials found
- try to authorise the credentials. This checks if the credentials correspond to a known user and are valid.
- create a user instance
- try to authorise the request. If succesful use this user and stop further processing.
- create an anonymous user
- try to authorise the request using the anonymous user. If succesful use this, if not:
- issue a challenge
11.1.14. Credential extraction
Within PAS credentials are a set of information which can identify and authenticate a user. A users login name and password are for example very common credentials. You may also use an HTTP cookie to track users; if you do so the cookie will be your credential.
PAS user credential extraction plugins to find all credentials in a request. Authentication of these credentials is done at a later stage by seperate authentication plugin.
Writing a plugin
If you want to write your own credential extraction plugin it has to implement the IExtractionPlugin interface. This interface only has a single method:
def extractCredentials( request ):
""" request -> {...}
o Return a mapping of any derived credentials.
o Return an empty mapping to indicate that the plugin found no
appropriate credentials.
"""
Here is a simple example:
def extractCredentials(self, request):
login=request.get("login", None)
if login is None:
return {}
password="request.get("password", None)
return { "login" : login, "password" : password }
This plugin extracts the login name and password from fields with the same name in the request object.
11.1.15. Credential authentication
The credentials as returned by the credential extraction plugins only reflect the authentication information provided by the user. These credentials need to be authenticated by an authentication plugin to check if they are correct for a real user.
The IAuthenticationPlugin interface is a simple one:
def authenticateCredentials( credentials ):
""" credentials -> (userid, login)
o 'credentials' will be a mapping, as returned by IExtractionPlugin.
o Return a tuple consisting of user ID (which may be different
from the login name) and login
o If the credentials cannot be authenticated, return None.
"""
Here is a simple example:
def authenticateCredentials(self, credentials):
users={ "hanno" : "hannosch", "martin" : "optilude",
"philipp" : "philiKON" }
if "login" not in credentials or "password" not in credentials:
return None
login=credentials["login"]
password=credentials["password"]
if users.get(login, None)==password:
return (login, login)
return None
This plugin allows the users hanno, martin and philipp to login with their nickname as password.
11.1.16. Challenges
If the current (possibly anonymous) user is not authorised to access a resource Zope asks PAS to challenge the user. Generally this will result in a login form being shown, asking the user with a appropriately priviliged account.
The IChallengeProtocolChooser and IChallengePlugins plugins work together to do this. Since Zope can be accessed via various protocols (browsers, WebDAV, XML-RPC, etc.) PAS first needs to figure out what kind of protocol it is dealing with. This is done by quering all IChallengeProtocolChooser plugins. The default implementation is ChallengeProtocolChooser, which asks all IRequestTypeSniffer plugins to test for specific protocols.
Once the protocol list has been build PAS will look at all active IChallengePlugins plugins.
Writing a plugin
The IChallengePlugin interface is very simple: it only contains one method:
def challenge( request, response ):
""" Assert via the response that credentials will be gathered.
Takes a REQUEST object and a RESPONSE object.
Returns True if it fired, False otherwise.
Two common ways to initiate a challenge:
- Add a 'WWW-Authenticate' header to the response object.
NOTE: add, since the HTTP spec specifically allows for
more than one challenge in a given response.
- Cause the response object to redirect to another URL (a
login form page, for instance)
"""
The plugin can look at the request object to determine what, or if, it needs to do. It can then modify the response object to issue its challenge to the user. For example:
def challenge(self, request, response):
response.redirect("http://www.disney.com/")
return True
this will redirect a user to the Disney homepage every time he tries to access something he is not authorised for.
11.1.17. PAS eats exceptions
A broken user folder is one of the worst things that can happen in Zope: it can make it impossible to access any objects underneath the user folders level.
In order to secure itself against errors in plugins PAS ignores all exceptions of the common exception types: NameError, AttributeError, KeyError, TypeError and ValueError.
This can make debugging plugins hard: an error in a plugin can be silently ignored if its exception is swallowed by PAS.
11.1.18. Plugins
Detail about the stock plugins provided by PAS and how to create new ones
11.1.18.1. Plugin Interfaces
PAS Plugins are broken down by the different functionalities they provide.
11.1.18.1.1. List of Plugin Interfaces
PAS Plugins are broken down by the different functionalities they provide. A particular plugin may provide one or many of the following interfaces
- Extraction Plugins
Extraction plugins are responsible for extracting credentials from the request.
- Authentication Plugins
Authentication plugins are responsible for validating credentials generated by the Extraction Plugin.
- Challenge Plugins
Challenge plugins initiate a challenge to the user to provide credentials.
- Update Credentials Plugins
Credential update plugins respond to the user changing credentials.
- Reset Credentials Plugins
Credential clear plugins respond to a user logging out.
- Userfactory Plugins
Create users.
- Anonymoususerfactory Plugins
Create anonymous users.
- Properties Plugins
Properties plugins generate property sheets for users.
- Groups Plugins
Groups plugins determine the groups to which a user belongs.
- Roles Plugins
Roles plugins determine the global roles which a user has.
- Update Plugins
Update plugins allow the user or the application to update the user's properties.
- Validation Plugins
Validation plugins specify allowable values for user properties (e.g., minimum password length, allowed characters, etc.)
- User_Enumeration Plugins
Enumeration plugins allow querying users by ID, and searching for users who match particular criteria.
- User_Adder Plugins
User Adder plugins allow the Pluggable Auth Service to create users.
- Group_Enumeration Plugins
Enumeration plugins allow querying groups by ID.
- Role_Enumeration Plugins
Enumeration plugins allow querying roles by ID.
- Role_Assigner Plugins
Role Assigner plugins allow the Pluggable Auth Service to assign
11.1.18.2. Plugin Types
A list of the different types of plugins
11.1.18.2.1. Extraction Plugins
Extraction plugins are responsible for extracting credentials from the request.
Stock Plugins
The following stock plugins provide the IExtractionPlugin Interface.
Cookie Auth Helper
This plugin helps manage the details of Cookie Authentication. Allows you to extract credentials from a cookie, update them, reset them, etc.
HTTP Basic Auth Helper
Multi-plugin for managing details of HTTP Basic Authentication. Extracts credentials from request and implements the HTTP Auth challenge.
Inline Auth Helper
Manages credentials for inline authentication.
Session Auth Helper
Extracts and manages credentials for session authentication.
Methods
Each plugin implements the following methods:
- extractCredentials() -- gets credential info from the relevant request, cookie, session, etc.
- updateCredentials() -- responds to a change of credentials
- resetCredentials() -- empties out currently stored values
if appropriate, the plugin will also implement a challenge() method which will challenge the user for authentication.
11.1.18.2.2. Authentication Plugins
Authentication plugins are responsible for validating credentials generated by the Extraction Plugin.
Stock Plugins
Delegating Multi Plugin
This plugin delegates a PAS interface to some other acl_user folder, typically a "legacy" folder that implements some specific authentication functionality. For example, you can delegate the IAuthenticationPlugin interface to a legacy user folder via a Delegating Multi Plugin.
Domain Auth Helper
Authenticates users based on their IP address. Has nothing to do with Windows "Domain" Authentication.ZODB User Manager
ZODB-based user storage. Does authentication, enumeration and properties for users and stores its data in the ZODB.11.1.18.2.3. Challenge Plugins
Challenge plugins initiate a challenge to the user to provide credentials.
Stock Plugins
Cookie Auth Helper
This plugin helps manage the details of Cookie Authentication. Allows you to extract credentials from a cookie, update them, reset them, etc.
HTTP Basic Auth Helper
Multi-plugin for managing details of HTTP Basic Authentication. Extracts credentials from request and implements the HTTP Auth challenge.
Inline Auth Helper
Manages credentials for inline authentication.
11.1.18.2.4. Update Credentials Plugins
Credential update plugins respond to the user changing credentials.
Stock Plugins
Cookie Auth Helper
This plugin helps manage the details of Cookie Authentication. Allows you to extract credentials from a cookie, update them, reset them, etc.
Delegating Multi Plugin
This plugin delegates a PAS interface to some other acl_user folder, typically a "legacy" folder that implements some specific authentication functionality. For example, you can delegate the IAuthenticationPlugin interface to a legacy user folder via a Delegating Multi Plugin.
Inline Auth Helper
Manages credentials for inline authentication.
Session Auth Helper
Extracts and manages credentials for session authentication.
11.1.18.2.5. Reset Credentials Plugins
Credential clear plugins respond to a user logging out.
Stock Plugins
Cookie Auth Helper
This plugin helps manage the details of Cookie Authentication. Allows you to extract credentials from a cookie, update them, reset them, etc.
Delegating Multi Plugin
This plugin delegates a PAS interface to some other acl_user folder, typically a "legacy" folder that implements some specific authentication functionality. For example, you can delegate the IAuthenticationPlugin interface to a legacy user folder via a Delegating Multi Plugin.
HTTP Basic Auth Helper
Multi-plugin for managing details of HTTP Basic Authentication. Extracts credentials from request and implements the HTTP Auth challenge.
Inline Auth Helper
Manages credentials for inline authentication.
Session Auth Helper
Extracts and manages credentials for session authentication.
11.1.18.2.6. Properties Plugins
Properties plugins generate property sheets for users.
Stock Plugins
Delegating Multi Plugin
This plugin delegates a PAS interface to some other acl_user folder, typically a "legacy" folder that implements some specific authentication functionality. For example, you can delegate the IAuthenticationPlugin interface to a legacy user folder via a Delegating Multi Plugin.
11.1.18.2.7. Groups Plugins
Groups plugins determine the groups to which a user belongs.
Stock Plugins
Dynamic Groups Plugin
This plugin allows you to create dynamic groups via business rules.
Recursive Groups Plugin
This plugin will recursively flatten a collection of groups.
ZODB Group Manager
This plugin lets you manage groups and groups of groups in the ZODB.
11.1.18.2.8. Roles Plugins
Roles plugins determine the global roles which a user has.
Stock Plugins
Delegating Multi Plugin
This plugin delegates a PAS interface to some other acl_user folder, typically a "legacy" folder that implements some specific authentication functionality. For example, you can delegate the IAuthenticationPlugin interface to a legacy user folder via a Delegating Multi Plugin.
Domain Auth Helper
Authenticates users based on their IP address. Has nothing to do with Windows "Domain" Authentication.
ZODB Role Manager
Stores role information for users in the ZODB. Handles roles storage, role enumeration, and role assignment.
11.1.18.2.9. User_Enumeration Plugins
Enumeration plugins allow querying users by ID, and searching for users who match particular criteria.
Stock Plugins
Delegating Multi Plugin
This plugin delegates a PAS interface to some other acl_user folder, typically a "legacy" folder that implements some specific authentication functionality. For example, you can delegate the IAuthenticationPlugin interface to a legacy user folder via a Delegating Multi Plugin.
Search Principals Plugin
Plugin to delegate enumerateUsers and enumerateGroups requests to another PluggableAuthService
ZODB User Manager
ZODB-based user storage. Does authentication, enumeration and properties for users and stores its data in the ZODB.
11.1.18.2.10. User_Adder Plugins
User Adder plugins allow the Pluggable Auth Service to create users.
Stock Plugins
ZODB User Manager
ZODB-based user storage. Does authentication, enumeration and properties for users and stores its data in the ZODB.
11.1.18.2.11. Group_Enumeration Plugins
Enumeration plugins allow querying groups by ID.
Stock Plugins
Dynamic Groups Plugin
This plugin allows you to create dynamic groups via business rules.
Search Principals Plugin
Plugin to delegate enumerateUsers and enumerateGroups requests to another PluggableAuthService
ZODB Group Manager
This plugin lets you manage groups and groups of groups in the ZODB.
11.1.18.2.12. Role_Enumeration Plugins
Enumeration plugins allow querying roles by ID.
Stock Plugins
ZODB Role Manager
Stores role information for users in the ZODB. Handles roles storage, role enumeration, and role assignment.
11.1.18.2.13. Role_Assigner Plugins
Role Assigner plugins allow the Pluggable Auth Service to assign
Stock Plugins
ZODB Role Manager
Stores role information for users in the ZODB. Handles roles storage, role enumeration, and role assignment.
12. Client-Side Functionality & JavaScript
12.1. Including JavaScript
When in doubt, use portal_javascripts
There are currently an unfortunately large number of ways to inject JavaScript link tags and direct code into the rendering pipeline for Plone pages. Many or most of these are likely candidates for deprecation during the 4.x series. The most "future-proof" method is to use portal_javascripts. While it may change, there will be a straightforward, probably automatic, migration path.
12.2. Included JavaScript Libraries
Several third-party libraries are included with the Plone core
12.2.1. jQuery
jQuery has been part of Plone since 3.1, and should be used for DOM selection, traversal and manipulation; event binding and handling; and AJAX.
Using jQuery
Plone components should not depend on the "$" alias for jQuery. Also, the "jq" alias is deprecated.
If you wish to use the "$" alias in your code, there are a couple of common conventions that will allow you to do so without making global changes:
(function($) {
/* some code that uses $ */
}(jQuery));
or, make use of the fact that jQuery passes itself to the document ready handler:
jQuery( function($) {
/* some code that uses $ */
});
12.2.2. jQuery Tools
jQuery Tools became part of Plone with 4.0 (and maybe added to Plone 3.x via plone.app.jquerytools).
Use jQuery Tools for tabs, tooltips, overlays, masking and scrollables.
Masking, in which most of the page is shaded in order to highlight the part of the page that most needs attention, should be minimized â though we do use masking for popup forms in order to emphasize their modal character.
12.2.3. jQuery Form Plugin
The jQuery Form Plugin became part of Plone in Plone 4.0, and is provided via plone.app.jquerytools.
Use it for AJAX form input marshaling and submission. Note that jQueryâs built-in form input marshaling is inadequate for many forms as it does not handle submit-button or file-input data.
12.2.4. Other Libraries
If you need to include a new third-party library with Ploneâs core, that inclusion must be PLIPâd and approved by the framework team. Licensing must be GPL 2 compatible.
12.3. Popup Overlays & Forms
plone.app.jquerytools provides a âprepOverlayâ plugin that makes it easy to create popup overlays to display images or AJAX-loaded content from other pages. It also handles AJAX submission of forms in popups.
The prepOverlay plugin is well-documented at http://pypi.python.org/pypi/plone.app.jquerytools. Many usage examples are available in Products/CMFPlone/skins/plone_ecmascripts/popupforms.js, which provides the setup for Plone 4âs standard popup forms.
12.4. JavaScript Standards
Community standards for use in JavaScript components.
12.4.1. Recommendations for JavaScript Components
JavaScript components should follow the principles of progressive enhancement and unobtrusive JavaScript.
Progressive Enhancement
Pages presented in Plone, including forms and form widgets, must be fully usable in situations where JavaScript is not available. The availability of JavaScript on the browser side should enhance the presentation of the page and its content. Additionally, the structured document delivered via HTML/XHTML should be semantically correct and complete in meaning.
The best way to accomplish these goals is to: first. compose complete and useful content in HTML; second, style its presentation with CSS; and third, make use of JavaScript behaviors to enhance presentation and interaction.
Unobtrusive JavaScript
JavaScript should nearly never be present in the content area of a page. Typically, it will only appear via link and script elements in the head of the document (or at its very end when that improved rendering).
In particular, HTML tags should nearly never have event-handler (e.g., onclick or onsubmit) tag attributes or JavaScript in URLs. Coding JavaScript into HTML tags generally makes for code that is hard to maintain and nearly impossible to test.
Instead of coding event handlers in HTML attributes, use jQueryâs âbindâ and its various convenience aliases like âclickâ methods to attach event handlers to elements. Use âliveâ if installing behaviors that need to operate in AJAX-loaded HTML.
12.4.2. Coding Standards
JSLint
All JavaScript components that are incorporated into the Plone core must pass JSLint code quality tests. At some point in the 4.x series, this will become part of Ploneâs continuous integration testing.
JSLint has many options, and it is our goal that our code pass âThe Good Partsâ tests. A couple of acceptable deviations from the good parts settings are to:
- Assume a browser /*jslint browser: true */;
- Relax white-space requirements (removing âwhite: trueâ) to allow for idiomatic composition of jQuery cascades.
- Assume availability of the globals jQuery, browser, window and location.
You may set these options by including at the top of your JavaScript file:
/*jslint white:false, onevar:true, undef:true, nomen:true, eqeqeq:true, plusplus:true, bitwise:true, regexp:true, newcap:true, immed:true, strict:false, browser:true */ /*global jQuery:false, document:false, window:false, location:false */
These settings are available as a file in Products/CMFPlone/skins/plone_ecmascript/js-standards.js. If you use those settings, you only need set the options for any deviations needed by the current file. Deviations like turning off the regular-expression "." prohibition, are perfectly reasonable when porting old code, but should be avoided in new code.
A common way to execute a command-line jslint test using these options would be to execute:
cat js-standards.js accessibility.js | jslint
if you were testing the âaccessibility.jsâ file. JSLint is also available as a plugin for most popular web code editors and can be set to test on save. An example of setting up TextMate to run jslint on save:
Strict Mode
Do not include the âuse strictâ directive unless you are testing in an environment that will enforce the strict standard. Otherwise, your code may fail when execu
ted on future browsers.
Globals
JavaScript components should create as few as possible global variables. If a component must create globals, it should only create one: a namespace object with a very distinctive name. Please document your new global at the top of the component file.
12.4.3. Rendering Tests
If you add a behavior that is intended to be generally useful, you should add an example/test usage to the rendering test document, available at: Products/CMFPlone/skins/plone_templates/test_rendering.pt. This document provides usage examples as well as rendering tests.
12.5. AJAX & Errors
AJAX work inevitably introduces error-handling problems. The recommended solution for this class of problems is to reverse the progressive-enhancement process. Since AJAX functionality should be an enhancing supplement, it should nearly always be possible to solve AJAX error problems by falling back on un-enhanced behavior. Note that the un-enhanced behavior may well be an error page.
This is more an error-handling philosophy than a recipe, and may in particular not be applicable in a multi-step process.
An example: it's common to use AJAX in forms to validate on a field-by-field basis. However, all such forms should also be handling form errors without JavaScript by returning error messages after feedback. Rather than introducing complex error-handling for AJAX failures during live form validation, it's simpler and more robust to fall back to server validation of the entire form.
12.6. Animations
The general purpose of JavaScript animations in Plone is to provide a visual cue to the user when a part of the page is changed. (Instantly changing the page may leave the user confused as to what has changed.) Developers should resist the temptation to deploy new animations. Stick with the animations provided by jQuery and jQuery Tools. Animations should last no longer than necessary to signal changes.
12.7. Platform Testing
JavaScript for Plone core must be tested on:
- IE 7, 8, 9
- Firefox, current release and most-recent beta or rc for the next release, if available
- Webkit browsers: Current
In general, degrade gracefully to no-js behavior when a platform cannot be supported. You need not support IE 6, but if you know that a feature works poorly on IE 6, simply turn it off:
if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) < 7) {
return;
}
12.8. Messages & Translation
JavaScript components should include as few messages as possible. Whenever possible, the messages you display via JavaScript should be drawn from the page.
If thatâs not possible, it is your responsibility to assure that the messages you need are translatable.
Our current mechanism for doing that is to include the messages via Products/CMFPlone/browser/jsvariables.py. This will nearly certainly be changed.
12.9. Testing
As of this writing, the Plone community has not settled on standards for JavaScript unit and integration testing. QUnit is (as of this writing), the most common unit-testing mechanism. Selenium and Windmill are both in use for unit testing.
Plone 4.2 is expected to include strong recommendations for both unit and integration testing, and these will be supported by our continuous integration testing.
12.10. KSS
KSS was Plones standard mechanism for binding behaviors to document nodes, but it is being gradually replaced with jQuery. As of Plone 4.1, KSS is deprecated. Please do not create new KSS-dependent functionality. Make plans to replace existing functionality.
















