Schemas#
Zope schemas are a database-neutral and form-library-neutral way to describe Python data models. Schemas extend the notion of interfaces to detailed descriptions of attributes (but not methods). Every schema is an interface and specifies the public fields of an object. A field roughly corresponds to an attribute of a Python object. But a field provides space for at least a title and a description. It can also constrain its value and provide a validation method. You can optionally specify characteristics, such as its value being read-only or not required.
Plone uses Zope schemas to describe:
persistent data models
HTML form data
Plone configuration data
ZCML configuration data
Since Zope schemas aren't bound to any persistent storage, such as an SQL database engine, it gives you a reusable way to define data models.
Schemas are just regular Python classes, with some special attribute declarations.
They're always subclasses of zope.interface.Interface
.
The schema itself can't be a concrete object instance.
You need to either have a persistent.Persistent
object (for database data) or a z3c.form.form.Form
object (for HTML forms).
Zope schemas are used for tasks such as:
defining allowed input data format (string, integer, object, list, and others) for Python class instance attributes
specifying required attributes on an object
defining custom validators on input data
The basic unit of data model declaration is the field, which specifies what kind of data each Python attribute can hold.
plone.schema
versus zope.schema
#
The main package is zope.schema
.
We can also use plone.schema
, which provides additional fields and widgets for z3c.form
and optional integration with Plone.
Additional features include:
Email field and widget
JSON field and widget
URI field and widget
IPath
asIChoice
derivative (and implementation)integration with
plone.supermodel
, optional (extrasupermodel
)integration with
plone.schemaeditor
, optional (extraschemaeditor
)
Example of a schema#
Tip
In VS Code editor, you can install the Plone Snippets extension. This will give you snippets for most fields, widgets, and autoform directives in Python and XML based schemas.
Define a schema for a data model to store addresses:
import zope.interface
from zope import schema
class ICheckoutAddress(zope.interface.Interface):
""" Provide meaningful address information.
"""
first_name = schema.TextLine(title=_("First name"), default="")
last_name = schema.TextLine(title=_("Last name"), default="")
organization = schema.TextLine(title=_("Organization"), default="")
phone = schema.TextLine(title=_("Phone number"), default="")
country = schema.Choice(
title = _("Country"),
vocabulary = "getpaid.countries",
required=False,
default=None,
)
state = schema.Choice(
title = _("State"),
vocabulary="getpaid.states",
required=False,
default=None,
)
city = schema.TextLine(title=_("City"), default="")
postal_code = schema.TextLine(title=_("Postal code"), default="")
street_address = schema.TextLine(title=_("Address"), default="")
This schema can be used in Forms and Dexterity Content Types data models.
Note
In Dexterity Content Types, the base class for a schema is plone.supermodel.model.Schema
.
This provides functionalities to export and import schemas via XML and the Through-the-web (TTW) editor.
Field constructor parameters#
The Field
base class defines a list of standard parameters that you can use to construct schema fields.
Each subclass of Field
will have its own set of possible parameters in addition to this.
The following is a list of a few of the most common parameters.
title
field title as Unicode string
description
field description as Unicode string
required
boolean, whether the field is required
default
Default value if the attribute isn't present
See also
See IField
interface and field implementation in the zope.schema
documentation for details.
Warning
Do not initialize any non-primitive values using the default
keyword parameter of schema fields.
Python and the ZODB stores objects by reference.
Python code will construct only one field value during schema construction, and share its content across all objects.
This is probably not what you intend.
Instead, initialize objects in the __init__()
method of your schema implementer.
In particular, dangerous defaults are default=[]
, default={}
, and default=SomeObject()
.
Use defaultFactory=get_default_name
instead.
Schema introspection#
The zope.schema._schema
module provides some introspection functions:
getFieldNames(schema_class)
getFields(schema_class)
getFieldNamesInOrder(schema)
retains the original field declaration order.getFieldsInOrder(schema)
retains the original field declaration order.
Example:
import zope.schema
import zope.interface
class IMyInterface(zope.interface.Interface):
text = zope.schema.TextLine()
# Get list of schema fields from IMyInterface
fields = zope.schema.getFields(IMyInterface)
Dump schema data#
Below is an example of how to extract all schema defined fields from an object.
from collections import OrderedDict
import zope.schema
def dump_schemed_data(obj):
"""
Prints out object variables as defined by its zope.schema Interface.
"""
out = OrderedDict()
# Check all interfaces provided by the object
ifaces = obj.__provides__.__iro__
# Check fields from all interfaces
for iface in ifaces:
fields = zope.schema.getFieldsInOrder(iface)
for name, field in fields:
# ('header', <zope.schema._bootstrapfields.TextLine object at 0x1149dd690>)
out[name] = getattr(obj, name, None)
return out
Find the schema for a Dexterity type#
When trying to introspect a Dexterity type, you can get a reference to the schema as follows:
from zope.component import getUtility
from plone.dexterity.interfaces import IDexterityFTI
schema = getUtility(IDexterityFTI, name=PORTAL_TYPE_NAME).lookupSchema()
Then you can inspect it using the methods above. Note this won't have behavior fields added to it at this stage, only the fields directly defined in your schema.
More schema resources#
zope.schema
on PyPIzope.schema
source code is the definitive source for field types and usage.
Schema directives#
With plone.autoform
and plone.supermodel
, we can use directives to add information to the schema fields.
Omit fields#
A field can be omitted entirely from all forms, or from some forms, using the omitted
and no_omit
directives.
In this example, the dummy
field is omitted from all forms, and the edit_only
field is omitted from all forms except those that provide the IEditForm
interface:
1from z3c.form.interfaces import IEditForm
2from plone.supermodel import model
3from plone.autoform import directives as form
4
5class IMySchema(model.Schema):
6
7 form.omitted("dummy")
8 dummy = schema.Text(
9 title="Dummy"
10 )
11
12 form.omitted("edit_only")
13 form.no_omit(IEditForm, "edit_only")
14 edit_only = schema.TextLine(
15 title = "Only included on edit forms",
16 )
In supermodel XML, this can be specified as:
<field type="zope.schema.TextLine"
name="dummy"
form:omitted="true">
<title>Dummy</title>
</field>
<field type="zope.schema.TextLine"
name="edit-only"
form:omitted="z3c.form.interfaces.IForm:true z3c.form.interfaces.IEditForm:false">
<title>Only included on edit form</title>
</field>
form:omitted
may be either a single boolean value, or a space-separated list of <form_interface>:<boolean>
pairs.
Reorder fields#
A field's position in the form can be influenced using the order_before
and order_after
directives.
In this example, the not_last
field is placed before the summary
field, even though it is defined afterward:
1from plone.supermodel import model
2from plone.autoform import directives as form
3
4class IMySchema(model.Schema):
5
6 summary = schema.Text(
7 title="Summary",
8 description="Summary of the body",
9 readonly=True
10 )
11
12 form.order_before(not_last="summary")
13 not_last = schema.TextLine(
14 title="Not last",
15 )
The value passed to the directive may be either *
, indicating before or after all fields, or the name of another field.
Use .<fieldname>
to refer to the field in the current schema or a base schema.
Prefix with the schema name, such as IDublinCore.title
, to refer to a field in another schema.
Use an unprefixed name to refer to a field in either the current or default schema for the form.
In supermodel XML, the directives are called form:before
and form:after
.
For example:
<field type="zope.schema.TextLine"
name="not_last"
form:before="*">
<title>Not last</title>
</field>
Organizing fields into fieldsets#
Fields can be grouped into fieldsets, which will be rendered within an HTML <fieldset>
tag.
In this example the footer
and dummy
fields are placed within the extra
fieldset:
1from plone.supermodel import model
2from plone.autoform import directives as form
3
4class IMySchema(model.Schema):
5
6 model.fieldset("extra",
7 label="Extra info",
8 fields=["footer", "dummy"]
9 )
10
11 footer = schema.Text(
12 title="Footer text",
13 )
14
15 dummy = schema.Text(
16 title="Dummy"
17 )
In supermodel XML, fieldsets are specified by grouping fields within a <fieldset>
tag:
<fieldset name="extra" label="Extra info">
<field name="footer" type="zope.schema.TextLine">
<title>Footer text</title>
</field>
<field name="dummy" type="zope.schema.TextLine">
<title>Dummy</title>
</field>
</fieldset>
Advanced#
Note
Most examples in this section are low level Zope stuff. In Plone, you rarely have to deal with it. But we keep it here for those who are interested in how things work internally.
We can use a schema class to store data based on our model definition in the ZODB database.
We use zope.schema.fieldproperty.FieldProperty
to bind persistent class attributes to the data definition.
Example:
from persistent import Persistent # Automagical ZODB persistent object
from zope.schema.fieldproperty import FieldProperty
class CheckoutAddress(Persistent):
""" Store checkout address """
# Declare that all instances of this class will
# conform to the ICheckoutAddress data model:
zope.interface.implements(ICheckoutAddress)
# Provide the fields:
first_name = FieldProperty(ICheckoutAddress["first_name"])
last_name = FieldProperty(ICheckoutAddress["last_name"])
organization = FieldProperty(ICheckoutAddress["organization"])
phone = FieldProperty(ICheckoutAddress["phone"])
country = FieldProperty(ICheckoutAddress["country"])
state = FieldProperty(ICheckoutAddress["state"])
city = FieldProperty(ICheckoutAddress["phone"])
postal_code = FieldProperty(ICheckoutAddress["postal_code"])
street_address = FieldProperty(ICheckoutAddress["street_address"])
For persistent objects, see the persistent object documentation.
Use schemas as data models#
Based on the example data model above, we can use it in content type browser views to store arbitrary data as content type attributes.
Example:
class MyView(BrowserView):
""" Connect this view to your content type using a ZCML declaration.
"""
def __call__(self):
# Get the content item which this view was invoked on:
context = self.context.aq_inner
# Store a new address in it as the ``test_address`` attribute
context.test_address = CheckoutAddress()
context.test_address.first_name = "Mikko"
context.test_address.last_name = "Ohtamaa"
# Note that you can still add arbitrary attributes to any
# persistent object. They are simply not validated, as they
# don't go through the ``zope.schema`` FieldProperty
# declarations.
# Do not do this, you will regret it later.
context.test_address.arbitary_attribute = "Don't do this!"
Field order#
The order
attribute can be used to determine the order in which fields in a schema were defined.
If one field was created after another (in the same thread), the value of order
will be greater.
Default values#
To make default values of schema effective, class attributes must be implemented using FieldProperty
.
Example:
import zope.interface
from zope import schema
from zope.schema.fieldproperty import FieldProperty
class ISomething(zope.interface.Interface):
""" Sample schema """
some_value = schema.Bool(default=True)
class SomeStorage(object):
some_value = FieldProperty(ISomething["some_value"])
something = SomeStorage()
assert something.some_value == True
Validation and type constraints#
Schema objects using field properties provide automatic validation facilities for the prevention of setting badly formatted attributes.
There are two aspects to validation:
Checking the type constraints (done automatically).
Checking whether the value fills certain constraints (validation).
Example of how type constraints work:
class ICheckoutData(zope.interface.Interface):
""" This interface defines all the checkout data we have.
It will also contain the ``billing_address``.
"""
email = schema.TextLine(title=_("Email"), default="")
class CheckoutData(Persistent):
zope.interface.implements(ICheckoutData)
email = FieldProperty(ICheckoutData["email"])
def test_store_bad_email(self):
""" Check that we can't put data to checkout """
data = getpaid.expercash.data.CheckoutData()
from zope.schema.interfaces import WrongContainedType, WrongType, NotUnique
try:
data.email = 123 # Can't set email field to an integer.
raise AssertionError("Should never be reached.")
except WrongType:
pass
Example of validation (email field):
from zope import schema
class InvalidEmailError(schema.ValidationError):
__doc__ = "Please enter a valid e-mail address."
def isEmail(value):
if re.match("^"+EMAIL_RE, value):
return True
raise InvalidEmailError
class IContact(Interface):
email = schema.TextLine(title="Email", constraint=isEmail)
Persistent objects and schema#
ZODB persistent objects don't provide facilities for setting field defaults or validating the data input.
When you create a persistent class, you need to provide field properties for it, which will sanitize the incoming and outgoing data.
When the persistent object is created, it has no attributes.
When you try to access the attribute through a named zope.schema.fieldproperty.FieldProperty
accessor, it first checks whether the attribute exists.
If the attribute isn't there, it's created and the default value is returned.
Example:
from persistent import Persistent
from zope import schema
from zope.interface import implements, alsoProvides
from zope.component import adapts
from zope.schema.fieldproperty import FieldProperty
# ... other implementation code ...
class IHeaderBehavior(form.Schema):
""" Sample schema """
inheritable = schema.Bool(
title="Inherit header",
description="This header is visible on child content",
required=False,
default=False)
block_parents = schema.Bool(
title="Block parent headers",
description="Do not show parent headers for this content",
required=False,
default=False)
# Contains list of HeaderAnimation objects
alternatives = schema.List(
title="Available headers and animations",
description="Headers and animations uploaded here",
required=False,
value_type=schema.Object(IHeaderAnimation))
alsoProvides(IHeaderAnimation, form.IFormFieldProvider)
class HeaderBehavior(Persistent):
""" Sample persistent object for the schema """
implements(IHeaderBehavior)
#
# zope.schema magic happens here - see FieldProperty!
#
# We need to declare field properties so that objects will
# have input data validation and default values taken from schema
# above
inheritable = FieldProperty(IHeaderBehavior["inheritable"])
block_parents = FieldProperty(IHeaderBehavior["block_parents"])
alternatives = FieldProperty(IHeaderBehavior["alternatives"])
Now you see the magic:
header = HeaderBehavior()
# This triggers the ``alternatives`` accessor, which returns the default
# value, which is an empty list
assert header.alternatives = []
Collections (and multiple choice fields)#
Collections are fields composed of several other fields. Collections also act as multiple choice fields.
For more information see:
Using Zope schemas with a complex vocabulary and multi-select fields
Collections section in
zope.schema
documentationSchema field sources documentation
Single choice example#
Only one value can be chosen.
Below is code to create a Python logging level choice:
import logging
from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
def _createLoggingVocabulary():
""" Create zope.schema vocabulary from Python logging levels.
Note that term.value is int, not string.
_levelNames looks like::
{0: "NOTSET", "INFO": 20, "WARNING": 30, 40: "ERROR", 10: "DEBUG", "WARN": 30, 50:
"CRITICAL", "CRITICAL": 50, 20: "INFO", "ERROR": 40, "DEBUG": 10, "NOTSET": 0, 30: "WARNING"}
@return: Iterable of SimpleTerm objects
"""
for level, name in logging._levelNames.items():
# logging._levelNames dictionary is bidirectional, let's
# get numeric keys only
if type(level) == int:
term = SimpleTerm(value=level, token=str(level), title=name)
yield term
# Construct SimpleVocabulary objects of log level -> name mappings
logging_vocabulary = SimpleVocabulary(list(_createLoggingVocabulary()))
class ISyncRunOptions(Interface):
log_level = schema.Choice(vocabulary=logging_vocabulary,
title="Log level",
description="One of python logging module constants",
default=logging.INFO)
Multiple choice example#
Using zope.schema.List
, many values can be chosen once.
Each value is atomically constrained by the value_type
schema field.
Example:
from zope import schema
from plone.supermodel import model
from plone.autoform import directives as form
from z3c.form.browser.checkbox import CheckBoxFieldWidget
class IMultiChoice(model.Schema):
# ...
# Contains lists of values from Choice list using special "get_field_list" vocabulary
# We also give a ``plone.autoform.directives`` hint to render this as
# multiple checbox choices
form.widget(yourField=CheckBoxFieldWidget)
yourField = schema.List(title="Available headers and animations",
description="Headers and animations uploaded here",
required=False,
value_type=zope.schema.Choice(source=yourVocabularyFunction),
)
Dynamic schemas#
Schemas are singletons, as there exists only one class instance per Python run time. For example, if you need to feed schemas generated dynamically to a form engine, then consider the following.
If the form engine, such as
z3c.form
, refers to schema fields, then replace these references with dynamically generated copies.Generate a Python class dynamically. Output Python source code, then
eval()
it. Usingeval()
is almost always considered a bad practice.
Warning
Though it is possible, you should not modify zope.schema
classes in-place, as the same copy is shared between different threads.
If there are two concurrent HTTP requests, problems may occur.
Replace schema fields with dynamically modified copies#
Below is an example for z3c.form
.
It uses the Python copy
module to copy the f.field
reference, which points to the zope.schema
field.
For this field copy, we modify the required
attribute, based on input.
Example:
@property
def fields(self):
""" Get the field definition for this form.
Form class's fields attribute does not have to
be fixed. It can also be a property.
"""
# Construct the Fields instance as we would
# normally do in more static way
fields = z3c.form.field.Fields(ICheckoutAddress)
# We need to override the actual required from the
# schema field which is a little tricky.
# Schema fields are shared between instances
# by default, so we need to create a copy of it
if self.optional:
for f in fields.values():
# Create copy of a schema field
# and force it unrequired
schema_field = copy.copy(f.field) # shallow copy of an instance
schema_field.required = False
f.field = schema_field