Weblog

published Nov 03, 2021, last modified Nov 04, 2021

Switch your skin if you are logged in

published Jan 28, 2008, last modified Jan 30, 2008

With collective.editskinswitcher 0.2 you can now also switch your skin if you are logged in.

I improved collective.editskinswitcher again. For explanation on what it is, see my first blog entry about it.

You could already get a different skin (presumably for editors) when you are visiting your Plone Site with an edit url, like edit.yourdomain.com.

Now you can also get that edit skin when you are logged in. More precisely: when you have the __ac cookie. This does not work when you are logged in with basic authentication, in other words: via the ZMI.

This is controlled by two new properties in the portal_properties/editskin_switcher.

  • based_on_url: when True (the default) you get the behaviour described earlier.
  • need_authentication: when True you need to be logged in before your skin is switched. By default this is set to False. This looks for the __ac cookie that Plone gives you when logged in. Note: logging in via the Zope Management Interface is handled without cookies, so the editskin switcher regards you as anonymous then.

You can combine the two behaviours if you want. If they are both True, then you need to have the right url and you need to be logged in.

When both are False, nothing happens: then you might as well simply uninstall this product as it is not useful.

Switch your skin based on the url

published Jan 25, 2008, last modified Jan 30, 2008

Announcing the new collective.editskinswitcher package

Collective Edit Skin Switcher

For a customer of Zest Software I created a package called collective.editskinswitcher. I gladly took some code from colleague Mark van Lent who did something similar for a different web site. The package is on the Cheese Shop so it can be easy installed. And the code is in the Plone collective.

What does it do?

Let's say you have a Plone Site. I tested this with Plone 3. I see no reason why it should fail on Plone 2.5. Maybe it even works on a CMF site. Anyway, whatever site you have is available on two urls: www.yourdomain.com and edit.yourdomain.com. Some day you should ask your local Apache guru how he did that.

With collective.editskinswitcher installed (with the portal quick installer), visitors that go to the website with the url edit.yourdomain.com will see the Plone Default skin, which is meant for content editors. Visitors to www.yourdomain.com will see whatever skin you have set as the default skin in portal_skins. Can be pretty handy.

Developer types probably like the fact that you also get the default skin when visiting localhost and the edit skin when you go to 127.0.0.1.

But maybe you want to turn it around: your visitors should see Plone Default and your editors should see your brilliant Monty Python Skin! Ni! Just go to the portal_properties, then editskin_switcher and change the edit_skin property to your dashing theme.

Why not CMFUrlSkinSwitcher?

I looked at CMFUrlSkinSwitcher first but it had not been touched in two years. One import error (CMFCorePermissions) could easily be fixed as that import was not even used. But after that tests were failing all over the place. Theoretically always fixable of course, but rolling an own package seemed easier, cleaner and faster.

Also, CMFUrlSkinSwitcher does some more things. At least it messes around with some methods like absolute_url. It could be that I find out later that this is necessary in collective.editskinswitcher too, but currently it does not look like that will be the case.

How do I know this is working?

The easiest way to test this package in a default plone site (apart from running the tests of course), is:

  • Install collective.editskinswitcher.
  • Go to portal_skins in the ZMI.
  • Create a new skin selection based on Plone Default. In the tests I call this Monty Python Skin, so I will use that term here as well.
  • Make Monty Python Skin the default skin.
  • Remove the custom skin layer from Plone Default.
  • Customize the main template or the logo or something else that is easy to spot.
  • Visit 127.0.0.1:8080/plonesite and you will see default Plone.
  • Visit localhost:8080/plonesite and you will see Plone with your customization.

On Linux you can edit /etc/hosts and add a line like:

127.0.0.1 edit.yourdomain.com www.yourdomain.com

Now visiting edit.yourdomain.com should give you default Plone and www.yourdomain.com should give you the customized Plone.

You can also let the edit urls begin with cms or manage. As long as the url is something like:

...//(edit|cms|manage).something.something....

you end up in the edit skin.

Have fun!

Maurits van Rees

Easily creating repeatable buildouts

published Jan 19, 2008, last modified Aug 04, 2016

When you create a buildout for a production website, you want to be able to recreate that exact buildout one year from now on a different server. How do you do that? This is a more simple and better working version of an earlier post.

Background

I first wrote a rather difficult guide to getting a stable, repeatable buildout. After some helpful comments from Martijn Faassen I could make this much simpler. I decided to keep the old weblog entry around: it could be helpful for people googling for strange errors in their buildout.

I assume you know what buildout is, otherwise you would probably not be reading this. Martin Aspeli has written a tutorial on Managing projects with zc.buildout. This should be your first stop for learning more about buildout, at least if you are a Plone developer. [Edit: I cannot find this tutorial anymore. Instead see for example buildout.org or an old manual for Plone.]

My brother and colleague Reinout has written a weblog entry about the Buildout development/production strategy that we are starting to use at Zest Software. The goal is to use the same buildout for both development and production deployment.

As the buildout should be repeatable, you may want to read the repeatable test file from the zc.buildout package.

Definitions

