Plone Core Developer Reference
This reference manual describes the conventions, concepts and components of the core Plone codebase. It is intended as a point of reference for new developers who want to be able to contribute, and for old developers who are doing things they haven't done before. At the moment, it is a work in progress, sections will be expanded as we are able to find time. This manual is of course useful to anybody doing development with Plone, but will focus on documenting the areas important to development of Plone itself.
1. Overview
A high level overview of the Plone codebase
1.1. Audience
Who is this manual for?
This reference manual is intended to give an overview of the processes and conventions of the development of Plone, both as a point of reference for existing developers, and a source of explanation for new developers.
It is not intended as a general guide for how to develop applications on top of Plone, but some of the "best practice" documentation here may be useful to third party developers as well.
This document is a work in progress, and will probably continue to be so for a good while. We will endeavour to publish pages and sections when they are complete, but don't be surprised if you find that there are major pieces of the puzzle missing at present.
1.2. Contributing
How you can contribute to Plone
Like all open source products, Plone only moves forward on the contributions of volunteers. If you would like to contribute, we will be incredibly happy!
However, there are many people and companies who rely on Plone day-to-day, so we have to introduce some degree of quality control over the code base. Plone's source code is hosted in an open Subversion respository on http://svn.plone.org, but only members of the developer team have commit-rights. Before you can get commit rights, three things need to happen:
- You must familiarise yourself a little with the community. Get on the chatroom, start asking and (especially) answering questions on the mailing lists, and get to know the more active developers a bit. If you arrive in the #plone chatroom and ask how you can help, chances are you will be met with gratitude and openness, so don't be afraid to introduce yourself. If no-one answers, it's probably just a bad time of the day: try the developers' list instead.
- You must prove that your code does not suck. There are two types of people in the world - those who can write code, and those who cannot. Actually, we take that back - there are also UI people. And accessibility experts. And translators. And documentation writers. And testers. However you wish to contribute, though, we'd like to see the calibre of what you produce. See below for ways of contributing without commit-access to the source code repository.
- You must sign the contributor agreement. This offers some intellectual copyright protection, and ensures that the Plone Foundation is able to exercise some control over the codebase to ensure it is not appropriated for someone's unethical purposes. Think IBM vs. SCO.
Now, there are a number of other ways you can help out, which do not require contributor agreements or anything else than the right attitude.
Testing
We always need more testers. If you believe you have found a bug in Plone, particularly if you are using a less common operating system, have a very large site, or operate in non-English locales, please report it in the bug collector. Please search the tracker thoroughly before submitting a bug, so that we avoid duplicates.
Before submitting a bug, please log in with your plone.org username. If you don't have an account, just click the "join" link in the blue bar at the top on plone.org. You can submit bugs without being logged in, but when you are logged in, you will be given email notification when your bug is responded to. This is important if we need clarification, for example - you can see a request for clarification, and then add a follow-up with more details.
Please think carefully before submitting a bug. If the bug is really in a third party product, do not use the Plone tracker, as we won't be able to help you - contact the authors directly, instead, or use their tracker in the products area. If it is a Plone bug, search for similar issues and add a follow-up to an existing bug if necessary - we will be notified of this just as clearly as with new bugs.
If you have to submit a new bug, try to write as clearly and concisely as you can to ensure that we understand your problem. Include relevant product versions, and always enter your Plone and Zope versions. Be honest about the severity - submitting a bug as "critical" if it is not is unlikely to win you any sympathy.
Patches and verification
Very often, issues end up in the collector for a long time because we don't know whether there was actually a problem, or if the submitter was experiencing a local configuration issue. If a bug looks sketchy, it's very helpful if you can try to replicate the problem on your own setup and add a follow-up about your experiences. If you have more insight to offer than original submitter, this is of course incredibly useful!
If you think you know how to fix a bug, this is your chance to shine. Once logged in, you can submit patches to the tracker. Even if you can't provide a patch, but can do some research and come up with a way in which the issue may be resolved easily, this will help the developers immensely. Adding patches to the collector is one of the best ways of showing off your skills and proving that you should be given commit-access to the Plone repository.
Documentation
Any member of plone.org (that includes you, if you want to be) can submit documentation to the Plone documentation area. If you have discovered something that you feel was poorly documented, you will gain much praise by writing a short How-to or a Tutorial on the subject. Once you are finished, submit your document for review, and we will publish it if it is not giving bad advice or too difficult to understand. You will probably find that writing documentation also helps you structure your own understanding of Plone, and forces you to consider issues you may have otherwise glossed over.
If you need help with writing documentation, or you're not sure where to start, send a mail to the plone-docs list/newsgroup
Helping others
The best way to gain respect in the community is by helping others. If you spend some time in the #plone chatroom or on the Plone users' mailing list answering people's questions when you know the answer, people will take notice. Helping others will help your own understanding, not to mention the warm fuzzy feeling you'll get afterwards.
1.3. Release process
This documents tries to explain our release cycle and how we discuss and decide about feature additions.
General
Plone uses a time-based release process, which helps to produce releases of predictable size and complexity. In order to make this work there are some requirements on how new features get accepted and implemented.
Special roles
There are some special roles in the Plone community of which the release manager and the framework team are the two important ones for the release process. For every major release series a release manager is elected by the Plone foundation board who has the authority to make certain final decisions for a release. The framework team is a group of people who assist the release manager in the process of reviewing any suggested features.
Release phases
The normal development cycle of a Plone release consists of various phases. See the Plone roadmap for concrete dates for the next upcoming releases.
- Discussion and Planning
- People suggest new features and discuss them. We use a lightweight formal process which helps to foster and document this. See the below paragraph on enhancing Plone for the details.
- Proposal freeze date
- For each release there is a certain date by which all new feature proposals must have been submitted for review. After this date no features will be accepted anymore for this particular release.
- Alpha releases
- We will release various snapshots of the current release bundle that we package up to give out for testing. Migration to/from specific alphas is not supported.
- Feature freeze date / Beta releases
- By this date all features must have been completed and all code has to be integrated. Only non-invasive changes, user interface work and bug fixes are done now. Migrations supported to/from specific betas.
- Release Candidate
- Ideally, the last beta that was bug free. No changes to the code. Should not require any migration steps apart from the ones required in the betas. If any problems are found and fixed, a new release candidate is issued.
- Final release / Expected release date
- Normally the last release candidate that was issued without any show-stopper bugs.
- Bug fix releases
- No software is perfect. Once a sufficient large or critical number of bugs have been found for a certain release, the release manager releases a new bug fix release a.k.a. third-dot release (for example 2.1.2).
Enhancing Plone
Work out the idea
If you have any idea on how to make Plone even better as it is today, you should first talk with other people about your ideas. Both the IRC channel and the developers mailing list are good starting points for this. Talking to people about it will give you some pointers about the feasibility of the feature, and give you a good starting point for further work.
If your change or improvement is a big one, then you'll be asked to write a PLIP - Plone Improvement Proposal. In the PLIP you'll explain exactly what it is you want to do, how to do it and how it will integrate. To add a PLIP, you need to be on the development team, so you can add it as an Improvement Proposal in the Plone Roadmap area.
When writing a PLIP, be as specific and to-the-point as you can. Remember your audience - to get support for your proposal, people will have to be able to read it! A good PLIP is sufficiently clear for a knowlegable Plone user to be able to understand the proposed changes, and sufficiently detailed for the release manager and other developers to understand the full impact the proposal would have on the codebase. You don't have to list every line of code that needs to be changed, but you should also give an indication that you have some idea that how the change can be feasibly implemented.
If your change is minor then a ticket in the tracker will be sufficient, added as an enhancement. The key point here is that each change needs documentation so other users can see what it is. This can be in the form of an issue tracker entry, or a PLIP in the case of a bigger change. A bug or minor change does normally not need to go through a review process - a PLIP does.
When you've done your PLIP and you're happy with it, announce it on the framework team mailing list . People will come and read the proposal and give feedback (hopefully positive).
At this point there is one key decision made by the developers:
- your PLIP will be adopted into Plone
- your PLIP will not be adopted
The latter isn't bad, it's just that this is not appropriate to be in the Plone core and have all the Plone team support. Each PLIP that gets accepted places a burden on the team, so people are selective in this process. There are many great products Plone uses that aren't directly part of Plone.
Start working on it
You can start the development at any time - but if you are going to modify Plone itself, you might want to wait to see if your idea is approved first to save yourself some work if it isn't. Your code has to be in an open accessible code versioning repository and you have to make a release bundle which allows the framework team to easily review your work. Please signify the release bundle on your PLIP. For a technical explanation see the "Using bundles for code review" paragraph of the "Version Control" page of this reference manual.
Once you've done developing it let the framework team know. At this point we'll do some things:
- review it and check you've done what you said
- see which release it will be accepted into
These are the criterias by which the framework team will review your review bundle:
- Does the PLIP itself need work?
- i.e. is this idea well baked and expressed clearly
- Does the work proposed belong in Plone now, in the future?
- Does it do the needful for Plone?
- Is it more appropriate as a qualified add-on?
- What condition is the review bundle in:
- tests
- sane implementation
- clearly coded
- uses current idioms of development
- documentation / doc-tests
Note that if you don't finish your implementation in time for the feature freeze, it will not be included in the next Plone release. You can always submit your PLIP for the next release.
Finishing your work
Once you have finished your work and it was accepted by the framework team and the release manager, you will be asked to merge your work into the main development line. Merging the PLIP in is not the hardest part, but you must think about it when you develop. You'll have to interact with a large number of people to get it all set up. The merge may cause problems with other PLIP's coming in. During the merge phase you must be prepared to help out with all the features and bugs that arise.
If all went as planned the next Plone release will carry on with your PLIP in it. You'll be expected to help out with that feature after it's been released within reason, and we look forward to the next feature.
1.4. Special events
From time to time we organize certain special events to move Plone forward. This page describes the most common ones: Bug days, Sprints and Symposiums/Conferences.
Overview
Part of our development process are some special events that are organized from time to time. The main idea behind these is to get people together to work on Plone at the same time, both as it is more efficient as you can get immediate feedback and as it is certainly more fun to work with others.
What is a Bug Day?
From time to time, especially in the weeks leading up to a release, the Plone Community arranges so-called "Bug Days". These days focus on identifying and fixing bugs and other issues with the Plone core, and is an excellent chance to get to know both Plone and its developers better. We therefor get together on IRC and collectively fix a selected set of bugs.
The Bug-Wrangler is the person who determines what bugs needs to be fixed and prioritizes as well as allocates bugs to each individual developer. Bug Day's happen in many other open source projects. Anyone can participate in a bug day; general users who are comfortable with using Subversion can test that a developer has fixed a bug or that the user can not provoke the bug in a different manner. And no matter what your current skill level is - from totally new to Plone to experienced Plone developer - you can make a difference!
Bug days are a critical development and social event that brings the developers closer through producively exorcising software of evil bugs. Bug days are usually announced on the developer mailing list and on the frontpage of plone.org.
What is a Sprint?
Tres Seaver originally came up with this idea. While a bug day lasts only one day or a weekend and people usually meet online, a Sprint typically lasts for several days to a full week and people meet in person. The idea of a Sprint is for a group of people to enhance, create, or fix one or more pieces of infrastructure. Usually they are focused on a specific set of topics instead of the entire product line.
Sprints are either funded by organizations or individuals who need specific features or are interested in working on some. Sprints happen yearly in the Plone community from sunny San Francisco to the top of mountains in the Alps where the only electricity is produced by a generator and a satellite uplink is used for internet connectivity. Sprints are extremly productive cultural events in the world of Plone. You can have a look at the list of past and upcoming Sprints.
What is a Symposium/Conference?
This is not a special kind of event for Plone or the open source world, but exactly what happens in other businesses as well. Usually there is one official Plone conference per year and at least one regional Symosium. Most of the time there is a business and a development orientated track each consisting of a series of talks and tutorials. See the events section for past and upcoming events.
1.5. The role of Zope 3
Plone is at a crossroads between Zope 2 and Zope 3. We take a pragmatic approach to Zope 3, but it is important to be aware of the new technologies that are enabled there, and how we may best make use of them in Plone.
Zope 3 is the new, cooler, faster, sexier version of Zope. Unfortunately, there is no direct migration path from Zope 2 to Zope 3. However, we are beginning to use Zope 3 technologies in Plone, alongside the Zope 2 core.
Zope 3 is a fairly different way of programming. It is based on the principle of components, that expose their functionality through well-defined interfaces. Whereas in Zope 2, you would need to mix classes into the inheritance hierarchy to gain certain functionality, for example by mixing in CopySupport to get cut/copy/paste support, Zope 3 uses the principle of adapters.
An adapter is simply a piece of glue code that can adapt an object to a given interface, so that the original object itself doesn't need to know anything about that interface or what it does. This makes it easier to re-use other Python objects that are not Zope aware, and makes components smaller, more precisely defined and thus easier to re-use.
Other types of Zope 3 components include utilities - stateless, global objects that can be looked up through a generic registry, and views, a special type of adapter that is typically used to contain the logic of page templates in a way that is more performant and easier to test.
Porting Plone wholesale to Zope 3 would essentially involve a re-write, and would break every installation, third party product and customisation out there. It is simply not an option. However, because Zope 3 is built out of small, re-usable components, we can use certain Zope 3 technologies today. Starting with Zope 2.8, the whole of Zope 3 ships as an add-on library and is available to use for Zope 2 applications like Plone. Over time, more and more parts of Zope 2 will get re-written to use Zope 3 technologies. This is accomplished via a product called Five (what's Zope 2 + 3?) that performs some magic to make the fundamental Zope 3 building blocks usable in Zope 2.
Zope 3 integration begun with the Plone 2.5 release cycle, and will continue gradually over time. That means that developers will have to start learning Zope 3 concepts. At the moment, there is still some overlap between what can be done with "old" techniques and what can be done in Zope 3. There are also dependencies on what happens in CMF and Zope 2 in general before certain things become available.
However, the basic mantra starting with Plone 2.5 is:
- New components should use Zope 3 technologies whenever possible and appropriate
- Any re-factoring of existing code should use Zope 3 technologies where possible and appropriate
This means that page templates with complex logic should use views. Any new component should make proper use of interfaces and adapters, and existing components should get interfaces and use adapters if at all possible.
It is important to realise that this is a gradual process, and there are many pragmatic decisions to be made. In general, you should always consult the developer list, the release manager or other members of the development team on non-trivial design decisions.
You can learn more about Zope 3 from one of the published books, from Philipp von Weitershausen's WorldCookery web site, from this tutorial or from the rest of documentation area.
1.6. Other resources
Other places you may want to go for help
This guide may not (and probably will not) give you all the details you need in order to resolve any confusion about Plone's internals. Luckily, there are several other places you can go.
- The rest of the documentation should be your first point of call.
- The Plone Developer list is a prime source of help. Please only post to this list with topics regarding the development of Plone itself, not third-party product development or customisation.
- Similarly, the #plone IRC chatroom is where most real-time discussion about Plone takes place. People with operator status are typically Plone developers.
- If you are contributing to Plone, you should make a point to both read the developer list regularly, and be online in #plone whenever you can, especially while you are working on Plone.
2. Conventions and professional practice
Plone conventions and professional practice - learn these!
2.1. Package naming conventions
How to name Python packages that contain plain-Python or Zope 3 components and thus do not need to live in the 'Products' directory.
Going forward, there is consensus that we should use Zope 3 style
programming practices whenever possible. One part of that is that things
that don't need to be Zope 2 products shouldn't live in in
$INSTANCE_HOME/Products/ - instead, they should be simple Python
packages, living in $INSTANCE_HOME/lib/python or somewhere else on the
PYTHONPATH that Zope is given when it starts up.
In doing so, code that is part of Plone core should adhere to the following conventions
Generic components without specific Plone dependencies live
directly under the top-level plone namespace
A good example is the the Zope3-style plone.i18n.
It is desirable to factor out generic interfaces and code into such packages whenever possible, to foster re-use.
Packages should have as few dependencies as possible. Thus, if plain-Python will do, that is better than plain-Zope 3, which is better than code that depends on specific facets of Zope 2.
Extensions of such general components (or separate components)
that provide a tighter integration with Plone-the-application should
live in the secondary namespace plone.app
See for example plone.app.i18n.
Components that need to be Zope 2 products will most likely continue
to live in $INSTANCE_HOME/Products still, at least for now. If possible
(which it may not be), however, you should try to not depend on
components being Zope 2 products.
Packages in the svn repository should follow modern Python guidelines and provide the necessary information to be packagable as eggs
The easiest way of achieving this is to use the ZopeSkel paste deploy script, as follows.
- Download ez_setup.py from PEAK
Runez_setup.py in Python:$ python ez_setup.py
This will install the easy_install program, normally to the Place
where the Python binary is found. Look in the log messages of the
installation script to see where they land. For more information, see
the documentation
- Install ZopeSkel:
$ easy_install ZopeSkel==dev
This will install Paste Deploy and the paster script in the same
location as easy_install (again, watch the terminal output) and some
Zope skeletons to use with this tool. To see them, run:
$ paster create --list-templates
- To create a basic Plone package, run:
$ paster create -t plone
It will ask you a number of questions and then generate the basic
package layout. When asked for a project name, use the dotted name of
the package, e.g. plone.i18n.
To create a plone.app package instead, run:
$ paster create -t plone_app
When ready, import the package to the Plone svn repository.
To learn more about setuptools, see its documentation. In particular, see the documentation on development-mode
2.2. Style
Preferred Plone coding style
Python, like any programming language, can be written in a number of styles. We're the first to admit that Zope and Plone are not the finest examples of stylistic integrity, but that doesn't stop us from trying!
First of all, you need to read PEP 8 - the python style guide. You might also have a look at the Zope codying style guidelines but these are not a mandatory reading.
Naming conventions
- Above all else, be consistent with any code your are modifying!
- Write method names and variable names in mixedCase
- Write class names in CapitalisedCase
File conventions
- If you need to create a new file, chances are you're doing something fairly fundamental. In that case, you should probably discuss it with the release manager or someone else more experienced first.
- In Zope 2, file names used to be MixedCase. In Zope 3, and thus in Plone going forward, we prefer all-lowercase filenames. This has the advantage that you can instantly see if you refer to a module / file or a class:
from zope.pagetemplate.pagetemplate import PageTemplate
compare that to:
from Products.PageTemplates.PageTemplate import PageTemplate
- Filenames should be short and descriptive. Think about how an import would read:
from Products.CMFPlone.utils import safe_hasattr
compare that to:
from Products.CMFPlone.PloneUtilities import safe_hasattr
the former is obviously much easier to read, less redundant and generally more aesthetically pleasing.
Some concrete rules
Certain things should never be seen in code. Some of these are covered in more detail in the patterns section, but briefly:
- Do not use tabs in Python code! Use spaces as indenting, 4 spaces for each level. Maybe you didn't hear that - do not use tabs and keep indentation to 4, that's four, spaces.
- Indent properly, even in HTML. (limi's note: especially in HTML ;)
- Never use a bare except. Anything like
except: passwill get you in real trouble - Avoid tal:on-error, since this swallows exceptions
- Don't use hasattr() - this swallows exceptions, use
CMFPlone.utils.base_hasattrorCMFPlone.utils.safe_hasattrinstead!The problem with swallowed exceptions is not just poor error reporting. This can also mask ConflictErrors, which indicate that something has gone wront at the ZODB level!
- Never, ever put any HTML in Python code and return it as a string
- Do not raise string exceptions - raise proper exception objects instead
- Do not acquire tools e.g. using
context.plone_utils. Instead, use:from Products.CMFCore.utils import getToolByName plone_utils = getToolByName(context, 'plone_utils')
(it's OK to acquire tools in unit tests)
- Do not put too much logic in ZPT (use Views instead!)
- Remember to add i18n tags in ZPTs and Python code
2.3. Version control
Plone uses Subversion for its version control. You must know the basics of using this tool to be able to contribute to Plone.
It is important that you familiarise yourself with subversion before committing to the Plone source repository, to reduce the risk of accidental screw-ups. The Subversion book is a comprehensive, but surprisingly accessible resource.
Setting up your environment
There is a list of non-command line clients which you might find easier to use than the standard command line client. If you are a developer from the Windows platform, we highly recommend the excellent TortoiseSVN product instead of the "standard" command line client.
Note that we are following the same policy as svn.zope.org regarding setup for line endings, please read SVN Configuration for line endings
Getting the source code
Development versions of Plone are managed in Subversion bundles. A bundle is simply a set of top-level modules that must be checked out. In general, you can check out a bundle into the Products/ directory of your Zope instance, run any external scripts needed to get resources not available in svn (such as getExternalEditor.sh), and start Zope. The bundles are found in https://svn.plone.org/svn/plone/bundles/.
At the time of writing, the 2.5 bundle contains the current development version of Plone. You can check this out by doing:
svn co https://svn.plone.org/svn/plone/bundles/2.5 ./getExternalEditor.sh
Commits
You should make one change per commit. If you are fixing three bugs, make three commits. That way, it is easier to see what was done when, and easier to roll back any changes if necessary. Also, large, monolithic commits are very difficult to read. Yes - people do read the checkin list to see what happens to their favourite CMS! If you want to make large changes cleaning up whitespace or renaming variables, it is especially important to do so in a separate commit.
It is a cardinal sin to commit any change without properly testing it with unit tests, as well as in the browser (see next page). Please review your code for silly mistakes, lingering pdb statements, debugging print statements or any other junk that shouldn't be there.
ALWAYS run all the Plone unit tests before commiting (see next page). There is NO exception to this rule. If you submit code that breaks a test, your change will almost certainly be reverted with a terse note to plone-dev. If you are at all in doubt, please also run the ATContentTypes and Archetypes unit tests, at the very least.
Before commiting any bugfix or feature enhancement, you must also add a note in Plone's HISTORY.txt. Follow the convention in that file, and note your name in brackets. This file is used to clarify what new features have been added when a new release is made, and should be an accurate account of the changes you have made.
Checkin messages
It is vital that you write proper checkin messages. A checkin message should inform the reader what was done, and why. If a bug was closed, the bug number should be included, e.g.:
Added the plone_deprecated skin layer back into migration. Fixes #1234.
The "Fixes #1234" will actually cause bug #1234 to be marked as closed in the bug tracker automatically.
Using branches for non-trivial changes
If you are doing anything non-trivial, changes should go on a branch! See also the page on the PLIP process. A svn branch is simply a copy of the existing development version that you can work on undisturbed. When your work is finished, tested and approved, the branch can be merged back.
It is important that you name your branch so that people can easily see who is working on it, and what it is for. For example, to branch CMFPlone trunk to work on refactoring foobar, I could write:
svn up # update to the latest revision beforehand
svn info # find out which revision you are branching from
svn cp https://svn.plone.org/svn/plone/CMFPlone/trunk \
https://svn.plone.org/svn/plone/CMFPlone/branches/optilude-2.5-foobar-refactoring
The checkin message for this operation must be informative, and must include the revision number of the branch you started from:
Branched Plone trunk r1234 to begin work on foobar refactoring.
Switching to an existing branch, for example if you want to test someone else's code, is also very simple:
cd CMFPlone./ svn switch https://svn.plone.org/svn/plone/CMFPlone/branches/optilude-2.5-foobar-refactoring
Never create a branch directly from your working directory. Copy it from the repostitory instead: "svn cp original_url branch_url". If you have changes in your working copy that sould be included in the new branch switch to it and commit (after checking for conflicts).
When a branch is ready to be merged, notify the release manager. If you are merging yourself, it is important that you include which revisions you merged in your checkin message:
Merged optilude-2.5-foobar-refactoring branch r1234:1245 into trunk.
Note the format of the range of revisions (r1234:1245). Please follow this format exactly, as it will make the job of the release manager much easier should there be a problem.
Please do not rename or move branches unless absolutely necessary - Subversion is not very good at handling this. Again, include the revision numbers of what you renamed or moved in the checkin message if you must do this. The reason for this is that the only way to find out where to branch from is to use svn log --stop-on-copy, which will reveal which revision the branch was originally copied from. When you rename or move a branch, this command will stop on the revision where the rename or move happened.
Using bundles for code review
When you are sponsoring a PLIP or wish to demonstrate some new feature, you should make life easier for the framework team, the release manager and the general public by using review bundles. A review bundle should work as the main development bundles do: Another developer should be able to check it out into the Products/ folder of a fresh Zope instance and see exactly what changes have been implemented, immediately.
To create a bundle, first create the necessary branches on the modules you are changing, and set up a sandbox that contains only the relevant checkouts. If the code is a moving target, packages are bundled at the revision that the developer wants reviewed.
All bundles must include:
- A README.txt in the root including any caveats or explanation necessary.
- A COMMENTS.txt for developers to record comments
- TODO.txt for recording things that need to be done
- The bundle must compile and run upon checkout!
A bundle is managed using a file in the root of the bundle, conventionally called EXTERNALS.txt. You should use the EXTERNALS.txt of your development bundle checkout as a starting point. Then, to create a new bundle, you do:
svn mkdir https://svn.plone.org/svn/plone/review/plip1234-my-crazy-feature-bundle svn co https://svn.plone.org/svn/plone/review/plip1234-my-crazy-feature-bundle my-bundle cd my-bundle svn propget svn:externals > EXTERNALS.txt # edit the file, changing any svn paths to point to the relevant branches svn propset svn:externals -F EXTERNALS.txt . svn commit
You can now test this by doing:
mkdir bundle-test cd bundle-test svn co https://svn.plone.org/svn/plone/review/plip1234-my-crazy-feature-bundle
This should include all the necessary products to start a Zope site demonstrating your new feature.
2.4. Unit testing Plone
Every feature, bugfix and other part of Plone should be properly unit tested, and all unit tests should be run and made to pass before each commit. Read on for more.
Plone is a large and complex system. Were you to blindly change some feature, you would probably break another one in ways you could never have imagined. Therefore, good unit testing is vital to the survival of Plone. Every feature needs a set of unit tests for base cases and edge cases.
Don't look at unit testing as some necessary evil: Unit testing actually makes testing fun! Ideally, you should write tests before you implement your code or make changes. Your mission is then to make the tests pass. This is generally much easier and more enjoyable than fiddling with things in UI. Unit tests also save you time in the long run, because you are less likely to let obscure bugs go unnoticed, or break code which you thought worked with seemingly "harmless" changes.
Please refer to the unit testing tutorial for the basics of unit testing. This is required reading!
Plone unit tests live in CMFPlone/tests. You should most definitely read through some of the existing test files and familiarise yourself with how unit tests are written.
Fixing bugs
When you fix a bug in Plone, you must write at least one unit test to prove that it's there! That test should fail the first time you run it. Then fix the bug and run the test again. This time, the test must pass. Be sure to test Plone in the browser too, and run all the unit tests before commiting your bug fix.
Adding features
When you add a new feature, it is especially important that you write unit tests up front. If you need to write migrations (more on that later) you will need to test both the migration methods themselves, and the state the migration would bring the portal to.
If you are touching code that appears to have poor test coverage, it is your duty to expand the test coverage. Just because the guy before you was negligent doesn't mean you have a license to be. Plone's test coverage is improving all the time, but could be better. If anything, writing tests for code you are interacting with will reduce the chance that you'll get blamed for someone else's mistakes.
When to change a test
Sometimes you may be tempted to change a test. Be prepared to defend yourself! If you change a test, you are invalidating the unit test runs of every single person who's run the test suite with that test before you. Occasionally, tests are wrong - sometimes they test the wrong thing, sometimes they never can fail (which means the person who wrote the test to begin with was bad and wrote the test after the code - remember, all tests start with a failure!). If you do feel you should change a test, make sure your checkin message is clear on why you changed it, and discuss it with the release manager if you are at all in doubt.
3. Plone patterns and best practice
Sometimes, there is a right way to do things. This section deals with how to approach Plone development with performance, usability, testability and localisability in mind.
3.1. Performance
Some general guidelines about coding for performance
Here's a little fact we should admit up front: Plone is slow. Not as slow as it used to be, not as slow that it's useless, but neither Zope nor Plone have ever been sold on speed. In general, Plone is a content production system, not a content delivery system, and has been written for features and ease-of-use — not for blazing speed. (We use caching to improve speed, or separate delivery platforms altogether.)
This has less to do with the design of the software (or the capabilities of the people involved) than with the trade-offs that have been made for usability and flexibility. However, it is important to think about performance when writing code for Plone core.
Profiling
To find performance bottlenecks, you need to use a profiler. The PTProfiler is very useful for finding out which expressions in the page templates are using up the most CPU time. An alternative profiler is the Call profiler. See also Dieter Mauer's Zope Profiler.
Waking up objects
If there is one axiomatic law of performance in Zope and Plone it is this: Do not wake up objects unless you need to. "Waking up" objects refers to pulling them out of the ZODB - traversing to them, loading them into memory. This is an expensive operation. The old navigation tree in Plone 2.0 used to wake up each and every object it displayed. Sometimes it could wake up each object in a BTreeFolder (aka "Large Folder") with 1,000 children, even if it only rendered a single one of them. This was bad.
In general, we prefer to use catalog queries to find objects. This has been possible since the introduction of ExtendedPathIndex in Plone 2.1. The getFolderContents script uses a catalog query. The navigation tree uses a catalog query. The portlets all use catalog queries.
Essentially, if you find yourself using objectValues() or contentValues() you need to think long and hard about whether you should be using a catalog query instead. The following pattern is the most common use of a path search:
portal_catalog = getToolByName(self, 'portal_catalog')
results = portal_catalog.searchResults(path = {'query' : '/'.join(self.getPhysicalPath()),
'depth' : 1})
for brain in results:
...
Catalog queries themselves are not the fastest to set up, so if you are expecting only one or two items, traversal may be better. For the breadcrumbs in Plone 2.5, for example, we use traversal up the acquisition parent chain (by using aq_parent, which returns full objects) instead of a catalog query, because the parent objects are quite likely to be in the ZODB cache already, and because setting up a catalog query for what is most likely going to be 1-5 results is sub-optimal.
Actions
CMF actions are used to drive things like the tabs in the editable border, the site actions (the sitemap, etc.), the personal bar and many other parts of the Plone UI. Unfortunately, actions are also slow, because each may be associated with a TALES expression that is used to determine whether the action should be displayed or not. Each expression requires an expression context to be set up, the expression to be executed, and the result to be evaluated. This is not particularly efficient.
Actions are still used for many of the dynamic parts of the Plone UI, but if you are using them, you should be aware of the performance implications. At the very least, avoid complex TALES expressions if possible.
Avoiding 404s
A 404, or a NotFoundException in Zope, is an expensive operation. 404s can occur unnoticed when a page template references and image or stylesheet that does not exist. Be careful about referencing items that may or may not exist. You can code defensively by doing something like:
<tal:block condition="exists:some-name">
...
</tal:block>
If you are in doubt, go to the error_log and remove NotFoundException from the list of ignored execptions. Re-load your page and watch out for new entries in the error log.
Making pages cacheable
One of the ways in which real-world installations make Plone faster is by using cacheing, for example via CacheFu. It is important that we make pages and elements of pages as easy to cache as possible. Most of the time, this simply boils down to avoiding unnecessarily dynamic elements.
Take the sitemap, for example. Constructing this is an expensive operation, since the sitemap view needs to execute query for all objects matching the current navigation filter settings. It then needs to render these using a recursive page template. Notice that the action pointing to the sitemap is ${portal_url}/sitemap.
At first sight, this is sub-optimal, because the sitemap can't know the context the user was in before clicking the link, and thus can't put a "you are here" marker in the sitemap. But if the path were ${object_url}/sitemap, then the sitemap on /foo would be different from the sitemap at /bar. Any cache for /foo/sitemap would be completely separate from /bar/sitemap even though 98% of the information on those two pages would be the same. Worse, when Google comes to index the site it will sprider a /sitemap for each and every page, which may bring your site to a grinding halt from executing too many queries.
This principle applies to other things too, such as the author page (which is linked to from all the by-lines), and most commonly image src attributes for common icons.
Cache with v attributes
Sometimes you can't avoid executing an expensive operation more than once during a request. In this case, you may want to cache the result. Zope provides a mechanism for simple caching called "volatile" attributes, or _v_ attributes.
Any non-transient object (that is, any object that lives in the ZODB - note that view classes are not included in this group) can store a value in a v attribute. A v attribute may or may not exist at any given point in time! It will only be valid whilst the object is cached in memory, and there is no guarantee it will stay in memory even during a single request cycle. However, it most likely will be, so it pays off to use it.
You need to code defensively whilst using v attributes:
from Products.CMFPlone.utils import base_hasattr
...
if not base_hasattr(self, '_v_cachedValue'):
self._v_cachedValue = self.calculateExpensiveValue()
return self._v_cachedValue
Download size
Nobody likes having an 800k download to get a few lines of text. Plone's UI is by necessity heavy, and there are many stylesheets, images and HTML snippets included when you download even the lightest of Plone pages. The least you can do is to not make this problem any worse. Think carefully about what you are including, especially if it is going to be included on every page. Keep whitespace and superfluous comments to a minimum, and reduce the size of images as much as you can.
3.2. Usability
Or... how not to destroy Plone's user-friendliness
Plone differentiates itself on usability. The intuitiveness of the user interface is what attracts people to Plone the most.
There are many subtle principles of usability that won't be discussed here, but there is one golden rule: be consistent. Do not introduce new UI metaphors without a damned good reason. Re-use existing HTML and CSS patterns. Make sure things look consistent.
Also, you must think about the purpose of a UI element and avoid overloading it. The classic example is that people use portlets for static UI elements. Remember that portlets can be removed and re-ordered by users, so depending on them for functionality is not a very good idea.
Similarly, the personal bar (the blue one with my folder, preferences etc.) is used for "personal" items, related to the user, not for site-wide actions. The site actions are used for ... site-wide actions like the sitemap and the site setup, and so on.
Usability often comes down to taste and finesse. If in doubt, ask on the developer list, do your best, and don't be offended if limi changes your carefully crafted HTML.
3.3. Testability
How to write code that is testable
As explained many times before, every feature, bug fix and re-factoring needs to be backed up with appropriate tests. Sometimes, it is necessary to write code in such a way that it can be tested. Normally, however, making code testable also forces you to think about how that code is modularised and what interfaces other code should be using to talk to your code. If your methods are predictable in a test, they will be predictable to calling code as well. Display logic is most easily tested by moving it from page templates to views.
And remember: the easiest way of making sure that code is testable is to write the tests first!
3.4. Localisability
Yes, we know it's not really a word, but Plone needs to be localised to a lot of different languages and locales. Think people will only be using your code in English, and will only read text on your page left-to-right? Think again!
Plone prides itself on being multi-lingual, multi-national and multi-cultural. The whole user interface is translatable, and supports right-to-left languages such as Hebrew or Farsi.
The most common interaction you will have with internationalisation (i18n for short) is through page templates. Please read the developers guide to internationalisation, but in general, be mindful that any user-visible string will need a translation key, and that this key will need to be unique within its domain.
Internationalisation of user-visible strings may also be an issue if you are returning a string from a script or method. For example, you may set a status message in a form controller script. In that case, you will need to invoke the translation machinery directly:
from Products.CMFPlone import PloneMessageFactory as _
def myMethod(self):
value = self.getValue()
return _(u'My name is ${fullname}', mapping=${u'fullname' : value})
Note the use of unicode strings (the u prefix) - all strings that form part of the UI should use unicode. Also note that this assumes the message will be processed by a page template (e.g. via a tal:replace or tal:content statement).
If you need to translate something that is not going to be processed by a page template, you will need to invoke the translation service directly:
from Products.CMFCore.utils import getToolByName
...
translation_service = getToolByName(self, 'translation_service')
value = u'John Doo'
return translation_service.utranslate('plone',
u'My name is ${fullname}',
mapping={u'fullname' : value})
In versions of Plone prior to 2.5, there used to be a translate python script. Please do not use this anymore.
3.5. Views
Views are a Zope 3 technology for making display components. They are used for certain things in Plone, but because views do not provide all the features users have gotten used to, they must be approached with a little bit of background knowledge.
Page templates promise to separate logic from presentation. Unfortunately, on their own they only deliver half of that promise. How many times have you made a page template that has a very complex python: expression and thought "I should move this out of the page template"? The options until Zope 3 have been:
- Make ever more complex
python:expressions in the page template and look the other way when people ask who the hell put that in there. Unfortunately, such expressions are difficult to maintain and debug, nearly impossible to test, and expensive to execute. - Make a new Script (Python) in a skins folder such as plone_scripts. Unfortunately, skin scripts are hard to debug, hard to test, quite slow to execute and litter a global namespace.
- Put the code in a content type class that is the context of the script. This is quite common in Archetypes, for example. The problem is that this bloats the interface to that class with quite display-specific code, making the type and (particularly) the page template more difficult to re-use.
- Put the code in the
plone_utilstool (PloneTool.py). This has made PloneTool an incredibly bloated set of losely related functions that may or may not be useful to more than one or two templates.
In Zope 3, this is solved with a view. A view is basically just a multi-adapter that is looked up on the context object (usually a content object) and the request (so that the view can be different for HTTP requests that for FTP requests, say). In pure Zope 3, views are used to represent objects to XML-RPC or FTP and typically comprise a Python class implementing a particular interface and a page template. Views are registered in ZCML under a given name and referred to by @@name (the @@ looks like a pair of eyes peering out, if it helps you remember the symbol).
In Plone 2.5, we used to use a custom base class in Products.CMFPlone.utils.BrowserView and a related utility method in Products.CMFPlone.utils called context() to deal with acquisition problems in views. However, this is now frowned-upon, because it introduced a hard dependency on CMFPlone and slightly deviates from the agreed IBrowserView interface.
Instead, views tend to be used like this in Plone 3:
from Acquisition import aq_inner
from Products.Five.browser import BrowserView
class MyView(BrowserView):
"""My new view
"""
def some_function(self):
context = aq_inner(self.context)
...
Notice the use aq_inner(). This ensures that the acqusition chain of the context does not include the view as a parent. There are numerous examples of browser views in the plone.app.* packages.
View tips and tricks
- View can (and should) be unit tested! They are just simple python classes, nothing else.
- Views are transient - they are never stored in the ZODB, and never acquisition-wrapped.
- Views are created fresh for each request cycle. Therefore, they cannot cache values directly between requests, nor can they rely on any form of persistence.
- Sometimes it's necessary to call a view method more than once. If that method does some expensive calculation, you should cache the results of the operation. To make this easier, a cache decorator exists in
plone.memoize. Using it is pretty simple:from plone.memoize.instance import memoize class MyView(BrowserView): ... @memoize def someExpensiveOperation(self): x = 10 for x in range(10000): x = x ** x return x
3.6. Adapters
Adapters are the Zope 3 way of making re-usable components, or of re-using existing components. It is important that you make proper use of interfaces and the component architecture in order to make your code more re-usable and more loosely coupled.
Consider a Python library that is not aware of Zope. To use this in Zope 2, one would have to mix in a large number of classes just to get the basic Zope object support, making this a very difficult task. In Zope 3, you use adapters to provide the glue. You start by defining an interface that explains how you want other Zope components to talk to the library. Then you provide an adapter that can adapt an object from the non-Zope library to that interface. The Component Architecture provides the registry that makes it easy to look up the appropriate adapter for any given context to any given interface (providing one exists).
As it turns out, adapters are also very useful in creating loosely-coupled components. An adapter is registered from a given interface, to a given interface. Because interfaces can subclass each other, you can have a general adapter for a very general interface, and a more specific adapter for a more specific interface. Similarly, you can provide a very different adapter for one interface than another. Thus, you could make one adapter that makes any code expecting the IMyFunctionality interface able to talk to any object implementing IMyContentType, and a very different adapter that works on ISomeOtherContentType.
By factoring functionality out into separate interfaces implemented with separate adapters, you create slimmer, more re-usable components that are easier to understand and test. Adapters do not necessarily have to "extend" the functionality of an object in a conventional sense. Consider the following example:
Navigation tree needs to consider a large number of options, set in portal_properties. The code that builds the navigation tree performs one complex, but well-defined task: taking a catalog query and turning it into a tree of nodes that can be displayed in the navtree. The old navigation tree code used to mix these tasks together, which meant that the code was impossible to re-use. If you wanted a navtree-like structure (such as the table of contents in this reference manual), you had to write it all again!
For Plone 2.1.3, this was rewritten to use a more generic method that externalised the navtree preferences into a more generic "strategy" object. This let other objects re-use the basic algorithm using different strategies. However, the navtree strategy for the actual navigation tree was still hard-coded into Plone, so if you wanted slightly different strategies in a different context, say, you couldn't do that.
In Plone 2.5, the strategy objects were defined with the following interface in 'CMFPlone.browser.interfaces':
class INavtreeStrategy(Interface):
rootPath = Attribute("The path to the root of the navtree (None means use portal root)")
showAllParents = Attribute("Whether or not to show all parents of the current context always")
def nodeFilter(node):
"""Return True or False to determine whether to include the given node
in the tree. Nodes are dicts with at least one key - 'item', the
catalog brain of the object the node represents.
"""
def subtreeFilter(node):
"""Return True or False to determine whether to expand the given
(folderish) node
"""
def decoratorFactory(node):
"""Inject any additional keys in the node that are needed and return
the new node.
"""
The basic implementations for these are defined in CMFPlone.browser.navtree, e.g.:
class SitemapNavtreeStrategy(NavtreeStrategyBase):
"""The navtree building strategy used by the sitemap, based on
navtree_properties
"""
implements(INavtreeStrategy)
#adapts(*, ISiteMap)
def __init__(self, context, view=None):
...
def nodeFilter(self, node):
...
def subtreeFilter(self, node):
...
def decoratorFactory(self, node):
...
ISiteMap is the interface that is used on the view that builds the sitemap, in CMFPlone.browser.navigation. The adapts line is commented out - in later versions of Zope it will be the easiest way of specifying which object this adapter class adapts from (the implements line tells you what it adapts to), but for now we need to wire this up in ZCML. In CMFPlone/browser/configure.zcml we have:
<adapter for="*
.interfaces.ISiteMap"
factory=".navtree.SitemapNavtreeStrategy"
provides=".interfaces.INavtreeStrategy" />
Note the newline - the adapter is registered for * (any interface) and ISiteMap. Since it's registered for more than one interface, it's a multi-adapter. The factory attribute specifies the class that is used to create the adapter, and the provides attribute tells the component architecture that this provider should be loaded when someone is looking for an INavtreeStrategy.
Also note the class paths starting with a .. This means they are relative to the directory that configure.zcml is in, CMFPlone.browser in this case. You could also use a full path, e.g. Products.CMFPlone.browswer.interfaces.INavtreeStrategy, but this makes it more difficult to refactor the package layout and is harder to read.
The sitemap creation code in CMFPlone.browser.navigation adapts the context (i.e. the content object being viewed) and the view class itself to an INavtreeStrategy in order to find out which strategy to use:
from zope.component import getMultiAdapter ... strategy = getMultiAdapter((context, self), INavtreeStrategy)
In fact, if you were using a single-context adapter, you could just instantiate it like so:
from interfaces import IFoobar ... foobar = IFoobar(context)
Here you are instantiating the interface as if it were an actual class (it kind of is, but that's beside the point). The component architecture knows to find the appropriate adapter factory by figuring out what interfaces context implements and finding the most specific adapter from one of these interfaces to IFoobar.
Note that the factory (e.g. the __init__ method) for the IFoobar adapter must take exactly one argument - the context (i.e. the object being adapted from), for a single-item adapter. For multi-adapters, it must take one argument for each interface specified in the for attribute in the adapter directive.
Now, let's say you wanted to use a different strategy when the sitemap was being viewed on IMyContentType. All you would need to do is to register a more specific adapter:
<adapter for=".interfaces.IMyContentType
.interfaces.ISiteMap"
factory=".mycontent.MyContentTypeNavtreeStrategy"
provides=".interfaces.INavtreeStrategy" />
The various Zope 3 resources cover adapters in much greater detail, but it is important to be familiar with adapters and understand the pattern they are trying to solve. Using adapters means talking to interfaces. If you talk to interfaces and not to classes directly, you not only ensure that your interface adequately covers the uses of your code, you also make it possible to substitute different implementations in different scenarios that you never dreamt of. Similarly, by using adapters to glue components together, you can avoid making "fat" interfaces that make components harder to re-use and understand.
3.7. Future proofing
How to stay in line with Zope 3 concepts and other "future-proofing" techniques
As Plone progresses towards Zope 3, there are some general principles you should keep in mind to make sure your code is "future proof" and won't need to be refactored in the near future. This list may grow, but briefly:
- Look around you! A lot of problems have been solved in CMF, in Zope 3, or by other Python or JavaScript libraries. Before you start inventing something new that the community will have to maintain, consider carefully whether you can re-use something else.
- Declare interfaces for new (or existing) code. In general, any new component should use a well thought-out interface. A class should be registered as a factory for an adapter, and any code using that class should use the interface directly, instantiating the class through the component architecture rather than use the class constructor directly. This will make code much easier to refactor and much easier to override.
- Similarly, where existing tools and objects have interfaces (the CMF tools all have interfaces starting with CMF 1.6, for example), instantiate the objects through the component architecture, not directly from the classes.
- Don't add new Script (Python)'s. In general, any view logic should be in a Zope 3 view. Generic functionality should use adapters or utilities.
- We only ever use DTML for email handling (because it's better preserving whitespace than page templates), and even there we keep it to a minimum. DTML is hard to debug and maintain, and is most definitely frowned upon.
- Use Zope 3 naming conventions for filenames and package layout.
3.8. Debugging
Tips on how to debug Plone
Whilst working on Plone, there will surely be times when you need to debug your own code, or someone else's code. Debugging Plone core is no different from debugging a third party producy, but there are some general tips you should bear in mind:
- pdb is your friend. Put
import pdb; pdb.set_trace()in your code, restart Zope, reload the page and use the debugger in your terminal. Learn how the debugger works and how to make the most of it. - Putting pdb sessions inside unit tests is often the most predictable way of entering a debugging session
- If it helps, put print statements in your code and watch the output in the terminal, or put debug elements in a page template with
tal:content. - If you need to debug a TTW Python Script, use
context.plone_log("message")and watch your event.log - If it makes your life easier, put some debug code deep inside CMF or Archetypes or even Zope itself. You can always remove the file, run an
svn upand get back to the previous state. - To debug visual problems, use the Mozilla DOM Inspector and the Web Developer and Firebug extensions to Firefox. These will save you hours of trial-and-error.
- To debug JavaScript, install the Venkman Debugger for Mozilla.
- Always remove any debug statements and run all tests before checkin! Checking in pdb statements is truly embarrassing.
4. General Plone concepts
Fundamental concepts used in Plone
4.1. Migration and portal creation
How to write and test Plone migrations, and how migrations are used during portal creation
4.1.1. Portal creation and migration concepts
What happens when the portal is created, and how does Plone handle migrations between versions?
Since Plone is a constantly evolving system, there is frequently a need to change the initial state of the system. However, to allow people to upgrade between releases, we cannot simply change the portal creation method. Instead, the portal_migration tool is used to provide incremental changes to the base installation.
Prior to version 2.5 and the introduction of GenericSetup, when a new Plone Site was created, it would begin at a base version (e.g. Plone 2.0) and then run the migration steps for each release until the most current release. This was done to ensure that migrated and freshly created sites were always in sync.
With the introduction of GenericSetup, the state of the Plone site is persisted to XML files found in CMFPlone/profiles/default. When a new Plone site is created (see CMFPlone/factory.py) this profile is loaded using the portal_setup tool. However, for users with an older version installed, migrations are run from their current installation to the most recent version.
Thus, writing migrations is vital when you change Plone in such a way that the base assumptions about what is available change. For example, should you need to add a new tool, this must be created during a migration step to ensure that it is always available. Similarly, when changing actions or default values for properties, adding new properties in portal_properties, adding a depedency on a new product, or changing anything else that becomes part of the "base configuration" in Plone, migrations are needed.
In fact, this means that you must make two changes: One to the XML files that set up the site, the other to the migration steps that update existing sites. This is expected to change over time, as GenericSetup becomes even more generic and usable for upgrades, but for now, migrations are necessary.
Luckily, changing the base profile XML is quite simple and self-explanatory. Consider the following, which sets up a few properties in 'portal_properties/navtree_properties':
<object name="portal_properties" meta_type="Plone Properties Tool"
xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<object name="navtree_properties" meta_type="Plone Property Sheet">
<property name="title">NavigationTree properties</property>
<property name="sortAttribute"
type="string">getObjPositionInParent</property>
...
Adding a new property to this propertysheet is as simple as adding a new <property> element to that file. Remember to write a test first, though!
Migrations are a bit tricker, because during upgrades you general cannot make any assumptions about the system. For example:
- Tools which are available in a standard Plone installation may have been deleted
- Types which ship with Plone may have been disabled or deleted
- Users may have changed actions, properties etc. - in most cases, they would expect their changes to persist after an upgrade, but in some cases, overriding users customisations may be necessary
- Templates, scripts and other skin elements may have been customised, and the skin layers re-ordered in such a way that an acquired script or template may not be the exact version you expect
- Content-space objects with ids of skin elements may mess with acquisition
- Users may run migrations several times, for example if one migration step (hopefully not yours!) fails the first time around. Generally, migrations should not break (or break the site!) if this happens.
Hence, you have to be very careful when writing migrations. The next page describes how to write and unit test migrations. Not properly writing and testing migrations is a sure-fire way of getting your contributions rejected, so please read this carefully.
4.1.2. Writing migrations
How to write migrations
Migrations take the form of global functions, collected in files created for each version to which migration would be expected. At the time of writing, Plone is at 2.1 alpha 2, and so new migrations should go in CMFPlone/migrations/v2_1/betas.py. Make sure that you select the latest appropriate file when adding new migrations.
In this file, you will notice a method:
def alpha2_beta1(portal):
"""2.1-alpha2 -> 2.1-beta1
"""
out = []
#Make object paste action work with all default pages.
fixObjectPasteActionForDefaultPages(portal, out)
return out
This shows the alpha2-to-beta1 migration method with one migration. At the time of writing, this is the latest migration method, but there will be later ones added shortly, so make sure you use the right one!
The method fixObjectPasteActionForDefaultPages is defined later in the same file. By convention, all migration methods take two arguments, portal - the root portal object, and out - a list to which string trace messages should be appended.
You should make a point to look at existing migrations before writing a new one. As mentioned on the previous page, you cannot make any assumptions about an installed system - people do delete tools and replace them with their own versions (for example CMFMember replaces MembershipTool), customise templates and scripts and generally try as hard as they can to make the life of a poor migration writer hard.
General migration advice
- Unit test carefully (see next page)
- Never assume tools and objects are there.
Always use the third default argument to getToolByName and getattr, for example: portal_membership = getToolByName(portal, "portal_membership", None) or child = getattr(parent, "childId", None). Then, make sure that you test the returned value:
if portal_membership is not None:
...
Notice the explicit test for is not None - the shortcut if portal_membership: is not sufficient.
- Always err on the side of caution - make sure your migration tool is safe to run twice, safe to run when none of your assumptions hold, and will never, ever destroy a user's site!
4.1.3. Testing migrations
How to write unit tests for migrations and portal creation
The easiest way to test your migration is to run a forced migration in the portal_migration tool. You should do this to make sure the migration gets the expected result in the UI. Ideally, you should also create a fresh Plone site and make sure your changes show up there as well.
However, it is vital to properly unit test your migrations. Given the variety of conditions under which they may be run, migrations need to be as simple and well tested as possible.
There are two unit test files which pertain to migrations and portal creation: CMFPlone/tests/testMigrations.py and CMFPlone/tests/testPortalCreation.py.
First of all, you need to test your migration functions individually. Look at testMigrations.py - you will need to import your migration method at the top of the file, and add your unit tests to the test class corresponding to the major version the migration is for - for example TestMigrations_v2_1 for Plone 2.1 migrations.
Be sure to look at the existing unit tests there for examples. Typically, you will have at least one unit test for when your migration works as normal, one for when it is called twice, and one for each assumption which may be removed, e.g. testMyMigration, testMyMigrationTwice and testMyMigrationNoTool. If you have more specific needs, you should have no problems finding examples of various ways of unit testing migrations in testMigrations.py.
Finally, you must add a test for the result of the migration to testPortalCreation.py. Typically, this will test for the existence of some new property or object created by the migration - in other words, it will assert the assumptions introduced by the migration.
The crucial difference between these two files is that in testMigrations, you test the migration function itself, whilst in testPortalCreation you test the state of the portal that is asserted by the migration. As of Plone 2.5, the portal creation tests will actually test the GenericSetup profile, not the migrations, but it is important that the final test state for a successful migration step is consistent with the portal creation test you add here.
Again, you should have no trouble finding examples of tests in testPortalCreation.
A note about version control
The migration function files and migration unit test files are prime candidates for svn conflicts, because they are typically worked on by several developers simultanoeusly during periods of high activity. Hence, be careful when resolving conflicts, and always re-run all unit tests before commiting after resolving any conflicts. This goes for any conflicts, of course, but in these files it is particularly important!
5. Specific areas
Explanations of various areas of the codebase that may not be obvious
5.1. Content types
The relationship between CMF content types, Plone extensions, and ATContentTypes, as well as the machinery that publishes content objects to the browser
5.1.1. ATContentTypes
Since Plone 2.1, Plone has shipped with ATContentTypes for its default content types
ATContentTypes is a re-implementation of the standard CMF types as Archetypes content. It adds a numer of features to the standard CMF types and offers more flexibility in extending and re-using content types. The RichDocument tutorial explains how ATContentTypes are subclassed and how they make use of the latest conventions in Archetypes and CMF.
The ATContentTypes product is installed during the creation of a Plone site. It will migrate the base CMF content types to its own equivalents using its own highly generic migration framework.
Please note that ATContentTypes aims to be usable in plain CMF. It has a number of optional Plone dependencies, in the form:
if HAS_PLONE21:
...
else:
...
Plone has no direct dependencies on ATContentTypes, nor on Archetypes. There are a few generic interfaces in CMFPlone.interfaces that are used by both Archetypes/ATContentTypes and Plone, but we do not wish to have any direct dependency on Archetypes, since Archetypes is essentially just a development framework to make developing CMF content types easier. By minimising the number of dependencies, we ensure that plain-CMF (and in the future, plain-Zope 3) content types are still usable within Plone.
ATContentTypes and Plone both depend on CMFDynamicViewFTI. This is a wrapper on the standard CMF FTI type that adds support for the display menu by recording a few extra properties for the available and currently-selected view methods. It also provides a mixin class, CMFDynamicViewFTI.browserdefault.BrowserDefaultMixin, which enables support for the display menu (or rather, the interface CMFDynamicViewFTI.interfaces.ISelectableBrowserDefault).
5.1.2. The 'display' menu
The 'display' menu is the drop-down that lets content authors select which view template to use, or which object to set as a default-page in a folder.
The display menu is found in global_contentmenu.pt and supports three different functions:
- Set the display template (aka "layout") of the current content object, provided that object supports this.
- Set the default-page of a folder, provided the folder supports this
- If viewing a folder with a default-page, allow selecting the standard view template/layout for that folder, thus unsetting the default-page.
There are two interfaces in CMFDynamicViewFTI.interfaces that are used to support this functionality:
- IBrowserDefault
- Provides information about the layout current selection of a given content object, including any selected deafult-page
- ISelectableBrowserDefault
- Extends IBrowserDeafult with methods to manipulate the current selection
The canonical implementation of both these interfaces is in CMFDynamicViewFTI.browserdefault.BrowserDefaultMixin. This in turn gets the vocabulary of available view methods from the FTI (and hence this can be edited through-the-web in portal_types), and stores the current selection in two properties on each content object: layout, for the currently selected view template, and default_page if any default page is selected. If both are set, the default-page will take precedence.
BrowserDefaultMixin actually provides a __call__ method which means that will render the object with its default layout template. However, PloneTool.browserDefault() will actually query the interface directly to find out which template to display - please see the next page for the gory details.
5.1.3. Restricting addable types
The constrain-types machinery and how it drives the "restrict..." option under the "add item" menu.
As of Plone 2.1, the "add item" menu supports a "restrict..." page that lets the user decide which items can and cannot be added to that folder. This functionality is defined in a pair of interfaces in CMFPlone.interfaces.constrains, IConstrainTypes for read-only access and ISelectableConstrainTypes for the mutators.
The canonical implementation of these interfaces is in ATContentTypes.lib.constraintypes. This provides storage for the constraint mode (more below) and the list of locally allowed and "preferred" types. The preferred types are the ones that appear in the list immediately, and the rest of the allowed types appear behind a "more..." item.
The constraint type mode can be ACQUIRE (the default), DISABLED or ENABLED. When disabled, the settings in portal_types are used. When enabled, the list of types explicitly set are used. When set to acquire, the parent folder's types will be used if the parent is of the same portal type as the folder in question. If they are of different types the settings in portal_types apply.
The rest of the ConstrainTypesMixin class overrides CMFCore's allowedContentTypes and invokeFactory methods to ensure the constraints are enforced.
5.1.4. From Zope to the Browser
How do content types get "published" (in the Zope sense, not the workflow sense) to the web browser?
There is a fairly complex mechanism that determines how a content object ends up being displayed in the browser. The following is an adaptation of an email to the plone-devel list which aims to untangle this complexity. It pertains to Plone 2.1 only.
Assumptions:
- You want the
viewaction to be the same as what happens when you go to the object directly for most content types...
- ...but for some types, like File and Image, you want the "view" action to display a template, whereas if you go straight to the object, you get the file's contents
- You want to be able to redefine the
viewaction in your custom content types or TTW in portal_types explicitly. This will essentially override the current layout template selection. Probably this won't be done very often for things deriving from ATContentTypes, since here you can register new templates with the FTI and have those be used (via the "display" menu) in a more flexible (e.g. per-instance, user-selectable) way, but you still want the "view" action to give the same power to change the default view of an object as it always has. - When you use the "display" menu (implemented with IBrowserDefault) to set a default page in a folderish container, you want it to display that item always, unless there is an index_html - index_html always wins (note - the "display" menu is disabled when there is an index_html in the folder, precisely because it will have no effect)
- When you use the "display" menu to set a layout template for an object (folderish or not), you want that to be displayed on the "view" tab (action), as well as by default when the object is traversed to without a template/action specified...
- ...except for ATFile and ATImage, which use a method index_html() to cut in when you don't explicitly specify an item. However, these types will still want their "view" action to show the selected layout, but will want a no-template invocation to result in the file content
Some implementation detail notes:
There are two distinct cases:
- CASE I: "New-style" content types using the paradigms of ATContentTypes
- These implement ISelectableBrowserDefault, now found in the generic CMFDynamicViewFTI product. They support the "display" menu with per-instance selectable views, including the ability to select a default-page for folders via the GUI. These use CMF 1.5 features explicitly.
- CASE II: "Old-style" content types, including CMF types and old AT types
- These do not implement this interface. The "display" menu is not used. The previous behaviour of Plone still holds.
The "old-style" behaviour is implemented using the Zope hook __browser_default__(), which exists to define what happens when you traverse to an object without an explicit page template or method. This is used to look up the default-page (e.g. index_html) or discover what page template to render. In Plone, __browser_default__() calls PloneTool.browserDefault() to give us a single place to keep track of this logic. The rules are (slightly simplified):
- A method, attribute or contained object
index_htmlwill always win. Files and Images use this to dump content (via a method index_html()); creating a content object index_html in a folder as a defualt page is the now-less-encouraged way, but should still be the method that trumps all others. - A propery
default_pageset on a folderish object giving the id of a contained object to be the default-page is checked next. - A property
default_pageinsite_propertiesgives us a list of ids to check and treat similarly to index_html. If a folder contains items with any of these magic ids, the first one found will be used as a default-page. - If the object has a
folderlistingaction, use this. This is a funny fallback which is necessary for old-style folders to work (see below). - Look up the object's
viewaction and use this if none of the above hold true.
In addition, we test for ITranslatable to allow the correct translation of returned pages to be selected (LinguaPlone), and have some WebDAV overrides.
Lastly, it has always been possible to put "/view" at the end of a URL and get the view of the object, regardless of any index_html() method. This means that you can go to /path/to/file/view and get the view of the file, even if /path/to/file would dump the content (since it has an index_html() method that does that).
This mechanism uses the method view(), defined in PortalContent in CMF (and also in BaseFolder in Archetypes). view() returns self(), which results in a call to __call__(). In CMF 1.4, this would look up the view action and resolve this. Note that for folders in Plone 2.0, the view action is just string:${object_url}/, which in turn results in __browser_default__() and the above rules. This means that /path/to/folder/view will render a default-page such as a content object index_html. The fallback on the folderlisting action in PloneTool.browserDefault() mentioned above is there to ensure that when there isn't an index_html or other default-page, we get folder_listing (instead of an infinite loop), essentially making the folderlisting action on Folders the canonical place to specfy the view template. If you think that sounds messy, you're right. (With CMF 1.5 types, things are little different - more on that later.)
Enter CMF 1.5. CMF 1.5 introduces "Method Aliases". It is important to separate these from actions:
- Actions
- These generate the content action tabs (the green ones). You almost always have
viewandedit. Other standard actions arepropertiesandsharing. Each action has a target, which is typically something likestring:${object_url}/base_editfor the edit tab.base_edithere is a page template. - Method aliases
- These let you generalise actions. The alias
editcan point toatct_editfor an ATContentTypes document, for example, and point todocument_edit_formfor a CMF document. Aliases can be traversed to, so /path/to/object/edit will send you toatct_editon the object if the object is an ATContentTypes document, and todocument_edit_formif it is a CMF Document.
This level of indirection is actually quite useful. First of all, we get a standard set of URLs, so /path/to/object/edit is always edit, /path/to/object/view is always view. The actions (tabs) can point to these, meaning that we can pretty much use the same set of actions for all common types, with the variation happening in the aliases instead.
Secondly, a method alias with the name "(Default)" specifies what happens when you browse to the object without any template or action specified. That is, /path/to/object will look up the "(Default)" alias. This may specify a page template, for example, or a method (such as a file-dumping index_html()) to call.
Crucially, if "(Default)" is not set or is an empty string, CMF falls back on the old behaviour of calling the __browser_default__() method. In PloneFolder.py, this is defined to call PloneTool.browserDefault(), as mentioned above, which implements the Plone-specific rules for the lookup. Hence, if we need the old behaviour, we can just unset "(Default)"! This is what happens with old-style content types (that is, it is the default if you're not using ATContentTypes' base classes or setting up the aliases yourself).
Now, CMFDynamicViewFTI, which is used by ATContentTypes, extends the standard CMF FTI and a adds a few things:
- A pair of interfaces, ISelectableBrowserDefault and IBrowserDefault (the former extends the latter) describing various methods for getting dynamic views, as found in Plone in the "display" menu.
- A class BrowserDefaultMixin which gives you a sensible implementation of these. This uses two properties, "default_page" and "layout" to keep track of which default-page and/or view template (aka layout) is currently selected on an object.
- Two new properties in the FTI in portal_types - the default view, and the list of available views.
- A special target for a method alias called
(selected layout), which will return the selected view template (layout). - Another special alias target called
(dynamic view), which will return a default-page, if set, or else the selected view template (layout) - you can think of "(dynamic view)" as a superset of "(selected layout)".
ATContentTypes uses BrowserDefaultMixin from CMFDynamicViewFTI, and sets up the standard aliases for "(Default)" and "view" to point to "(dynamic view)". The exceptions are File and Image, which have the "(Default)" alias pointing to "index_html", and the "view" alias pointing to "(selected layout)". This way, /path/to/file results in the file content (via the index_html() method) and /path/to/file/view shows the selected layout inside Plone. (Note that using "(dynamic view)" for the "view" alias would not work, because the index_html attribute would take precedence over the layout when testing for a default-page.) Additionaly, the view action (tab) for each of these types must be string:${object_url}/view to ensure it invokes the "view" alias, not the "(Default)" alias.
For Folders, the use of "(dynamic view)" takes care of the default-page and the selected view template. The folderlisting fallback is no longer needed - the view action can still be "string:${object_url}", and the "(Default)" alias pointing to "(dynamic view)" takes care of the rest.
In order for the "(dynamic view)" target to work as expected, it needs to delegate to PloneTool so that Plone's rules for lookup order and (especially) ITranslatable/LinguaPlone support are used. However, delegating to PloneTool.browserDefault() is not an option, because this does other checks which are not relevant (this essentially stems from the fact that browserDefault() is implementing both the "(Default)" and "view" cases above in a single method). Thus, the code for determining which, if any, contained content object should be used as a default-page has been factored out to its own method, PloneTool.getDefaultPage(). Helpfully, this can also be used by PloneTool.isDefaultPage(), radically simplifying that method.
Calling content objects
The last issue is what happens with view() and __call__() in this equation. The first thing to note is that view() method is masked by the view method alias. Hence, /path/to/object/view will invoke the method alias view if it exists, not call view(), making that method a lot less relevant.
However, we still want __call__() to have a well-defined behaviour. In CMF 1.4, __call__()used to look up the view action, and this is still the default fallback, but if the "(Default)" alias is set, this is used instead. This may give somewhat unexpected behaviour, however: From the comments in the source code and the behaviour in Zope, where __call__() is the last fallback if neither __browser_default__() nor index_html are found, and to ensure that the "view() --> __call__()" mechanism always returns the object itself, never dumped file content, it seems to be the intention that __call__() should always return the object, never a default-page or file content dumped via an index_html() method. For Folders in Plone 2.0, this was actually not the case: __call__() would look up the view action, which was "string:${object_url}", which with the use of __browser_default__() resulted in a lookup of a default-page if one was present. With the CMF 1.5 behaviour, the use of the "(Default)" alias in __call__() will mean that calling a File returns the dumped file content. Calling a Folder will return the default-page (or the Folder in its view if no default page is set) as in Plone 2.0.
The behaviour in Plone 2.1 is that __call__(), as overridden in BrowserDefaultMixin, should always return the object itself as it would be rendered in Plone without any index_html or default-page magic. Hence, __call__() in CMFDynamicViewFTI looks up the "(selected layout)" target and resolves this. This behaviour is thus consistent with the old behaviour of Documents and Files, but whereas Folders with a default-page in 2.0 used to return that default page from __call__(), in 2.1, it returns the Folder itself rendered in its selected layout. Again remember that this method will rarely if ever be called, since /path/to/object is intercepted by CMF's pre-traversal hook and ends up looking up the "(Default)" method alias (which does honour default-page for Folders), and /path/to/object/view uses the "view" method alias, as described above.
5.2. Navigation structures
How the navtree is constructed, and how it may be extended
5.2.1. Navigation root
How Plone determines the navigation root to use for the sitemap, navigation tree, tabs and breadcrumbs.
As of Plone 2.5, the root property in portal_properties/navtree_properties enables the site admin to set a folder as the root of the site's navigation. This will affect not only the navigation tree (which was all it did in Plone 2.1.3 and later versions in the 2.1 series), but also affect where the breadcrumbs, the tabs and the sitemap are rooted.
The canonical navigation root is returned as a path, relative to the portal root, in CMFPlone.browser.navtree.getNavigationRoot(). This will use the property in navtree_properties, unless the context or a parent object is marked with the CMFPlone.browser.interfaces.INavigationRoot interface. If such an object is found, that is used as the navigation root. By default, the portal root is marked with this interface, but it's possible to mark other objects with it as well.
The root path is used by the navtree and sitemap query builders and the navigation tabs and breadcrumb algorithms (see the following pages) to root the catalog query used to construct these.
5.2.2. Constructing the navigation tree
How the navigation tree is constructed from a catalog query and how the settings in portal_properties are able to influence the shape of the final tree.
The navigation tree portlet uses a view, found in CMFPlone.browser.interfaces.INavigationPortlet with a factory in CMFPlone.browser.portlets.navigation. This view in turn uses a more general view, CMFPlone.browser.interfaces.INavigationTree that is implemented in CMFPlone.browser.navigation.CatalogNavigationTree.
Previously, the navigation tree used to be constructed in CMFPlone.PloneTool, and there is still a deprecated createNavTree() method there. A utility method that invokes the view is found in CMFPlone.utils.createNavTree().
The actual construction of the navtree is delegated to a generic function found in CMFPlone.browser.navtree called buildFolderTree(). This function has a single purpose: Execute a catalog query and turn the results into a navigation tree-like structure. It is thus generic, and can be used to construct other navigational structures (such as the TOC for this reference manual).
Because the navigation tree needs to take several properties into account, the navtree builder can take a "strategy" object, which implements CMFPlone.browser.interfaces.INavtreeStrategy. This can provide methods used to decide whether a given node should be included and/or whether a whole subtree should be pruned. It can also decorate the node dicts with additional keys, used to hold domain-specific information.
The standard navigation tree strategy is found in CMFPlone.browser.navtree.DefaultNavtreeStrategy. In fact this extends the sitemap navtree strategy, because a navtree is essentially a more restricted form of a sitemap.
The navtree strategy is an adapter, looked up on the context (the content object being viewed - registered for * by default) and the interface of the view that is constructing it (to distinguish the sitemap from the navtree):
<adapter for="*
.interfaces.INavigationTree"
factory=".navtree.DefaultNavtreeStrategy"
provides=".interfaces.INavtreeStrategy" />
The CatalogNavigationTree class that implements INavigationTree thus does this:
def navigationTree(self):
context = utils.context(self)
queryBuilder = NavtreeQueryBuilder(context)
query = queryBuilder()
strategy = getMultiAdapter((context, self), INavtreeStrategy)
return buildFolderTree(context, obj=context, query=query, strategy=strategy)
The NavtreeQueryBuilder, is found in CMFPlone.browser.navtree, with an interface in CMFPlone.browser.interfaces.INavigationQueryBuilder. This simply wraps up the task of constructing the catalog query dict that is used to build the navtree, so that it may be re-used.
To make it easier to create custom navtrees and similar structures for site integrators, the contents of CMFPlone.browser.navtree may be imported from protected code, by virtue of the following statement in 'CMFPlone/__init__.py':
allow_module('Products.CMFPlone.browser.navtree')
To ensure that the methods and attributes of the query builders and navtree strategies are accessible as well, CMFPlone/browser/configure.zcml contains:
<content class="Products.CMFPlone.Portal.PloneSite">
<implements interface=".interfaces.INavigationRoot" />
</content>
<content class=".navtree.NavtreeStrategyBase">
<allow interface=".interfaces.INavtreeStrategy" />
</content>
<content class=".navtree.NavtreeQueryBuilder">
<allow interface=".interfaces.INavigationQueryBuilder" />
</content>
5.2.3. Constructing the sitemap
How the sitemap is constructed
The sitemap is constructed using the same mechanisms as the navigation tree - it simply uses a different query builder and navtree strategy.
The view that is used by the sitemap.pt page template is found in CMFPlone.browser.sitemap.SitemapView, and implements CMFPlone.browser.interfaces.ISitemapView. It in turn delegates to the more general view CMFPlone.browser.navigation.CatalogSiteMap which implements CMFPlone.browser.interfaces.ISitemap. This does:
def siteMap(self):
context = utils.context(self)
queryBuilder = SitemapQueryBuilder(context)
query = queryBuilder()
strategy = getMultiAdapter((context, self), INavtreeStrategy)
return buildFolderTree(context, obj=context, query=query, strategy=strategy)
Note that the sitemap and navtree query builders and strategies share much code and are derived from one another.
There is a utilty method in CMFPlone.utils called createSiteMap() that invokes the ISitemap view. The deprecated way of constructing the sitemap is through a call to PloneTool.createNavTree setting sitemap=True.
5.2.4. Navigation tabs
How the navigation tabs are constructed
Since Plone 2.1, the navigation tabs at the top of the site have been constructed from two sources:
- Any actions in the
portal_tabscategory - Any folders in the root of the site
In Plone 2.1, these were created using PloneTool.createTopLevelTabs(). This is now deprecated. A utility method in PloneTool.utils with the same name now wraps the call to the view that implements this functionality. The view can be found in CMFPlone.browser.navigation.CatalogNavigationTabs, with an interface in CMFPlone.browser.INavigationTabs.
The view implementation performs an action lookup, and constructs a query that that finds folderish items in the navigation root. Note that the "navigation root" is not necessarily the portal root - see the section on the navigation root.
5.2.5. Breadcrumbs
How the breadcrumbs in the pathbar are constructed.
Plone 2.5 actually ships with two implementations of the breadcrumbs. Both can be used as the view that implements CMFPlone.browser.interfaces.INavigationBreadcrumbs. The older, currently disabled method is found in CMFPlone.browser.navigation.CatalogNavigationBreadcrumbs, and, as it name implies, uses a catalog query to find the breadcrumbs.
The newer method is found in CMFPlone.browser.navigation.PhysicalNavigationBreadcrumbs. This walks up the acquisition parent hierarchy to find each parent. The astute reader will remember that waking objects like this is generally bad. However, some benchmarking revealed that since the direct parents of the context are quite likely to be in the ZODB cache, and since there are not likely to be very many of them, the cost of traversing to these objects is smaller than the cost of constructing and executing a catalog query.
By being based on real objects, the breadcrumbs can make more extensive use of views. The breadcrumbs are constructed in reverse order, starting at the depeest node. The parents are prepended to the breadcrumbs trail of this node by recursively doing a view lookup on the parent, like so:
def breadcrumbs(self):
context = utils.context(self)
request = self.request
container = utils.parent(context)
...
view = getMultiAdapter((container, request), name='breadcrumbs_view')
base = tuple(view.breadcrumbs())
...
rootPath = getNavigationRoot(context)
itemPath = '/'.join(context.getPhysicalPath())
...
if not utils.isDefaultPage(context, request) and not rootPath.startswith(itemPath):
base += ({'absolute_url': item_url,
'Title': utils.pretty_title_or_id(context, context),
},)
return base
The traversal is stopped at the navigation root by the following degenerate view:
class RootPhysicalNavigationBreadcrumbs(utils.BrowserView):
implements(INavigationBreadcrumbs)
def breadcrumbs(self):
# XXX Root never gets included, it's hardcoded as 'Home' in
# the template. We will fix and remove the hardcoding and fix
# the tests.
context = utils.context(self)
return ()
Then, the two views are registered in CMFPlone/browser/configure.zcml as follows:
<browser:page
for="*"
name="breadcrumbs_view"
class=".navigation.PhysicalNavigationBreadcrumbs"
permission="zope.Public"
allowed_attributes="breadcrumbs"
/>
<browser:page
for=".interfaces.INavigationRoot"
name="breadcrumbs_view"
class=".navigation.RootPhysicalNavigationBreadcrumbs"
permission="zope.Public"
allowed_attributes="breadcrumbs"
/>
This pattern is quite elegant, and allows for more specific implementations to be plugged based on the interface of the object, rather than having to substitute it globally.