z3ext has its own content types system that allows to easily add new content types, automagically creating its forms, add menu items, etc.
The package providing main content type creation features is z3ext.content.type. It provides content type registration functionality, common base classes for content types, including containers, ordering support for containers, etc.
To create a content type, of course, we need to define it’s schema interface and its class. It is strongly recommended to use the z3ext.content.type.interfaces.IItem interface as base interface, because it defines “title” and “description” fields that are used in many places of z3ext system. Example:
from z3ext.content.type.interfaces import IItem
class IDocument(IItem):
body = Text(title=u'Body text', required=True)
The interface above actually defines three fields: title, description and body. To provide an implementation, there is very handy content type base class - z3ext.content.type.item.PersistentItem. It implements the IItem interface, supports ZODB persistency, containment (knows its parent and name), and is annotatable (so it can support various content extensions). Example implementation:
from z3ext.content.type.item import PersistentItem
class Document(PersistentItem):
implements(IDocument)
body = None
The Document class can now be registered as z3ext content type by using the z3ext:contenttype ZCML directive:
<z3ext:contenttype
name="yourmodule.document"
title="Document"
schema=".interfaces.IDocument"
class=".Document"
permission="yourmodule.AddDocument"
/>
This ZCML directive creates a special “content type” object, which contains information about our content type and handles content object creation and adding. It is available several ways:
As a z3ext.content.type.interfaces.IContentType utility, named by the name specified in ZCML directive argument.
As an adapter from the content class to the IContentType interface. This is useful if you want to get a content type object only having a content object.
As an unnamed utility providing own interface, created especially for this content type. This interface is stored in the z3ext.content module by name composed from the name specified in ZCML directive argument, replacing dots and hyphens with underscores. So, for example, an interface for our Document content type will be available as z3ext.content.yourmodule_document. This is useful if you want to register some adapters or views for the content type object via ZCML.
You can override this behaviour by providing your own interface for this use, see detailed ZCML directive description in the “Advanced Usage” section.
Creating a new container content type is as easy as creating simple content types. There is a base class provided for containers - z3ext.content.type.container.ContentContainer. It has the same features as the PersistentItem type, but also support object containment.
The content type interface could inherit from standard zope IContainer and z3ext IItem:
from zope.app.container.interfaces import IContainer
from z3ext.content.type.interfaces import IItem
class IFolder(IItem, IContainer):
"""custom folder interface"""
The ContentContainer base class fully implements these interfaces:
from z3ext.content.type.container import ContentContainer
class Folder(ContentContainer):
implements(IFolder)
It is registered with z3ext:contenttype ZCML directive just like any other content type:
<z3ext:contenttype
name="yourmodule.folder"
title="Folder"
schema=".interfaces.IFolder"
class=".Folder"
permission="yourmodule.AddFolder"
/>
So, what can we do with the content type object? First, it can be used for creating actual content objects. This is done using the create method:
from z3ext.content.type.interfaces import IContentType
ct = getUtility(IContentType, name='yourmodule.document')
obj = ct.create(
title=u'My TODO',
description=u'Things i would like to do soon',
body=u'1. Finish this document\n'
u'2. Get paid'
)
As you can see, we passed attribute values to the create method. It is quite smart, as it handles constructor arguments (positional and keywords), passing given arguments to it, and if some of keyword arguments are not handled by the class constructor, they are used to set attribute values for fields defined by content type schema.
In case of our example Document content class, the “title” and “description” values will be passed to the constructor, because the PersistentItem base class handles them, and the “body” value will be set as a field value, after object creation, because IDocument defines the “body” field.
The last thing done by the create method is firing the ObjectCreatedEvent for newly created object, so you don’t need to do it yourself.
The second purpose content type objects are used for is adding content to containers. To add a created content object to some container, first, we need to “bind” a content type to the container. This is done by using the __bind__ method:
bound = ct.__bind__(container)
This method returns a bound clone of a content type object that knows about the container we’re going to add our objects to. Now, to add an object to a container, we use the add method:
bound.add(obj)
This simple call does the required security and constraint checks and adds an object to the current container. It also automatically chooses a name for an object using standard INameChooser mechanism provided by zope containers, but if you want to specify your own name for added object, you can pass it as second argument for the add method:
bound.add(obj, 'main_todo')
Name chooser, constraints and addability checkers are described in advaced sections of this document.
The z3ext:contenttype directive has more features than described in the basic usage section. Here we’ll describe all arguments it supports:
Types of a “content type” objects are special marker interfaces used to define its nature. They are specified in the “type” argument of z3ext:contenttype ZCML directive. There are serveral default types provided and used in other parts of z3ext. They are available in the z3ext.content.type.interfacess module:
Note
TODO
This section should be reviewed!
As described in the z3ext:contenttype ZCML directive description, content types can define their containment restrictions. Containers can restrict content types that can be added to it and any content type can restrict containers that this content type can be added to. This is normally done by “contains” and “containers” ZCML directive arguments. This section describes how to check these constraints.
Every content type objects has the listContainedTypes method that returns an iterable of content type objects that can be contained in this content type. Example:
for contained_ct in ct.listContainedTypes():
pass # contained_ct is a content type that is available for adding
This call returns an iterable of content types that can be added to an object of this content type AND are available in current context (passes security and other checks, see the next chapter). You can disable availability checking by passing False as an argument to this method:
for contained_ct in ct.listContainedTypes(False):
pass # contained_ct is a content type that is registered as addable
# to current content type, but it may not pass other availability
# checks, like permission check, etc.
Also, every content type has the checkObject method that checks whether given concrete object of this content type can be placed in a given container under given name:
ct.checkObject(container, 'some_name', obj)
Note, that this method does not return True or False, instead it raises a certain exception in case of failure. These exceptions could be one of these:
You can catch TypeError if you want to simply check the addability without digging into actual potential problem.
Note
TODO
Do we need to describe things in z3ext.content.type.constraints here?
Content type objects have isAddable and isAvailable methods uses to check availability and addability of a content type in current bound container. Default implementation uses the pluggable mechanism that allows to add additional checks without providing custom content type class.
This is done by providing an adapter from a pair of content type object and container object to the z3ext.content.type.interfaces.IContentTypeChecker interface. This adapter is very simple - it should only provide the check method that returns whether the content type is available in this container.
Example:
from z3ext.content.type.interfaces import IContentTypeChecker
class ExampleChecker(object):
adapts(ISomeContentType, ISomeContainer)
implements(IContentTypeChecker)
def __init__(self, contenttype, container):
self.contenttype = contenttype
self.container = container
def check(self):
if len(self.container) >= 30:
return False
return True
The checker should be registered as a named adapter using the standard zope adapter ZCML directive:
<adapter
name="yourmodule.examplechecker"
factory=".ExampleChecker"
/>
Of course, there can be any number of checkers provided per any content type and/or container.
The zope.app.container package contains an useful mechanism for choosing and checking names for new objects. z3ext content types system provides an extended version of name chooser.
One of the features of z3ext name chooser is that it supports reserved names. For example, you may want to reserve some names for views (so they won’t be overriden by contained items on URL traversing) or special child objects. The default implementation is very simple: a list of reserved names are specifying using the z3ext:reservedNames ZCML directive:
<z3ext:reservedNames
for=".interfaces.ISomeContainer"
names="categories index"
/>
This will restrict adding objects under names “categories” and “index”. If you want more smart reserved names logic, you can provide an adapter for your container implementing z3ext.content.type.interfaces.IReservedNames interface, which is simply define the “names” attribute which is a tuple of reserved names. Let’s provide an example of dynamic reserved names adapter:
from z3ext.content.type.interfaces import IReservedNames
class ExampleReservedNames(object):
adapts(ISomeContainer)
implements(IReservedNames)
def __init__(self, context):
names = getReservedNames(context) # getReservedNames is some function
# that returns a sequence of names
self.names = tuple(names)
It is registered as an unnamed adapter using zope adapter ZCML directive:
<adapter factory=".ExampleReservedNames" />
Default zope name chooser mechanism allows to define only one name chooser per container, but in z3ext, you can provide several name choosers depending on type of object that is being added to the container. To do that, you need to provide an adapter from (container, object) pair to the INameChooser interface.
Example:
from zope.app.container.contained import NameChooser
class ExampleNameChooser(NameChooser):
adapts(ISomeContainer, ISomeContent)
def __init__(self, context, object):
super(ExampleNameChooser, self).__init__(context)
# note that we don't store the object, as it will be passed in
# the `chooseName` method anyway.
def chooseName(self, name, object):
# ... choose a name and return it ...
return name
This adapter should be registered for container and object using the adapter ZCML directive:
<adapter
for=".interfaces.ISomeContainer .interfaces.ISomeContent"
factory=".ExampleNameChooser"
/>
One of object-dependent name chooser provided is the title-based name chooser. It is registered for objects providing z3ext.content.type.interfaces.ITitleBasedName marker interfaces and chooses a name for a new object from its title.
This name chooser has some per-site configuration that can be changed by portal manager. Check “Name chooser” configlet in the portal control panel.
z3ext.content.type package provides item ordering support for any container type. The z3ext.content.type.interfaces.IOrder defines a read-only container interface that returns items in an order. An order-aware container should be adapted to this interface to get items in the correct order. So, for example to list items in the ordered container, you write this:
for name in IOrder(container):
pass # do something
Pretty easy, isn’t it? But note, that the adapter only works for containers that are marked as order-aware. The default implementation uses annotations to store order information, so your container should also be annotatable. To mark your container as order-aware using annotations, you should use the z3ext.content.type.interfaces.IAnnotatableOrder marker interface. Example:
<class class=".SomeContainer">
<implements interface="z3ext.content.type.interfaces.IAnnotatableOrder" />
</class>
In the default annotatable order implementation, newly added objects are added to the end of the order.
Default IOrder adapter also provides other important interface - z3ext.content.type.interfaces.IReorderable which defines methods to change the order:
Example:
order = IOrder(container)
if IReorderable.providedBy(order): # we can check if order supports modifying
order.moveTop(('main-page', 'documents'))
z3ext content types system provides a bunch of default UI for managing content objects, including add and edit forms, content listing and deletion views.
To enable the “Contents” view for a container, simply make it implement the z3ext.content.type.interfaces.IContainerContentsAware interface. Example:
<class class=".ExampleContainer">
<implements interface="z3ext.content.type.interfaces.IContainerContentsAware" />
</class>
Content adding forms are accessed using a special “+” view for a container. This view publishes content types when traversed further (e.g. “http://path.to/your/container/+/yourmodule.document“). And the view for a context type object is its add form, automatically generated by z3ext.content.forms.form.AddForm class.
Earlier, we described one way of providing a custom add form by using the “addform” argument to the z3ext:contenttype directive. Other way is to override the view named “index.html” for your concrete content type. If you just want to provide a same form with small changes, use the z3ext.content.forms.form.AddForm as a base class for your form. Example:
from z3ext.content.forms.form import AddForm
class CustomAddForm(AddForm):
label = u'Custom Label'
By default, form gets its label from content type’s title, but we override it. Now register it with a broser:page ZCML directive, just like the default form is registered:
<browser:page
name="index.html"
for=".interfaces.IYourContentType"
class=".CustomAddForm"
permission="zope.View"
/>
Content editing, changing its settings, ownership, etc. is combined to a flexible content edit wizard that is greatly customizable by using pluggable “wizard steps”. It is accessed by appending “context.html” to the URL of your content object.
Content editing wizard steps are named pagelets with type “wizard.step” registered for content and wizard pair. They must provide the z3ext.wizard.interfaces.IWizardStep interface and there are two useful base classes provided by the z3ext.wizard.step module - WizardStep (generic wizard step) and WizardStepForm (form step).
For example, you may want to override the default field editing form step, which is registered as a “wizard.step” type pagelet named “content”. To do this, first create a custom pagelet class:
from z3ext.wizard.step import WizardFormStep
class CustomEditStep(WizardFormStep):
def update(self):
ct = IContentType(self.context)
self.fields = Fields(ct.schema)
super(CustomEditStep, self).update()
And then register it with the z3ext:pagelet directive:
<z3ext:pagelet
name="content"
type="wizard.step"
for="yourmodule.interfaces.IYourContent
z3ext.content.forms.interfaces.IEditContentWizard"
class=".CustomEditStep"
permission="z3ext.ModifyContent"
weight="100"
/>
Notice that we registered the pagelet for IYourContent and z3ext.content.forms.interfaces.IEditContentWizard. The second is an interface of the content edit wizard we create steps for.
You can add and override any content edit steps this way. See z3ext.wizard documentation for more details on wizard step creation.