Now I am going to write about creating a really stable and repeatable buildout. In Reinout's story that would fit in with creating a stable.cfg. If you are not concerning yourself with development or if you have a different buildout for production deployment, this would just be the buildout.cfg file.

Let's begin with some perhaps arbitrary definitions.

With stable I mean: you run the buildout script in a directory; one year later you run buildout for the second time; you get the same result as one year earlier. Use case: by accident you removed the entire parts directory and you need to rebuild it.

With repeatable I mean: you run the buildout script in a directory; one year later you run the buildout on a completely different server; both directories are exactly the same. Use case: you want to move the zope instance of a customer to a new server.

Creating the buildout

What I write in this entry should be applicable to any buildout with or without Plone. My main focus and example is a Plone buildout though. You can fix up a current buildout of course, but here let's start with a clean default buildout for Plone 3, which you can create with paster:

paster create -t plone3_buildout test
cd test
python2.4 bootstrap.py

No newer packages

When running bin/buildout by default it picks the newest package it can find for any dependency. We can tell buildout to first look if some version of that package is already available:

[buildout]
newest = false

This way, a second call of bin/buildout will not try to get any new packages. So it will finish quickly and just keep the current packages. This already makes the buildout stable in the sense that a new run of bin/buildout in that same directory will give you no new packages.

But if you want to recreate that buildout in a different directory or even a different server, you can get other versions. For starters, currently this buildout config will give you Plone 3.0.5, but a few months from now you will probably get Plone 3.1. So this is not yet a repeatable buildout.

Simple index

As an aside, while we are in the buildout section, let's quickly specify a simpler (read: faster) index than the default CheeseShop:

[buildout]
newest = false
index = http://download.zope.org/ppix

See my brother's weblog entry on ppix instead of pypi from now on.

By the way, this has nothing to do with making your buildout more stable or repeatable.

Pinning versions

Let's go get some more stability in our buildout. For some recipes and packages there are more ways to pin versions. But none of them work in all cases, except one. So we will just use that one way:

[buildout]
...
versions = versions

[versions]
...

So in your [buildout] section you add an option versions in which you point to another section that contains the versions that packages should be pinned to.

Pinning the plone version

First things first: we pick a Plone version. We simply add a line to the [versions] section that we added above:

[versions]
plone.recipe.plone = 3.0.5

This recipe has strict versions for the Plone products and packages that make up a Plone release, so for example Products.CMFPlone, Products.CMFCore, the various plone.* packages, etcetera.

And this has a zope2-url property that tells the plone.recipe.zope2install recipe that it should use Zope 2.10.5.

So the most important versions have already been pinned now.

Knowing which versions are not pinned

Wichert Akkerman came up with the following one-liner to get a list of versions that are not pinned by you, but picked by buildout:

bin/buildout -Novvvvv |sed -ne 's/^Picked: //p' | sort | uniq

Currently this returns this list:

elementtree = 1.2.7-20070827-preview
plone.recipe.distros = 1.3
plone.recipe.zope2install = 1.2
plone.recipe.zope2instance = 1.3
python-openid = 2.0.1
setuptools = 0.6c7
zc.recipe.egg = 1.0.0

This basically means that the mentioned packages are not pinned by our buildout config, but chosen (picked) by buildout. This means that they are not stable yet: rerunning this script in a few months time will likely give you different versions.

So what should you do? Very simple: you literally add those lines to the [versions] section. Now all your packages are pinned.

Pinning extra products

You can of course also pin any extra products that you want to use in your buildout. Best is to use an official release, such as a tar ball:

[productdistros]
recipe = plone.recipe.distros
urls =
    http://plone.org/products/poi/releases/1.1/poi_1.1.tgz

or a checkout of a subversion tag:

[productcheckouts]
recipe = infrae.subversion
urls =
  http://svn.plone.org/svn/collective/eXtremeManagement/tags/1.5.2/ eXtremeManagement

If for some reason there is no tag you can use, you can still specify a revision in the url with the @ sign. Thanks to Guido Wesdorp for pointing this out:

[productcheckouts]
recipe = infrae.subversion
urls =
    http://getpaid.googlecode.com/svn/trunk/products/PloneGetPaid@1132 PloneGetPaid

For now let's keep the example simple here and remove these products again from our buildout.cfg.

Conclusions

A buildout can be made very stable: just put newest = false in your buildout section. If one year later you accidentally remove the parts directory, you can rerun buildout and get your original directory back. If you remove your eggs though, you are in trouble.

Making a buildout repeatable is not that difficult either after all. Put this in your buildout.cfg:

[buildout]
...
versions = versions

[versions]

Right below [versions] add the result of this one-liner:

bin/buildout -Novvvvv |sed -ne 's/^Picked: //p' | sort | uniq

Contrary to some earlier conclusions that I drew, this is everything that is needed to pin all packages.

For pinning old-style Products, see the section on Pinning extra products.

Bonus

plone.recipe.plone rigourously depends on specific packages and products. So what do you do if you want to use a newer version of just one or two packages? For instance, I did some fixes for plone.locking which are still not in Plone 3.0.5. And for multilingual sites you really want to use a newer version with an important bug fix. You might at first think that this would be enough:

