Personal tools
plone.app.registry: use it and love it

STORING COMPLEX DATA HAS NEVER BEEN SO SIMPLE

Jul 23, 2012

plone.app.registry: use it and love it

Once upon a time there was Plone; it used to store its configuration inside a ZMI repository called "Plone Properties Tool"... But time has passed and now Plone is moving to plone.app.registry

First of all, let's start reading the plone.app.registry description, or better, the plone.registry ones:

A debconf-like (or about:config-like) registry for storing application settings

I think this is great: I really like the about:config page on my Firefox!

An about:config screenshot

This means that there is a unified configuration panel for storing "data", in the most general way. Let me show you some examples:

  • If your application need to store a secret API key...
  • When your product must store some general installation preference...
  • If your site need to store something that isn't content specific...

...Let's use the registry! Ok, I think you got it. Now, let's see how it works.

 

 

Quick start

What you'll surely love of plone.app.registry is that you only need to think about data name and data type of what you want to store (as always in Plone, this mean designing the interface through zope.schema).

class IYourConfiguration(Interface):
    first_data = schema.Text(
            title=_(u"First data needed"),
            required=True,
    )
    second_data = schema.Tuple(
            title=_(u'JavaScript to be included when an error message is get'),
            required=False,
    )

Then Plone(.app.registry) will generate for you a proper user interface through z3c.form (another Plone piece that you'll hate love).

Anyway, I don't want to digress further. The plone.app.registry documentation is quite clear and there is plenty of examples on web.

Drawbacks

I have to admit it, I wasn't a plone.registry enthusiast. I mean, it's a great concept, but when you are using your about:config Mozilla configuration, you don't have to care about all the garbage that a Firefox plugin can leave behind: you can always go there and remove entries manually.

This is not always true with the Plone registry. You have to provide a way to clean things up or configuration will stay there forever. But thing can get even worst: if you are storing complex data types in the registry, you'll need an uninstall procedure that cleans all up, or your registry may be broken.

One thing I've always liked is the easiness of using simple Property Sheet through ZMI (but also nimbly addable using Python or Generic Setup). Those tools are part of Zope and perfectly integrated in ZMI, so a site Manager can always go there and add, update, delete items, so cleaning up is simple.

ZMI: manage properties sheet

To be brutally honest, using ZMI properties is nearer to the idea of about:config Mozilla-like configuration we know.

However we can't ignore that:

  • Plone is moving on!
  • plone.app.registry is giving you a Plone-level interface
  • Site Administrator role can't reach ZMI for manage Zope properties
  • ZMI properties can't store complex data (only primitive types are handled)
ComplexObviously you can manually create a Plone user interface to handle Zope properties (yes, you can easily do it using again z3c.form), but there's a great feature of plone.app.registry+z3c.form that can really make the difference: building automatically complex configuration interfaces.

Going complex

Recently, in one of our products we needed to store a complex configuration: site administrators had to be able to add a collection of configurations couples. A single set is done by a simple string of information and a long text:

class IErrorCodeValuePair(Interface):
    message = schema.ASCIILine(title=_(u"Error message"), required=True)
    message_snippet = schema.SourceText(title=_(u"Code to include"), required=False)

... then you have to write down the configuration registry's interface almost like this:

class IAnalyticsSettings(Interface):
...
    error_specific_code = schema.Tuple(
        ...
    )
...

Now we need to specify what type of element is inside your tuple (in facts: form of form). The great news is that with z3c.form you can easily create a form that handle this kind of complex data!

class IAnalyticsSettings(Interface):
...
    error_specific_code = schema.Tuple(
        title=_(u'...'),
        ....
        value_type=schema.Object(IErrorCodeValuePair, title=u"..."),
    )

This will automatically build a form where you can add sub-elements inside, where a single sub-element is composed by the definition of the sub-interface.

Simple z3c.form rendering example

Please note that for now I'm talking about z3c.form only (we are building simply a form, then later on we'll see how to manage what happens when the user press the save button).

Difficulties arise when you need to store those complex values inside the Plone registry. Plone registry explicitly don't want to support the storage of complex data. Motivation of this is one of the drawbacks: if you'll remove the product that own the sub-interface later, your registry will be broken.

Although, keep in mind that saying plone.registry doesn't support this, it doesn't mean that you can't do it. After looking deeply at z3c.form and plone.registry code, and asking on Stackoverflow, I've found a way.

Store complex data in the Plone registry

Let's see how to reach this task.

class IAnalyticsSettings(Interface):
...
    error_specific_code = schema.Tuple(
            title=_(u'...'),
            value_type=PersistentObject(IErrorCodeValuePair, title=_(u"...")),
            required=False,
            default=(),
            missing_value=(),
    )

Please note thata the PersistentObject reference is not part of Plone. As said above, you must explicitly define it:

from plone.registry.field import PersistentField
...
class PersistentObject(PersistentField, schema.Object):
    pass

Another important task is to use the registerFactoryAdapter function of z3c.form.object module:

from z3c.form.object import registerFactoryAdapter
...
class ErrorCodeValuePair(object):
    implements(IErrorCodeValuePair)
registerFactoryAdapter(IErrorCodeValuePair, ErrorCodeValuePair)

That's it. Now you can enjoy your super-complex-form in the Plone registry.

Clean up, Clean up, Clean up!

Don't forget what you are doing... you are storing complex alien data in the Plone registry. It's really important to clean things up later, if you (or your users) want to remove your product. This is the difference between love and hate.

Don't leave garbage behind!

This way of cleaning up the registry also works on Plone 3, with plone.app.registry 1.0b1

So: how to clean things up? It's quite simple (but not so clear reading the documentation of plone.app.registry): you simply need to use Generic Setup.

As usual, register your uninstall profile. Then provide to it a registry.xml file with a content like this:

<registry>
    ...    
    <record interface="collective.analyticspanel.interfaces.IAnalyticsSettings"
	    field="error_specific_code"
            delete="True"/>
    ...
</registry>

This code will remove your data from the Plone registry, then you can safely remove the product.

Migration

This is the last problem we need to handle: changes inside the interface definition. What happens if a new release of your product need to add another field to the sub-interface?

Going back to out previous example, it should be something like this:

class IErrorCodeValuePair(Interface):
    message = schema.ASCIILine(title=_(u"Error message"), required=True)
    message_snippet = schema.SourceText(title=_(u"Code to include"), required=False)
newfield = schema.Bool(title=_(u"..."), required=False, default=True)

What can the problem be? You stored inside the registry a persistent Object that is not providing the new boolean attribute.

Thankfully, Plone (Plone 3 also) simply shows you an error message when you enter the registry form, so that you are still able to delete old entries manually and add theme from scratch (not very funny for your users).

The best thing you can do is provide a good upgrade to your product, that fix old entries. This is really simple. Follow this example of a proper upgrade step to version 0.2 of your product.

    registry = queryUtility(IRegistry)
    settings = registry.forInterface(IAnalyticsSettings, check=False)
    for config in settings.error_specific_code:
        if not hasattr(config, 'newfield'):
            config.newfield = True
            logger.info('Added new boolean property "newfield"')
    logger.info('Migrated to version 0.2')

As a conclusion, I can say that...

Goodbye...now that I have such an easy way to store complex data in my Plone sites without blowing up everything, I see absolutely no reasons not to use plone.app.registry for storing everything I need!

Goodbye property sheet... keep in touch.

comments powered by Disqus