Adding the condition
A Content Rules condition (or action) consists of:
- An interface that describes the configurable aspects of the condition.
- A persistent object that is used to store the configuration of the condition.
- An executor adapter, which is used to execute the condition when the rule is executed
- An add view and an edit view, which let the end user add and configure the condition
We will place all of these in a file called keyword.py inside the collective.keywordcondition package.
Storing the condition settings
First, we import a few things.
from persistent import Persistent from OFS.SimpleItem import SimpleItem from zope.interface import implements, Interface from zope.component import adapts from zope.formlib import form from zope import schema from zope.app.component.hooks import getSite from zope.component.interfaces import IObjectEvent from plone.contentrules.rule.interfaces import IExecutable, IRuleElementData from plone.app.contentrules.browser.formhelper import AddForm, EditForm from Acquisition import aq_inner from collective.keywordcondition import MessageFactory as _
Notice the message factory, which is used for translation purposes. It is defined in the __init__.py file in the collective.keywordcondition package like so:
from zope.i18nmessageid import MessageFactory
MessageFactory = MessageFactory('collective.keywordcondition')
Then, we define an interface using zope.schema, which describes the configurable aspects of the condition: in this case, a list of keywords. The title, description and other metadata will be used by zope.formlib in the add and edit forms defined later. See the zope.schema package for more information about which field types are available and how they can be configured.
class IKeywordCondition(Interface):
"""Interface for the configurable aspects of a keyword condition.
This is also used to create add and edit forms, below.
"""
keywords = schema.Tuple(title=_(u"Keywords"),
description=_(u"The keywords to check for."),
required=True,
value_type=schema.TextLine(title=_(u"Keyword")))
Next, we create the actual condition storage implementation.
class KeywordCondition(SimpleItem):
"""The actual persistent implementation of the keyword condition element.
Note that we must mix in SimpleItem to keep Zope 2 security happy.
"""
implements(IKeywordCondition, IRuleElementData)
keywords = []
element = "collective.keywordcondition.Keyword"
@property
def summary(self):
return _(u"Keywords contains: ${names}", mapping=dict(names=", ".join(self.keywords)))
The keywords variable sets the default value of the field defined in the aforementioned IKeywordCondition interface. The element variable specifies a unique name for this rule element (conditions and actions are commonly referred to as rule elements). By convention, this is a dotted name that includes the package name, so as to guarantee uniqueness. The element name will be referenced again in the configure.zcml file shortly.
The storage object must also implement a summary property. This is used in the Content Rules control panel to given a summary of the condition when a rule is being viewed.
The executor
After defining the storage for the condition, we define an executor.
class KeywordConditionExecutor(object):
"""The executor for this condition.
This is registered as an adapter in configure.zcml
"""
implements(IExecutable)
adapts(Interface, IKeywordCondition, IObjectEvent)
def __init__(self, context, element, event):
self.context = context
self.element = element
self.event = event
def __call__(self):
context = aq_inner(self.event.object)
keywords = frozenset()
try:
keywords = frozenset(context.Subject())
except (AttributeError, TypeError,):
# The object doesn't have a Subject method
return False
return (len(keywords.intersection(self.element.keywords)) > 0)
This is simply an adapter that adapts the context content object type (in this case, we don't care about what type of object we use, and so we specify Interface, the most general interface), the condition type (our newly defined IKeywordCondition interface), and the event type (in this case, the IObjectEvent, a generic interface that describes events about objects). We will come back to the event registration in a moment.
It is possible to override the implementation of the executor for different context types or different event types, by registering additional adapters.
The executor has one important method - __call__() - which is called during rule execution. It must return True or False. If it returns True, rule execution will continue with the next condition or action, otherwise the rule will be aborted.
If you are creating an action, your executor adapter will be doing the actual work of the action. It should still return True or False to indicate whether rule processing can continue.
Notice how the executor is coded quite defensively. Since rules may be invoked on a variety of events, in a variety of circumstances, we cannot make too many assumptions about what we get as the context object or the event. The implementation itself simply checks whether the set of keywords that were entered by the user intersects with the set of keywords that are defined on the event context.
Add and edit forms
Finally, we must define add and edit views that are used to create and modify an instance of our new condition. We use the zope.formlib library and our IKeywordCondition interface like so:
class KeywordAddForm(AddForm):
"""An add form for portal type conditions.
"""
form_fields = form.FormFields(IKeywordCondition)
label = _(u"Add Keyword Condition")
description = _(u"A keyword condition makes the rule apply only to content with certain keywords.")
form_name = _(u"Configure element")
def create(self, data):
c = KeywordCondition()
form.applyChanges(c, self.form_fields, data)
return c
class KeywordEditForm(EditForm):
"""An edit form for portal type conditions
"""
form_fields = form.FormFields(IKeywordCondition)
label = _(u"Edit Keyword Condition")
description = _(u"A keyword condition makes the rule apply only to content with certain keywords.")
form_name = _(u"Configure element")
The AddForm and EditForm base classes are helpers from plone.app.contentrules. They provide us with standard buttons and validators.
Configuring the components
With the code in place, we must register our components in configure.zcml:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:plone="http://namespaces.plone.org/plone"
xmlns:five="http://namespaces.zope.org/five"
i18n_domain="collective.keywordcondition">
<include package="plone.contentrules" />
<include package="plone.contentrules" file="meta.zcml" />
<!-- the keyword condition -->
<adapter factory=".keyword.KeywordConditionExecutor" />
<browser:page
for="plone.app.contentrules.browser.interfaces.IRuleConditionAdding"
name="collective.keywordcondition.Keyword"
class=".keyword.KeywordAddForm"
permission="cmf.ManagePortal"
/>
<browser:page
for="collective.keywordcondition.keyword.IKeywordCondition"
name="edit"
class=".keyword.KeywordEditForm"
permission="cmf.ManagePortal"
/>
<plone:ruleCondition
name="collective.keywordcondition.Keyword"
title="Keyword"
description="Apply only when the current content object has one of the given keywords"
for="*"
event="zope.component.interfaces.IObjectEvent"
addview="collective.keywordcondition.Keyword"
editview="edit"
/>
</configure>
Here, we first import the necessary configuration elements from plone.app.contentrules and then register the executor adapter, the add view and the edit view. The name of the add view should correspond to the element name (collective.keywordcondition.Keyword in this case) as it was defined in the element class above. The name of the edit view should always be edit.
Finally, we register the condition type itself.
When creating an action, use the <plone:ruleAction /> directive. It uses the same attributes as <plone:ruleCondition />
Again, the name must correspond to the element name defined in the element class. The title and description are used in the user interface. The for and event attributes are used to control when the rule element will be available. In this case, it is available for any context type, and any object event.
Most of the events registered for use with the Content Rules system inherit from IObjectEvent. This interface simply guarantees that there will be an object attribute on the event instance that contains the object for which the event took place. If you want to restrict the condition to a more specific type of event, you can specify a different interface here. This is generally only necessary if you need additional information in order to execute the condition. For example, the "workflow transition" condition that comes with plone.app.contentrules is registered for Products.CMFCore.interfaces.IActionSucceededEvent, since it needs to determine the workflow transition that took place, and this is only sent as part of this particular type of event. If you do not care about the event type at all, you can sepcify "*" as the event type.
There must be at least one executor adapter applicable for event that the condition or action is registered for. In this example, we registered the executor as an adapter of IObjectEvent, and restricted the condition to only be available for events of this type.