[versions]
plone.locking = 1.0.5
plone.app.i18n = 1.0.2

But running bin/buildout then gives an error:

The version, 1.0.2, is not consistent with the requirement,
'plone.app.i18n==1.0.1'.
While:
  Installing plone.
Error: Bad version 1.0.2

So you need to add two lines to the [plone] section:

[plone]
recipe = plone.recipe.plone
eggs =
    plone.locking
    plone.app.i18n

The versions for these two packages are already specified in the recipe, but with different versions than we want. By listing those two eggs here, we tell the recipe to forget its version pinnings and be happy with any version of those two packages. After this, the pinnings for these two eggs in the [versions] section can finally take effect.

The final version

Let's end with showing what our buildout.cfg now looks like. Or actually, let's let's show a buildout that uses some earlier versions: Plone 3.0.2, older recipes, and older setuptools and elementtree. And instead of plone.app.i18n 1.0 that is required by Plone 3.0.2 we pin this package to a slightly newer version 1.0.1, which is not the latest version in the CheeseShop. This older buildout should show nicely that you can have a stable, repeatable buildout months later. Try it! So here is the file itself:

[buildout]
newest = false
index = http://download.zope.org/ppix
parts =
    instance
    zope2
    plone
    zopepy
    productdistros

find-links =
    http://dist.plone.org
    http://download.zope.org/ppix/
    http://download.zope.org/distribution/
    http://effbot.org/downloads

# Add additional eggs here
# elementtree is required by Plone
eggs =
    elementtree

versions = versions

[versions]
setuptools = 0.6c6
zc.recipe.egg = 1.0.0b6
plone.recipe.plone = 3.0.2
elementtree = 1.2.6-20050316
plone.recipe.distros = 0.3
plone.recipe.zope2install = 1.0
plone.recipe.zope2instance = 1.0
python-openid = 2.0.1
plone.app.i18n = 1.0.1

[plone]
recipe = plone.recipe.plone
eggs =
    plone.app.i18n

[zope2]
recipe = plone.recipe.zope2install
url = ${plone:zope2-url}

[productdistros]
recipe = plone.recipe.distros
urls =
nested-packages =
version-suffix-packages = 

[instance]
recipe = plone.recipe.zope2instance
zope2-location = ${zope2:location}
user = admin:
http-address = 8080
#debug-mode = on
#verbose-security = on

# If you want Zope to know about any additional eggs, list them here.
# This should include any development eggs you listed in develop-eggs above,
# e.g. eggs = ${buildout:eggs} ${plone:eggs} my.package
eggs =
    ${buildout:eggs}
    ${plone:eggs}

# If you want to register ZCML slugs for any packages, list them here.
# e.g. zcml = my.package my.other.package
zcml = 

products =
    ${productdistros:location}
    ${plone:products}

[zopepy]
recipe = zc.recipe.egg
eggs = ${instance:eggs}
interpreter = zopepy
extra-paths = ${zope2:location}/lib/python
scripts = zopepy

Creating repeatable buildouts

published Jan 19, 2008, last modified Jan 30, 2008

When you create a buildout for a production website, you want to be able to recreate that exact buildout one year from now on a different server. How do you do that? Earlier version, kept for historical reasons

I made a better, easier version of this weblog entry. Please read that instead, unless you are interested in some strange buildout errors that can occur when you are doing things a bit too difficult, like I am doing here.

When you create a buildout for a production website, you want to be able to recreate that exact buildout one year from now on a different server. How do you do that?

Background

I assume you know what buildout is, otherwise you would probably not be reading this. Martin Aspeli has written a tutorial on Managing projects with zc.buildout. This should be your first stop for learning more about buildout, at least if you are a Plone developer.

My brother and colleague Reinout has written a weblog entry about the Buildout development/production strategy that we are starting to use at Zest Software. The goal is to use the same buildout for both development and production deployment.

As the buildout should be repeatable, you may want to read the repeatable test file from the zc.buildout package.

Definitions

Now I am going to write about creating a really stable and repeatable buildout. In Reinout's story that would fit in with creating a stable.cfg. If you are not concerning yourself with development or if you have a different buildout for production deployment, this would just be the buildout.cfg file.

Let's begin with some perhaps arbitrary definitions.

With stable I mean: you run the buildout script in a directory; one year later you run buildout for the second time; you get the same result as one year earlier. Use case: by accident you removed the entire parts directory and you need to rebuild it.

With repeatable I mean: you run the buildout script in a directory; one year later you run the buildout on a completely different server; both directories are exactly the same. Use case: you want to move the zope instance of a customer to a new server.

Creating the buildout

What I write in this entry should be applicable to any buildout with or without Plone. My main focus and example is a Plone buildout though. You can fix up a current buildout of course, but here let's start with a clean default buildout for Plone 3, which you can create with paster:

paster create -t plone3_buildout test
cd test
python2.4 bootstrap.py

No newer packages

