==================================================
plone.supermodel: content schemata loaded from XML
==================================================
This package allows content schemata to be read and written as XML. It has a
standard importer and serialiser for interfaces that contain zope.schema
fields. The format is general enough to be able to handle future fields
easily, so long as they are properly specified through interfaces.
Parsing and serializing simple schemata
---------------------------------------
Before we can begin, we must register the field handlers that know how to
import and export fields from/to XML. These are registered as named utilities,
and can be loaded from the configure.zcml file of plone.supermodel.
>>> configuration = """\
...
...
...
...
...
...
...
... """
>>> from StringIO import StringIO
>>> from zope.configuration import xmlconfig
>>> xmlconfig.xmlconfig(StringIO(configuration))
Next, let's define a sample model with a single, unnamed schema.
>>> schema = """\
...
...
...
...
... Title
... True
...
...
...
... Description
... A short summary
... False
... 10
...
...
...
... """
We can parse this model using the loadString() function:
>>> from plone.supermodel import loadString
>>> model = loadString(schema)
This will load one schema, with the default name u"":
>>> model.schemata.keys()
[u'']
We can inspect this schema and see that it contains zope.schema fields with
attributes corresponding to the values set in XML.
>>> schema = model.schema # shortcut to model.schemata[u""]
>>> from zope.schema import getFieldNamesInOrder
>>> getFieldNamesInOrder(schema)
['title', 'description']
>>> schema['title'].title
u'Title'
>>> schema['title'].required
True
>>> schema['description'].title
u'Description'
>>> schema['description'].description
u'A short summary'
>>> schema['description'].required
False
>>> schema['description'].min_length
10
If we try to parse a schema that has errors, we'll get a useful
SupermodelParseError that includes contextual information. (This requires
lxml.)
>>> schema = """\
...
...
...
...
...
...
...
... """
>>> loadString(schema)
Traceback (most recent call last):
...
SupermodelParseError: Field type aint_gonna_exist specified for field title is not supported
File "", line ...
In addition to parsing, we can serialize a model to an XML representation:
>>> from plone.supermodel import serializeModel
>>> print serializeModel(model) # doctest: +NORMALIZE_WHITESPACE
TitleA short summary10FalseDescription
Building interfaces from schemata
---------------------------------
Above, we saw how to parse a schema from a file directly. Next, let's see how
this can be used more practically to define a custom interface. Here, we will
use two schemata in one file.
>>> schema = """\
...
...
...
...
... Title
... True
...
...
... Body text
... True
... 10000
...
...
...
...
...
... Created date
... False
...
...
... Creator
... Name of the creator
... True
...
...
...
...
... """
Ordinarily, this would be in a file in the same directory as the module
containing the interface being defined. Here, we need to create a temporary
directory.
>>> import tempfile, os.path, shutil
>>> tmpdir = tempfile.mkdtemp()
>>> schema_filename = os.path.join(tmpdir, "schema.xml")
>>> schema_file = open(schema_filename, "w")
>>> schema_file.write(schema)
>>> schema_file.close()
We can define interfaces from this using a helper function:
>>> from plone.supermodel import xmlSchema
>>> ITestContent = xmlSchema(schema_filename)
Note: If the schema filename is not an absolute path, it will be found
relative to the module where the interface is defined.
After being loaded, the interface should have the fields of the default
(unnamed) schema:
>>> getFieldNamesInOrder(ITestContent)
['title', 'body']
We can also use a different, named schema:
>>> ITestMetadata = xmlSchema(schema_filename, schema=u"metadata")
>>> getFieldNamesInOrder(ITestMetadata)
['created', 'creator']
Of course, a schema can also be written to XML. Either, you can build a model
dict as per the serializeModel() method seen above, or you can write a model
of just a single schema using serializeSchema():
>>> from plone.supermodel import serializeSchema
>>> print serializeSchema(ITestContent) # doctest: +NORMALIZE_WHITESPACE
Title10000Body text
>>> print serializeSchema(ITestMetadata, name=u"metadata") # doctest: +NORMALIZE_WHITESPACE
FalseCreated dateName of the creatorCreator
Finally, let's clean up the temporary directory.
>>> shutil.rmtree(tmpdir)
Base interface support
----------------------
When building a schema interface from XML, it is possible to specify a base
interface. This is analogous to "subclassing" an existing interface. The XML
schema representation can override and/or extend fields from the base.
For the purposes of this test, we have defined a dummy interface in
plone.supermodel.tests. We can't define it in the doctest, because the import
resolver needs to have a proper module path. The interface looks like this
though:
class IBase(Interface):
title = zope.schema.TextLine(title=u"Title")
description = zope.schema.TextLine(title=u"Description")
name = zope.schema.TextLine(title=u"Name")
In real life, you'd more likely have a dotted name like
my.package.interfaces.IBase, of course.
Then, let's define a schema that is based on this interface.
>>> schema = """\
...
...
...
...
... Description
... A short summary
...
...
... Age
...
...
...
... """
Here, notice the use of the 'based-on' attribute, which specifies a dotted
name to the base interface. It is possible to specify multiple interfaces
as a space-separated list. However, if you find that you need this, you
may want to ask yourself why. :) Inside the schema proper, we override the
'description' field and add a new field, 'age'.
When we load this model, we should find that the __bases__ list of the
generated interface contains the base schema.
>>> model = loadString(schema)
>>> model.schema.__bases__
(, )
The fields of the base interface will also be replicated in the new schema.
>>> getFieldNamesInOrder(model.schema)
['title', 'description', 'name', 'age']
Notice how the order of the 'description' field is dictated by where it
appeared in the base interface, not where it appears in the XML schema.
We should also verify that the description field was indeed overridden:
>>> model.schema['description'] # doctest: +ELLIPSIS
Finally, let's verify that bases are preserved upon serialisation:
>>> print serializeSchema(model.schema) # doctest: +NORMALIZE_WHITESPACE
A short summaryDescriptionAge
Fieldset support
----------------
It is often useful to be able to group form fields in the same schema into
fieldsets, for example for form rendering. While plone.supermodel doesn't have
anything to do with such rendering, it does support some markup to make it
possible to define fieldsets. These are stored in a tagged value on the
generated interface, which can then be used by other code.
Fieldsets can be defined from and serialised to XML, using the
tag to wrap a sequence of fields.
>>> schema = """\
...
...
...
...
...
... Title
... True
...
...
... Body text
... True
... 10000
...
...
...
...
...
... Author
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
... """
Fields outside any tag are not placed in any fieldset. An
empty will be recorded as one having no fields. This is sometimes
useful to control the order of fieldsets, if those are to be filled later.
If there are two blocks with the same name, fields from the second
will be appended to the first, and the label and description will be kept
from the first one, as appropriate.
Note that fieldsets are specific to each schema, i.e. the fieldset in the
default schema above is unrelated to the one in the metadata schema.
>>> model = loadString(schema)
>>> getFieldNamesInOrder(model.schema)
['title', 'body', 'publication_date', 'author', 'expiry_date', 'notification_date']
>>> getFieldNamesInOrder(model.schemata['metadata'])
['created', 'creator']
>>> from plone.supermodel.interfaces import FIELDSETS_KEY
>>> model.schema.getTaggedValue(FIELDSETS_KEY)
[