Professional Plone 4 Development: Developing a Site Strategy

Exclusive offer: get 50% off this eBook here
Professional Plone 4 Development

Professional Plone 4 Development — Save 50%

Build robust, content-centric web applications with Plone 4.

$35.99    $18.00
by Martin Aspeli | August 2011 | Content Management Open Source Web Development

Plone is a web content management system that features among the top two percent of open source projects and is used by more than 300 solution providers in 57 countries. Its powerful workflow system, outstanding security track record, friendly user interface, elegant development model, and vibrant community makes Plone a popular choice for building content-centric applications. By customizing and extending the base platform, integrators can build unique solutions tailored to specific projects quickly and easily.

In this article by Martin Aspeli, author of Professional Plone 4 Development, we will:

  • Create the initial version of a policy package that will help us customize Plone
  • Add a GenericSetup extension profile to this package
  • Write our first tests to prove that our customizations are working as expected

 

Professional Plone 4 Development

Professional Plone 4 Development

Build robust, content-centric web applications with Plone 4.

        Read more about this book      

(For more resources on Plone, see here.)

Creating a policy package

Our policy package is just a package that can be installed as a Plone add-on. We will use a GenericSetup extension profile in this package to turn a standard Plone installation into one that is configured to our client's needs.

We could have used a full-site GenericSetup base profile instead, but by using a GenericSetup extension profile we can avoid replicating the majority of the configuration that is done by Plone.

We will use ZopeSkel to create an initial skeleton for the package, which we will call optilux.policy, adopting the optilux.* namespace for all Optilux-specific packages.

In your own code, you should of course use a different namespace. It is usually a good idea to base this on the owning organization's name, as we have done here. Note that package names should be all lowercase, without spaces, underscores, or other special characters. If you intend to release your code into the Plone Collective, you can use the collective.* namespace, although other namespaces are allowed too. The plone.* namespace is reserved for packages in the core Plone repository, where the copyright has been transferred to the Plone Foundation. You should normally not use this without first coordinating with the Plone Framework Team.

We go into the src/ directory of the buildout and run the following command:

$ ../bin/zopeskel plone optilux.policy

This uses the plone ZopeSkel template to create a new package called optilux.policy. This will ask us a few questions.

We will stick with "easy" mode for now, and answer True when asked whether to register a GenericSetup profile.

Note that ZopeSkel will download some packages used by its local command support. This may mean the initial bin/zopeskel command takes a little while to complete, and assumes that we are currently connected to the internet.

A local command is a feature of PasteScript, upon which ZopeSkel is built. ZopeSkel registers an addcontent command, which can be used to insert additional snippets of code, such as view registrations or new content types, into the initial skeleton generated by ZopeSkel. We will not use this feature, preferring instead to retain full control over the code we write and avoid the potential pitfalls of code generation. If you wish to use this feature, you will either need to install ZopeSkel and PasteScript into the global Python environment, or add PasteScript to the ${zopeskel:eggs} option in buildout.cfg, so that you get access to the bin/paster command.

Run bin/zopeskel --help from the buildout root directory for more information about ZopeSkel and its options.

Distribution details

Let us now take a closer look at what ZopeSkel has generated for us. We will also consider which files should be added to version control, and which files should be ignored.

Item

Version control

Purpose

setup.py

Yes

Contains instructions for how Setuptools/Distribute (and thus Buildout) should manage the package's distribution. We will make a few modifications to this file later.

optilux.policy.egg-info/

Yes

Contains additional distribution configuration. In this case, ZopeSkel keeps track of which template was used to generate the initial skeleton using this file.

*.egg

No

ZopeSkel downloads a few eggs that are used for its local command support (Paste, PasteScript, and PasteDeploy) into the distribution directory root. If you do not intend to use the local command support, you can delete these. You should not add these to version control.

README.txt

Yes