When running bin/buildout by default it picks the newest package it can find for any dependency. We can tell buildout to first look if some version of that package is already available:

[buildout]
newest = false

This way, a second call of bin/buildout will not try to get any new packages. So it will finish quickly and just keep the current packages. This already makes the buildout stable in the sense that a new run of bin/buildout in that same directory will give you no new packages.

But if you want to recreate that buildout in a different directory or even a different server, you can get other versions. For starters, currently this buildout config will give you Plone 3.0.5, but a few months from now you will probably get Plone 3.1. So this is not yet a repeatable buildout.

Simple index

As an aside, while we are in the buildout section, let's quickly specify a simpler (read: faster) index than the default CheeseShop:

[buildout]
newest = false
index = http://download.zope.org/ppix

See my brother's weblog entry on ppix instead of pypi from now on.

By the way, this has nothing to do with making your buildout more stable or repeatable.

Pinning the plone version

Let's go get some more stability in our buildout. First things first: we pick a Plone version. Change this:

recipe = plone.recipe.plone

into this:

recipe = plone.recipe.plone == 3.0.5

This recipe has strict versions for the Plone products and packages that make up a Plone release, so for example Products.CMFPlone, Products.CMFCore, the various plone.* packages, etcetera.

And this has a zope2-url property that tells the plone.recipe.zope2install recipe that it should use Zope 2.10.5.

So the most important versions have been pinned now.

Pinning extra products

You can of course also pin any extra products that you want to use in your buildout. Best is to use an official release, such as a tar ball:

[productdistros]
recipe = plone.recipe.distros
urls =
    http://plone.org/products/poi/releases/1.1/poi_1.1.tgz

or a checkout of a subversion tag:

[productcheckouts]
recipe = infrae.subversion
urls =
  http://svn.plone.org/svn/collective/eXtremeManagement/tags/1.5.2/ eXtremeManagement

If for some reason there is no tag you can use, you can still specify a revision in the url with the @ sign. Thanks to Guido Wesdorp for pointing this out:

[productcheckouts]
recipe = infrae.subversion
urls =
    http://getpaid.googlecode.com/svn/trunk/products/PloneGetPaid@1132 PloneGetPaid

You can of course also pin the eggs of packages that you need, which we will see below. For now let's keep the example simple here and remove these products again from our buildout.cfg.

Knowing which versions are not pinned

Wichert Akkerman came up with the following one-liner to get a list of versions that are not pinned by you, but picked by buildout:

bin/buildout -Novvvvv |sed -ne 's/^Picked: //p' | sort | uniq

Currently this returns this list:

elementtree = 1.2.7-20070827-preview
plone.recipe.distros = 1.3
plone.recipe.zope2install = 1.2
plone.recipe.zope2instance = 1.3
python-openid = 2.0.1
setuptools = 0.6c7
zc.recipe.egg = 1.0.0

This basically means that the mentioned packages are not pinned by our buildout config, but chosen (picked) by buildout. This means that they are not stable yet: rerunning this script in a few months time will likely give you different versions.

Pinning a package

We start easily enough by pin elementtree. Instead of this:

eggs =
    elementtree

we write this:

eggs =
    elementtree == 1.2.7-20070827-preview

As an aside, note that the top directive eggs has one equals sign after it and the option elementtree below it has a double equals sign. The point is that in the options you write a test, so you could also choose greater than (>=) or smaller than (<=). But we want stability, so we keep the double equals sign.

If you now run bin/buildout and then run that one-liner from Wichert again you will now see that the elementtree line has disappeared from the output: buildout no longer picks that version for us as we have pinned it. We are getting closer to stability!

Timeout

At this point it is good to say that you could stop here: all Zope and Plone products and packages are pinned, which is the most important. Does it really matter if one year from now plone.recipe.zope2instance is at version 2.0 instead of 1.3? Probably not. But there is still room for more stability and repeatability. So if you are still interested, let's continue our quest.

Pinning recipes

Now we start pinning the other recipes that are in the output. Find these four lines in your buildout.cfg (they will not be right below each other):

recipe = plone.recipe.zope2install
recipe = plone.recipe.distros
recipe = plone.recipe.zope2instance
recipe = zc.recipe.egg

and pin them to specific versions:

recipe = plone.recipe.zope2install == 1.2
recipe = plone.recipe.distros == 1.3
recipe = plone.recipe.zope2instance == 1.3
recipe = zc.recipe.egg == 1.0.0

Now we run the one-liner again and get:

plone.recipe.distros = 1.3
python-openid = 2.0.1
setuptools = 0.6c7
zc.recipe.egg = 1.0.0

This may not be what you had expected. We have pinned plone.recipe.distros and zc.recipe.egg right? That is correct, but look at this partial output of bin/buildout with the verbose option:

$ bin/buildout -v
Installing 'plone.recipe.plone == 3.0.5'.
We have the distribution that satisfies 'plone.recipe.plone==3.0.5'.
Getting required 'plone.recipe.distros'
  required by plone.recipe.plone 3.0.5.
Picked: plone.recipe.distros = 1.3
Getting required 'zc.recipe.egg'
  required by plone.recipe.plone 3.0.5.
