================================================== 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 Title A short summary 10 False Description 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 Title 10000 Body text >>> print serializeSchema(ITestMetadata, name=u"metadata") # doctest: +NORMALIZE_WHITESPACE False Created date Name of the creator Creator 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 summary Description Age 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 ... ... ...
... ... Publication date ... ...
... ... ... Author ... ... ...
... ... Expiry date ... ... ... Notification date ... ...
...
... ... ... ...
...
...
... ...
... ... Created date ... False ... ...
... ...
... ... Creator ... Name of the creator ... True ... ...
... ... ... ... """ 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) [
] >>> model.schemata[u"metadata"].getTaggedValue(FIELDSETS_KEY) [
,
,
] When we serialise a schema with fieldsets, fields will be grouped by fieldset. >>> print serializeModel(model) # doctest: +NORMALIZE_WHITESPACE Title 10000 Body text Author
Publication date Expiry date Notification date
Name of the creator Creator
False Created date
Invariant Support ----------------- We may specify one or more invariants for the form via the "invariant" tag with a dotted name for the invariant function. >>> schema = """\ ... ... ... ... plone.supermodel.tests.dummy_invariant ... plone.supermodel.tests.dummy_invariant_prime ... ... Description ... A short summary ... ... ... Age ... ... ... ... """ >>> model = loadString(schema) >>> model.schema.getTaggedValue('invariants') [, ] When invariants are checked for our model.schema, we'll see our invariant in action. >>> model.schema.validateInvariants(object()) Traceback (most recent call last): ... Invalid: Yikes! Invalid The model's serialization should include the invariant. >>> print serializeModel(model) # doctest: +NORMALIZE_WHITESPACE plone.supermodel.tests.dummy_invariant plone.supermodel.tests.dummy_invariant_prime A short summary Description Age Invariant functions must provide plone.supermodel.interfaces.IInvariant or we won't accept them. >>> schema = """\ ... ... ... ... plone.supermodel.tests.dummy_unmarkedInvariant ... ... Description ... A short summary ... ... ... Age ... ... ... ... """ >>> model = loadString(schema) Traceback (most recent call last): ... SupermodelParseError: Invariant functions must provide plone.supermodel.interfaces.IInvariant File "", line ... Internationalization -------------------- Translation domains and message ids can be specified for text that is interpreted as unicode. This will result in deserialization as a zope.i18nmessageid message id rather than a basic Unicode string:: >>> schema = """\ ... ... ... ... ... ... Title ... ... ... ... description ... ... ... ... feature ... ... ... ... ... """ >>> model = loadString(schema) >>> msgid = model.schema['title'].title >>> msgid u'supermodel_test_title' >>> type(msgid) >>> msgid.default u'Title' >>> print serializeModel(model) # doctest: +NORMALIZE_WHITESPACE Title description feature Creating custom metadata handlers --------------------------------- The plone.supermodel format is extensible with custom utilities that can write to a "metadata" dictionary. Such utilities may for example read information captured in attributes in particular namespaces. Let's imagine we wanted to make it possible to override form layout on a per-schema level, and override widgets on a per-field level. For this, we may expect to be able to parse a format like this: >>> schema = """\ ... ... ... ... ... Title ... True ... ... ... Description ... A short summary ... False ... 10 ... ... ... ... """ We can register schema and field metadata handlers as named utilities. Metadata handlers should be able to reciprocally read and write metadata. >>> from zope.interface import implements >>> from zope.component import provideUtility >>> from plone.supermodel.interfaces import ISchemaMetadataHandler >>> from plone.supermodel.utils import ns >>> class FormLayoutMetadata(object): ... implements(ISchemaMetadataHandler) ... ... namespace = "http://namespaces.acme.com/ui" ... prefix = "ui" ... ... def read(self, schemaNode, schema): ... layout = schemaNode.get(ns('layout', self.namespace)) ... if layout: ... schema.setTaggedValue(u'acme.layout', layout) ... ... def write(self, schemaNode, schema): ... layout = schema.queryTaggedValue(u'acme.layout', None) ... if layout: ... schemaNode.set(ns('layout', self.namespace), layout) >>> provideUtility(component=FormLayoutMetadata(), name='acme.ui.schema') >>> from plone.supermodel.interfaces import IFieldMetadataHandler >>> class FieldWidgetMetadata(object): ... implements(IFieldMetadataHandler) ... ... namespace = "http://namespaces.acme.com/ui" ... prefix = "ui" ... ... def read(self, fieldNode, schema, field): ... name = field.__name__ ... widget = fieldNode.get(ns('widget', self.namespace)) ... if widget: ... widgets = schema.queryTaggedValue(u'acme.widgets', {}) ... widgets[name] = widget ... schema.setTaggedValue(u'acme.widgets', widgets) ... ... def write(self, fieldNode, schema, field): ... name = field.__name__ ... widget = schema.queryTaggedValue(u'acme.widgets', {}).get(name, {}) ... if widget: ... fieldNode.set(ns('widget', self.namespace), widget) >>> provideUtility(component=FieldWidgetMetadata(), name='acme.ui.fields') When this model is loaded, utilities above will be invoked for each schema and each field, respectively. >>> model = loadString(schema) >>> model.schema.getTaggedValue('acme.layout') 'horizontal' >>> model.schema.getTaggedValue('acme.widgets') {'title': 'largetype'} Of course, we can also serialize the schema back to XML. Here, the 'prefix' set in the utility (if any) will be used by default. >>> print serializeModel(model) # doctest: +NORMALIZE_WHITESPACE Title A short summary 10 False Description