If you intend to release your package to the public, you should document it here. PyPI requires that this file be present in the root of a distribution. It is also read into the long_description variable in setup.py. PyPI will attempt to render this as reStructuredText markup (see http://docutils.sourceforge.net/rst.html).

docs/

Yes

Contains additional documentation, including the software license (which should be the GNU General Public License, version 2, for any packages that import directly from any of Plone's GPL-licensed packages) and a change log.

Changes to setup.py

Before we can progress, we will make a few modifications to setup.py. Our revised file looks similar to the following code, with changes highlighted:

from setuptools import setup, find_packages
import os
version = '2.0'
setup(name='optilux.policy',
version=version,
description="Policy package for the Optilux Cinemas project",
long_description=open("README.txt").read() + "\n" +
open(os.path.join("docs", "HISTORY.txt")).read(),
# Get more strings from
# http://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
"Framework :: Plone",
"Programming Language :: Python",
],
keywords='',
author='Martin Aspeli',
author_email='optilude@gmail.com',
url='http://optilux-cinemas.com',
license='GPL',
packages=find_packages(exclude=['ez_setup']),
namespace_packages=['optilux'],
include_package_data=True,
zip_safe=False,
install_requires=[
'setuptools',
'Plone',
],
extras_require={
'test': ['plone.app.testing',]
},
entry_points="""
# -*- Entry points: -*-

[z3c.autoinclude.plugin]
target = plone
""",
# setup_requires=["PasteScript"],
# paster_plugins=["ZopeSkel"],
)

The changes are as follows:

  1. We have added an author name, e-mail address, and updated project URL. These are used as metadata if the distribution is ever uploaded to PyPI. For internal projects, they are less important.
  2. We have declared an explicit dependency on the Plone distribution, that is, on Plone itself. This ensures that when our package is installed, so is Plone. We will shortly update our main working set to contain only the optilux. policy distribution. This dependency ensures that Plone is installed as part of our application policy.
  3. We have then added a [tests] extra, which adds a dependency on plone. app.testing. We will install this extra as part of the following test working set, making plone.app.testing available in the test runner (but not in the Zope runtime).
  4. Finally, we have commented out the setup_requires and paster_plugins options. These are used to support ZopeSkel local commands, which we have decided not to use. The main reason to comment them out is to avoid having Buildout download these additional dependencies into the distribution root directory, saving time, and reducing the number of files in the build. Also note that, unlike distributions downloaded by Buildout in general, there is no "offline" support for these options.

Changes to configure.zcml

We will also make a minor change to the generated configure.zcml file, removing the line:

<five:registerPackage package="." initialize=".initialize" />

This directive is used to register the package as an old-style Zope 2 product. The main reason to do this is to ensure that the initialize() function is called on Zope startup. This may be a useful hook, but most of the time it is superfluous, and requires additional test setup that can make tests more brittle.

We can also remove the (empty) initialize() function itself from the optilux/policy/__init__.py file, effectively leaving the file blank. Do not delete __init__.py, however, as it is needed to make this directory into a Python package.

Updating the buildout

Before we can use our new distribution, we need to add it to our development buildout. We will consider two scenarios:

  1. The distribution is under version control in a repository module separate to the development buildout itself. This is the recommended approach.
  2. The distribution is not under version control, or is kept inside the version control module of the buildout itself. The example source code that comes with this article is distributed as a simple archive, so it uses this approach.

Given the approach we have taken to separating out our buildout configuration into multiple files, we must first update packages.cfg to add the new package. Under the [sources] section, we could add:

[sources]
optilux.policy = svn https://some-svn-server/optilux.
policy/trunk

Or, for distributions without a separate version control URL:

[sources]
optilux.policy = fs optilux.policy

We must also update the main and test working sets in the same file:

[eggs]
main =
optilux.policy
test =
optilux.policy [test]

Finally, we must tell Buildout to automatically add this distribution as a develop egg when running the development buildout. This is done near the top of buildout.cfg:

auto-checkout =
optilux.policy

We must rerun buildout to let the changes take effect:

$ bin/buildout

We can test that the package is now available for import using the zopepy interpreter:

$ bin/zopepy
>>> from optilux import policy
>>>

The absence of an ImportError tells us that this package will now be known to the Zope instance in the buildout.

To be absolutely sure, you can also open the bin/instance script in a text editor (bin/instance-script.py on Windows) and look for a line in the sys.path mangling referencing the package.

Working sets and component configuration

It is worth deliberating a little more on how Plone and our new policy package are loaded and configured.

At build time:

  1. Buildout installs the [instance] part, which will generate the bin/instance script.
  2. The plone.recipe.zope2instance recipe calculates a working set from its eggs option, which in our buildout references ${eggs:main}.
  3. This contains exactly one distribution: optilux.policy.
  4. This in turn depends on the Plone distribution which in turn causes Buildout to install all of Plone.

Here, we have made a policy decision to depend on a "big" Plone distribution that includes some optional add-ons. We could also have depended on the smaller Products.CMFPlone distribution (which works for Plone 4.0.2 onwards), which includes only the core of Plone, perhaps adding specific dependencies for add-ons we are interested in.

When declaring actual dependencies used by distributions that contain reusable code instead of just policy, you should always depend on the packages you import from or otherwise depend on, and no more. That is, if you import from Products.CMFPlone, you should depend on this, and not on the Plone meta-egg (which itself contains no code, but only declares dependencies on other distributions, including Products. CMFPlone). To learn more about the rationale behind the Products. CMFPlone distribution, see http://dev.plone.org/plone/ticket/10877.

At runtime:

  1. The bin/instance script starts Zope.
  2. Zope loads the site.zcml file (parts/instance/etc/site.zcml) as part of its startup process.
  3. This automatically includes the ZCML configuration for packages in the Products.* namespace, including Products.CMFPlone, Plone's main package.
  4. Plone uses z3c.autoinclude to automatically load the ZCML configuration of packages that opt into this using the z3c.autoinclude.plugin entry point target = plone.
  5. The optilux.policy distribution contains such an entry point, so it will be configured, along with any packages or files it explicitly includes from its own configure.zcml file.
Professional Plone 4 Development Build robust, content-centric web applications with Plone 4.
Published: August 2011
eBook Price: $35.99
Book Price: $59.99
See more
Select your format and quantity:
        Read more about this book      

(For more resources on Plone, see here.)

Creating an extension profile

Let us now register an extension profile for the policy package. ZopeSkel has already done some of the work for us. In configure.zcml, we have:

<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five"
xmlns:i18n="http://namespaces.zope.org/i18n"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="optilux.policy">
<genericsetup:registerProfile
name="default"
title="Optilux Site Policy"
directory="profiles/default"
description="Turn a Plone site into the Optilux site."
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
<!-- -*- extra stuff goes here -*- -->
</configure>

The XML standard requires that all namespaces be declared using the xmlns: syntax seen on the first few lines of this file. A common mistake is to use a directive such as <genericsetup:registerProfile />, but forget to declare the genericsetup namespace. This will result in an "unknown directive" error.

The <genericsetup:registerProfile /> stanza registers a new profile. The title and description (which we have edited from their generated defaults) will be shown to the user when activating the policy package. The name is almost always default, unless the package contains multiple profiles. The full profile name as known to GenericSetup includes the package name, so in this case, it will be profile-optilux. policy:default.

The profile- prefix indicates that this is an (extension) profile. GenericSetup snapshots have names starting with snapshot-.

The directory argument tells GenericSetup where to look for the XML files which the various import handles will read, relative to the package. By convention, this is profiles/default for the primary profile.

We then create the profile directory (src/optilux.policy/optilux/policy/ profiles/default), and add a metadata.xml file inside it:

<metadata>
<version>1</version>
<dependencies>
</dependencies>
</metadata>

This defines the profile version, which should always start at 1. It will stay that way until we need to worry about upgrades. We have also added an empty dependencies list, as our package currently has no dependencies.

Writing tests for customizations

We will begin by making a simple change: setting the browser window page title and site description. These values are managed as properties called title and description on the Plone site root. We can view these in the Site control panel under Site Setup in Plone.

As good software developers, we will write automated tests before implementing the functionality. In this case, we will write integration tests that inspect the state of the Plone site after our package has been configured and activated.

By convention, tests go into a module or subpackage called tests. We already have such a module (tests.py) containing some example code, which we will replace with our own test code. Test fixture setup code conventionally lives in a module called testing (testing.py), which may be imported by other code that wishes to reuse a package's test setup.

The boilerplate in the tests.py as generated by ZopeSkel at the time of writing uses the Products.PloneTestCase testing framework. Our code will use the newer plone.app.testing framework, which aims to replace Products.PloneTestCase. See the plone.app.testing documentation (http://pypi.python.org/pypi/plone.app.testing) for more details, including a comparison between the two.

Before we write the tests themselves, we will add a test layer that configures a shared test fixture for our integration tests.

A test layer allows multiple tests to share the same fixture, alleviating the need for each test to set up and tear down a complex fixture. Test layers can also control the lifecycle of individual tests, for example to isolate each test in its own transaction.

In testing.py, we have:

from plone.app.testing import PloneSandboxLayer
from plone.app.testing import applyProfile
from plone.app.testing import PLONE_FIXTURE
from plone.app.testing import IntegrationTesting

from zope.configuration import xmlconfig

class OptiluxPolicy(PloneSandboxLayer):

defaultBases = (PLONE_FIXTURE,)

def setUpZope(self, app, configurationContext):
# Load ZCML
import optilux.policy
xmlconfig.file('configure.zcml',
optilux.policy,
context=configurationContext
)
def setUpPloneSite(self, portal):
applyProfile(portal, 'optilux.policy:default')

OPTILUX_POLICY_FIXTURE = OptiluxPolicy()
OPTILUX_POLICY_INTEGRATION_TESTING = IntegrationTesting(
bases=(OPTILUX_POLICY_FIXTURE,),
name="Optilux:Integration"
)

This layer uses the PloneSandboxLayer helper from plone.app.testing. It first loads the package's configuration, and then installs its GenericSetup profile. All tests that use the OPTILUX_POLICY_INTEGRATION_TESTING layer will thus be able to assume the package has been configured and its profile applies to the Plone site that is set up by the PLONE_FIXTURE base layer.

The tests, in tests.py, look like this:

import unittest2 as unittest
from optilux.policy.testing import OPTILUX_POLICY_INTEGRATION_
TESTING
class TestSetup(unittest.TestCase):
layer = OPTILUX_POLICY_INTEGRATION_TESTING
def test_portal_title(self):
portal = self.layer['portal']
self.assertEqual(
"Optilux Cinemas",
portal.getProperty('title')
)
def test_portal_description(self):
portal = self.layer['portal']
self.assertEqual(
"Welcome to Optilux Cinemas",
portal.getProperty('description')
)

If you have not done so already, you should read the documentation at http://pypi.python.org/pypi/plone.testing and http://pypi.python.org/pypi/plone.app.testing to familiarize yourself with testing concepts and APIs.

We should now be able to run the tests. Both of the preceding tests should fail, since we have not yet written the functionality to make them pass.

$ bin/test -s optilux.policy
Running optilux.policy.testing.Optilux:Integration tests:
...
Failure in test test_portal_description (optilux.policy.tests.
TestSetup)
...
AssertionError: 'Welcome to Optilux Cinemas' != ''
...
Failure in test test_portal_title (optilux.policy.tests.TestSetup)
...
AssertionError: 'Optilux Cinemas' != u'Plone site'

Ran 2 tests with 2 failures and 0 errors in 0.010 seconds.

The actual output is a little more verbose, but these lines tell us that both our tests failed, as expected.

Making a change with the extension profile

To implement the desired functionality, we create a file inside profiles/default called properties.xml, containing the following code:

<?xml version="1.0"?>
<site>
<property name="title">Optilux Cinemas</property>
<property name="description">Welcome to Optilux Cinemas</property>
</site>

This was taken from the corresponding file in Products/CMFPlone/ profiles/default, reduced to only the properties that we wanted to change. Plone's base profile is a good place to look for examples of GenericSetup syntax. See also http://plone.org/documentation/manual/developer-manual/generic-setup.

One of the import steps installed with Plone, Site Properties, knows how to read this file and set properties on the site root accordingly.

Our tests should now pass:

$ bin/test -s optilux.policy
Running optilux.policy.testing.Optilux:Integration tests:
...
Ran 2 tests with 0 failures and 0 errors in 0.008 seconds.

Activating the package

Finally, we should verify that we can activate the package through the Plone interface. After starting Zope, we can go to our existing Plone site, log in as a user with Manager rights. Under Site Setup, in the Add-ons control panel, the new package should show up as shown in the following screenshot:

Professional Plone 4 Development: Developing a Site Strategy

We can now activate it, and verify that this causes the title in the browser to change.

If we are setting up a new site, we can activate the profile by selecting Optilux Site Policy on the Create a Plone Site screen.

Rebuildout, restart, reapply?

New Plone developers are often confused about when to rerun buildout, restart Zope, or reapply a profile in Plone. In this article, we have made three types of changes that required different types of reinitialization:

  • Changes to the buildout configuration and distribution metadata (in setup. py). These types of changes only take effect once we rerun buildout.
  • Changes to Python and ZCML code. These generally require a Zope restart, although we can often bypass this using plone.reload.
  • Changes to a GenericSetup extension profile. These require that the profile be reapplied through the portal_setup tool (or deactivated and re-activated through the Add-ons control panel). They do not require a Zope restart or reload. The exception is the metadata.xml file, which is read at Zope startup only.

Summary

In this article, we have seen:

  • How to create a "policy package" to encapsulate a specific policy for a site
  • How to use a GenericSetup extension profile to customize various aspects of Plone
  • When to rerun buildout, restart Zope, and reinstall a package in Plone

Further resources related to this subject:


Professional Plone 4 Development Build robust, content-centric web applications with Plone 4.
Published: August 2011
eBook Price: $35.99
Book Price: $59.99
See more
Select your format and quantity:

About the Author :


Martin Aspeli

Martin Aspeli is an experienced Plone consultant and a prolific Plone contributor. He served on the Framework Team for Plone 3.0, and is responsible for many new features such as the improved portlets infrastructure, the “content rules” engine, and several R&D efforts relating to Plone 4.0. He is a former leader of the Plone Documentation Team and has written a number of well-received tutorials available on plone.org. He is also the author of Professional Plone Development and was recognized in 2008 by Packt Publishing as one of the “Most Valuable People” in Open source Content Management Systems.

Books From Packt


Practical Plone 3: A Beginner's Guide to Building Powerful Websites
Practical Plone 3: A Beginner's Guide to Building Powerful Websites

Plone 3 Intranets
Plone 3 Intranets

Plone 3 Products Development Cookbook
Plone 3 Products Development Cookbook

Plone 3.3 Site Administration
Plone 3.3 Site Administration

Building Websites with Plone
Building Websites with Plone

Plone 3 Multimedia
Plone 3 Multimedia

Plone 3 for Education
Plone 3 for Education

Plone 3 Theming
Plone 3 Theming


No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
8
1
F
a
V
r
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software