Picked: zc.recipe.egg = 1.0.0

What happens here is that plone.recipe.plone depends on two other recipes. They are in the install_requires option of the setup.py file of the recipe. So buildout itself picks a version of those dependencies. It does not look at our pinnings at it is handling the [plone] part of buildout.cfg now and not the [productdistros] or [zopepy] parts that try to pin versions for those recipes.

In fact, you can get a conflict with this. Temporarily we try to pin plone.recipe.distros to an earlier version:

[productdistros]
recipe = plone.recipe.distros == 0.3

We run bin/buildout:

While:
  Installing.
  Getting section productdistros.
  Initializing section productdistros.
  Loading recipe 'plone.recipe.distros == 0.3'.

An internal error occured due to a bug in either zc.buildout or in a
recipe being used:

VersionConflict:
(plone.recipe.distros 1.3
 (.../plone.recipe.distros-1.3-py2.4.egg),
 Requirement.parse('plone.recipe.distros==0.3'))

In this case buildout already picked version 1.3 and now we tell it that we require 0.3 so this conflicts. How do we solve that? We change the order in which the buildout parts are executed. Currently it is this:

[buildout]
parts =
    plone
    zope2
    productdistros
    instance
    zopepy

Now we make sure that productdistros and zopepy are above plone:

[buildout]
parts =
    productdistros
    zopepy
    plone
    zope2
    instance

We run bin/buildout -v again:

Uninstalling productdistros.
While:
  Installing.
  Uninstalling productdistros.
  Loading recipe 'plone.recipe.distros == 1.3'.

An internal error occured due to a bug in either zc.buildout or in a
recipe being used:

VersionConflict:
(plone.recipe.distros 0.3
 (.../plone.recipe.distros-0.3-py2.4.egg),
 Requirement.parse('plone.recipe.distros==1.3'))

Oops. Here we are uninstalling version 1.3 as that was installed by the previous buildout run and at the same time we want to install 0.3. buildout does not like this. Only solution I know: remove the .installed.cfg file that is automatically created by buildout. The function of that file is to keep track of what buildout has previously installed, so it knows if it should do any uninstalling or reinstalling or if it can just do nothing and finish within one second. In other words: removing that file should be okay. Anyway, you are keeping backups, right?

Five minutes later...

Right, we are keeping backups. We remove that .installed.cfg file and run bin/buildout again. This will take a bit longer now, as among others it compiles Zope again. At least it completes without conflicts now, and plone.recipe.distros is not picked by buildout anymore, but pinned by us.

For some reason zc.recipe.egg is still picked though. Ah, the [zopepy] part that pins this recipe contains this line:

extra-paths = ${zope2:location}/lib/python

So this part depends on the [zope2] part, which in turn depends on the [plone] part, which in turn depends on the zc.recipe.egg package, which is therefore picked by buildout, ignoring our pinning.

Wonderful.

Okay, seems like we have to resort to trickery. We introduce a new section [dummy-for-pinning]:

[dummy-for-pinning]
recipe = zc.recipe.egg == 1.0.0
eggs = zc.recipe.egg == 1.0.0

Yes, I tried this and you need the version number in both lines. Now we simply put that section at the top of the buildout parts. And we move the [zopepy] section down, as it depends on those other parts anyway. So our parts now looks like this:

parts =
    dummy-for-pinning
    productdistros
    plone
    zope2
    instance
    zopepy

Pinning the rest

What does the one-liner tell us?:

$ bin/buildout -Novvvvv |sed -ne 's/^Picked: //p' | sort | uniq
python-openid = 2.0.1
setuptools = 0.6c7
zc.buildout = 1.0.0

zc.buildout is a new one. This is a requirement of zc.recipe.egg, though I could not say why it did not end up in that list earlier. We can add all three remaining picked packages to our [dummy-for-pinning] section so it now looks like this:

[dummy-for-pinning]
recipe = zc.recipe.egg == 1.0.0
eggs =
    zc.recipe.egg == 1.0.0
    zc.buildout == 1.0.0
    setuptools == 0.6c7
    python-openid == 2.0.1

After this we hit the end of the road. zc.buildout is pinned, but setuptools and python-openid are still picked for us by buildout. The last possibility I can think of is adding those two to a [versions] section, like this:

[versions]
setuptools = 0.6c7
python-openid = 2.0.1

But this has no noticeable effect. In fact, when I change the setuptools version to 0.6c6 (so 6 instead of 7) in both cases, I get this output from buildout:

Installing dummy-for-pinning.
Installing 'zc.recipe.egg == 1.0.0', 'zc.buildout == 1.0.0', 'setuptools == 0.6c6', 'python-openid == 2.0.1'.
We have the distribution that satisfies 'zc.recipe.egg==1.0.0'.
We have the distribution that satisfies 'zc.buildout==1.0.0'.
We have the distribution that satisfies 'setuptools==0.6c6'.
We have the distribution that satisfies 'python-openid==2.0.1'.
...
Getting required 'setuptools'
  required by five.customerize 0.2.
  ...
  required by plone.app.kss 1.2.5.
Picked: setuptools = 0.6c7

So our pinning of setuptools is ignored. The same is true for python-openid:

Getting required 'python-openid>=2.0.0,<2.0.999'
  required by plone.openid 1.0.1.
Picked: python-openid = 2.0.1

One last resort: introduce a dummy package that requires a specific version of those two packages in its setup.py. We add it to our buildout and see that it gets used correctly:

We have the distribution that satisfies 'zest.recipe.dummy==0.3'.
Getting required 'python-openid==2.0.1'
  required by zest.recipe.dummy 0.3.
We have the distribution that satisfies 'python-openid==2.0.1'.
Getting required 'setuptools==0.6c6'
  required by zest.recipe.dummy 0.3.
We have the distribution that satisfies 'setuptools==0.6c6'.

But further on in the buildout nothing has changed and buildout still picks its own versions.

One final twist though: I removed both versions of setuptools from the eggs directory. I then ran buildout again. It again fetched version 6 and then claimed to have picked version 7, but the only package that I could actually find, was version 6. And the second time I ran buildout, it already had version 6, so it no longer picked version 7, but version 6.

Are you still with me? :-) Then maybe just a few more random notes before we get to the conclusions.

  • Try this: create a new buildout with paster. Do the usual python2.4 bootstrap.py. You will now have downloaded version 7 of setuptools and version 1.0.0 of zc.buildout. The task of persuading the bootstrap to give you earlier (or later) versions of those two packages is left as an exercise to the reader. Assignments are due next week.
  • What I had not realized yet: what you put in the [versions] section influences what goes in the bin/buildout script itself. I have not looked at what the end-effect here is.
  • After some of these changes I have seen bin/buildout finish succesfully and then found that the bin/buildout file was gone. Running python2.4 bootstrap.py fixes that of course, but it is strange.

Conclusions

A buildout can be made very stable: just put newest = false in your buildout section. If one year later you accidentally remove the parts directory, you can rerun buildout and get your original directory back. If you remove your eggs though, you are in trouble.

Making a buildout repeatable is more difficult. The first easy step is pinning the plone.recipe.plone package. Any extra packages or products can be pinned quite easily as well.

What is difficult is pinning dependencies. If you do not do this correctly, your pinning will cause a conflict. The order in which the buildout parts or sections get executed is important here.

You will have to resort to trickery to get the last few packages pinned down. And even then it does not seem possible to pin really everything. But we are very close and I think there is a good chance that the remaining issues will get solved in buildout, setuptools or easy_install.

Bonus

plone.recipe.plone rigourously depends on specific packages and products. So what do you do if you want to use a newer version of just one or two packages? For instance, I did some fixes for plone.locking which are still not in Plone 3.0.5. And for multilingual sites you really want to use a newer version with an important bug fix. You might at first think that this would be enough:

[plone]
recipe = plone.recipe.plone == 3.0.5
eggs =
    plone.locking == 1.0.5
    plone.app.i18n == 1.0.2

But running bin/buildout then gives an error:

ValueError:
('Missing distribution spec', '==')

This is how you accomplish that:

[versions]
plone.locking = 1.0.5
plone.app.i18n = 1.0.2

[plone]
recipe = plone.recipe.plone
eggs =
    plone.locking
    plone.app.i18n

To start at the bottom: these lines basically erase the version pinning of those two packages that is in the recipe. After that, the pinning in the [versions] section can take effect.

But in fact, nothing happens just yet. This is because we are running buildout in non-newest mode (newest = false in the [buildout] section). But here we actually do not want stability: we want newer versions! So we run buildout in the newest mode, either by temporarily setting newest = true or by calling bin/buildout -n once. Now we get the new packages that we want. Since we have pinned almost everything else, this should be quite safe.

The final version

Let's end with showing what our buildout.cfg now looks like. Note here that the mention of setuptools, zc.buildout and python-openid in [versions] or [dummy-for-pinning] may not be too useful. And I will not fault you if you skip that [dummy-for-pinning] section entirely. The reported picked versions are now:

plone.app.i18n = 1.0.2
plone.locking = 1.0.5
python-openid = 2.0.1
setuptools = 0.6c7

And here is the file itself:

[buildout]
newest = false
index = http://download.zope.org/ppix
parts =
    dummy-for-pinning
    productdistros
    plone
    zope2
    instance
    zopepy

find-links =
    http://dist.plone.org
    http://download.zope.org/ppix/
    http://download.zope.org/distribution/
    http://effbot.org/downloads

# Add additional eggs here
# elementtree is required by Plone
eggs =
    elementtree == 1.2.7-20070827-preview

[dummy-for-pinning]
recipe = zc.recipe.egg == 1.0.0
eggs =
    python-openid == 2.0.1
    setuptools == 0.6c7
    zc.buildout == 1.0.0
    zc.recipe.egg == 1.0.0

[versions]
setuptools = 0.6c7
zc.buildout = 1.0.0
plone.locking = 1.0.5
plone.app.i18n = 1.0.2

[plone]
recipe = plone.recipe.plone == 3.0.5
eggs =
    plone.locking
    plone.app.i18n

[zope2]
recipe = plone.recipe.zope2install == 1.2
url = ${plone:zope2-url}

[productdistros]
recipe = plone.recipe.distros == 1.3
urls =
nested-packages =
version-suffix-packages = 

[instance]
recipe = plone.recipe.zope2instance == 1.3
zope2-location = ${zope2:location}
user = admin:
http-address = 8080
#debug-mode = on
#verbose-security = on

# If you want Zope to know about any additional eggs, list them here.
# This should include any development eggs you listed in develop-eggs above,
# e.g. eggs = ${buildout:eggs} ${plone:eggs} my.package
eggs =
    ${buildout:eggs}
    ${plone:eggs}

# If you want to register ZCML slugs for any packages, list them here.
# e.g. zcml = my.package my.other.package
zcml = 

products =
    ${buildout:directory}/products
    ${productdistros:location}
    ${plone:products}

[zopepy]
recipe = zc.recipe.egg == 1.0.0
eggs = ${instance:eggs}
interpreter = zopepy
extra-paths = ${zope2:location}/lib/python
scripts = zopepy

Search forms with zope formlib and batching

published Nov 23, 2007, last modified Jan 30, 2008

Combine a search form and a results page by using zope.formlib and plone.app.content.batching.

In this tutorial we want to have a search form on our web site. After a visitor fills in the form he should see a page with results. But on that same page he should see the search form again, with his previous values filled in. On top of that, the results should be batched, displaying only the first ten results and having links to the next pages. On Plone 3.0 you can do this by combining zope.formlib and plone.app.content.batching.

Some details are left out, like explaining how files and classes are called and where they are located (most are in the browser/ directory), or not showing some methods whose goal and code are hopefully clear enough from their name. The idea is not to make this too long and I am failing at that already. :-) If I omitted too much for your taste and you have a specific question, just ask.

Building search forms

We start with a browser view. On the top we need some imports:

from zope import schema
from zope.interface import Interface
from zope.interface import implements
from zope.formlib import form
from Products.Five.formlib import formbase

With our search form we want to search for some searchable text and for Products of a specific Brand. Brand is a content type we (Zest Software) made for a customer. A Product is another content type, which is added to a Brand. We made a vocabulary for this which maps the title of a Brand (shown in the form) to the path where this Brand is found.

We define an Interface for our search form. Very important: let the parameters match the names of the catalog indices that you want to search on:

class ISearchForm(Interface):
    """These are the fields of our search form.
    """
    path = schema.Choice(title = _(u'Brand'),
        vocabulary=u"customer.brand",
        description = _(u'Brand, mark, formula.'),
        required = False,
    )
    SearchableText = schema.TextLine(title = _(u'Search word'),
        description = _(u'Search for specific words.'),
        required = False,
    )

For schema.Choice the default widget is a SelectWidget or RadioWidget. As empty value it displays "(no value)" in the drop down box. At least for a Dutch web site this is not very nice, as there is no translation. You can provide an own version of that message by doing something like this:

from customer.product import customerMessageFactory as _
from zope.app.form.browser.itemswidgets import SelectWidget
SelectWidget._messageNoValue = _("vocabulary-missing-single-value-for-edit",
                      "Selecteer indien gewenst een waarde.")

Or do this in English and use i18ndude of course.

Now write a class that inherits from:

  • Products.Five.formlib.formbase.PageForm to have a complete page.
  • Products.Five.formlib.formbase.SubPageForm to get a partial page.

In our case the search form will be only part of a page, so we choose the second option:

class Search(formbase.SubPageForm):
    implements(ISearchForm)
    form_fields = form.FormFields(ISearchForm)
    label = _(u"Working independently")
    description = _(u"You are looking for a product for:")

Add an action button to that class:

@form.action(_(u"Search"))
def action_search(self, action, data):
    """Perform search.
    """

On a SubFormPage this can actually be empty (pass): the surrounding form tag that you have to supply yourself takes care of redirecting. For a complete FormPage you would at least need something like this:

self.request.response.redirect(target)

where target should be a page with search results.

Search form and results on one page

Hook up your browser view in configure.zcml. We only register this view for the Site Root in this case:


Same for a results page:


And the main page showing search plus results:


Use something like CMFPlone/skins/plone_content/document_view.pt as base and add the structure of the search and results subpages:





...

        
...

Overwriting the search form template

If the default template from zope.formlib does not give you what you want, you overwrite it. Configure some named adapters in zcml:



The factory for the first adapter is in zope.formlib. Our own alternative factory is this:

from zope.formlib import interfaces, namedtemplate
from zope.app.pagetemplate import ViewPageTemplateFile
subpage_template = namedtemplate.NamedTemplateImplementation(
    ViewPageTemplateFile('searchform.pt'), interfaces.ISubPageForm)

Let your class know it should use the new template:

class Search(formbase.SubPageForm):
    ...
    original_template = namedtemplate.NamedTemplate('default.subform')
    template = namedtemplate.NamedTemplate('customer.search')

Now write the template, using the original template via the original_template attribute from the view. In this case we add the form description in a slot:


  

A nice description

Remembering form values

The form now submits to itself. Currently its values are forgotten. So we fix that:

from zope.formlib.form import setUpWidgets

class Search(formbase.SubPageForm):
    ...
    # We set an empty prefix, otherwise we would end up with
    # 'form.SearchableText', etc, in the request while the code here and in
    # results.py expects 'SearchableText'.
    prefix = ''

  def setUpWidgets(self, ignore_request=False):
      """From zope.formlib.form.Formbase.
      
      Difference: pass extra data=self.request.form.
      """
      self.adapters = {}
      self.widgets = setUpWidgets(
          self.form_fields, self.prefix, self.context, self.request,
          form=self, adapters=self.adapters, ignore_request=ignore_request,
          data=data)

While we are here, let's fix a possible unicode error. There is a difference between getting a value from the request or from request.form when unicode is involved:

>>> request.form.get('SearchableText')
u'\xff'
>>> request.get('SearchableText')
'\xc3\xbf'

In the method zope.app.form.browser.textwidgets.TextWidget._getFormInput() the value is fetched from the request and not request.form. So you get a UnicodeDecodeError. We need to guard against that:

def setUpWidgets(self, ignore_request=False):
    """From zope.formlib.form.Formbase.
    """
    self.adapters = {}
    fieldnames = [x.__name__ for x in self.form_fields]
    data = {}
    for key in fieldnames:
        value = self.request.form.get(key)
        if value is not None and not value == u'':
            data[key] = value
            self.request[key] = value
    
    self.widgets = setUpWidgets(
        self.form_fields, self.prefix, self.context, self.request,
        form=self, adapters=self.adapters, ignore_request=ignore_request,
        data=data)

Batching on the results page

The search form is working fine now. So we move on the the search results. The results template can be quite simple: a ul with an li for every search result if that is enough for you. The only interesting part is batching:

...

So we need a view with attributes has_results and batching. This is the base of the browser view:

from plone.app.content.batching import Batch
class IResults(Interface):
    """List of Products.
    """

    has_results = Attribute("The search has matching results.")
    items = Attribute("List of Products")
    batch = Attribute("Batch of brains")
    url = Attribute("URL for this context")


class Results(BrowserView):
    implements(IResults)

    # We want to use the batching.pt file from plone.app.content, but
    # that has a few drawbacks so we copied it ourselves.
    batching = ViewPageTemplateFile('batching.pt')

We overwrite it because it has one flaw. It has a question mark where we want an ampersand, so we can add more options in the url. It was:

tal:attributes="href string:${view/url}?pagenumber=${batch/previouspage}&sort_on=${request/sort_on|string:getObjPositionInParent}">

and we change that to:

tal:attributes="href string:${view/url}&pagenumber=${batch/previouspage}&sort_on=${request/sort_on|string:getObjPositionInParent}">

This change means we can add our filled in search terms (path, SearchableText) to the url method of the view:

@property
def url(self):
    """Base url, needed by the batching template."""
    url = self.context.absolute_url()
    terms = ["%s=%s" % (key, value) for key, value in self.search_filter.items()]
    query = '&'.join(terms)
    return url + '?' + query

We add a search_filter to our view that gets the filled in values from our search form:

@property
@memoize
def search_filter(self):
    """Construct search filter.

    Only add valid search terms from the request.
    """
    context = aq_inner(self.context)
    form = self.request.form
    search_filter = {}
    for key in ['path', 'SearchableText']:
        value = form.get(key)
        if value is not None and not value == u'':
            search_filter[key] = value
    # When viewing a Brand, add its path to the filter.
    if search_filter.get('path') is None:
        if IBrand.providedBy(context):
            search_filter['path'] = '/'.join(context.getPhysicalPath())
    return search_filter

Incidentally, our search form and results will also be used on the view of a Brand, which is why the last few lines were added.

Our view needs a batch property:

@property
def batch(self):
    """Batch of Products (brains).
    """
    context = aq_inner(self.context)
    # We only search for Products.  We need to copy the
    # search_filter as we need it original contents somewhere
    # else, without the portal_type.
    search_filter = self.search_filter.copy()
    search_filter['portal_type'] = 'Product'
    catalog = getToolByName(context, 'portal_catalog')
    brains = catalog.searchResults(search_filter)
    batch = Batch(items=brains, pagesize=10,
                  pagenumber=self.pagenumber, navlistsize=5)
    return batch

We use that batch in the shown batching.pt template, but also in our own view, as we do not want to pass brains to our template, but a nice list of dictionaries:

def items(self):
    """List of Products.

    Here we get some info for all the Products that are in the
    current page of the batch.
    """

    batch = self.batch
    items = []
    for brain in batch:
        product = brain.getObject()
        info = dict(
            url=product.absolute_url(),
            title=product.Title(),
            isbn=product.getIsbn(),
            )
        items.append(info)        
    return items

There you have it: a search form with batched results on the same page. And all thanks to the authors of zope.formlib and plone.app.content.batching!