Abusing Plone Content Rules to allow Site Admin customizations of sent emails

This would be a sort of tutorial on how to implement a new plone.app.contentrules Trigger Event, how to write a new plone.stringinterp variable substitutor and how to trigger the event from a browser page (or z3c.form).

This allows configuring a new trigger event from the Content Rules Plone control panel configlet and to assign a new Email action, with customizable body template.

Let's say our task is to implement a contact form:

from plone.directives import form
from plone.api.portal import show_message
from zope.event import notify

class IContactForm(form.Schema):
    name = schema.TextLine(title=u"Your Name", required=True)
    email = Email(title=u"Contact eMail:", required=True)

class ContactInformation(Implicit):
    """ A container to be passed to plone.app.contentrules for contact info
    """

    def __init__(self, name=None, email=None):
        self.name = name
        self.email = email

class ContactForm(form.SchemaForm):

    schema = IContactForm
    ignoreContext = True

    label = u"Contact Form"

    fields = field.Fields(IContactForm)

    @button.buttonAndHandler(u"Submit")
    def handleApply(self, action):
        data, errors = self.extractData()
        if errors:
            self.status = self.formErrorsMessage
            return

        name = data.get('name')
        email = data.get('email')

        obj = ContactInformation(name=name, email=email)
        obj = obj.__of__(self.context)
        notify(ContactEvent(obj))
        show_message(message="Message sent", request=self.request, type='info')

The ContactInformation object is needed to pass information from the form to the content rules machinery. It needs to be acquisition aware, as plone.app.contentrules will try to trigger events up its chain of acquisition. This can also be replaced by setting some annotation on the request.

We need a custom event with an event handler that triggers the content rules execution:

from zope.interface import implements
from zope.component.interfaces import ObjectEvent, IObjectEvent
from plone.app.contentrules.handlers import execute_rules

class IContactEvent(IObjectEvent):
    """
    """

class ContactEvent(ObjectEvent):
    implements(IContactEvent)

def trigger_contentrules(event):
    execute_rules(event)

Now, a bit of zcml to register the event with plone.app.contentrules and setup the event handler:

<interface
    interface=".events.IContactEvent"
    type="plone.contentrules.rule.interfaces.IRuleEventType"
    name="Contact form triggered"
    />

<subscriber for=".events.IContactEvent" handler=".events.trigger_contentrules" />

This will make the "Contact form triggered" event available for the Plone Content Rules and, thanks to the notify() call in the form handler, trigger the content rules execution.

One last thing that we need is to access the information submitted in the form. The ContactInformation object serves as a container for that information, so we'll need special variables for the Email action:

class contact_email(BaseSubstitution):
    description = u"Contact email"
    category = 'ContactForm'

    adapts(Interface)

    def safe_call(self):
        return self.context.email


class contact_name(BaseSubstitution):
    description = u"Contact name"
    category = 'ContactForm'

    adapts(Interface)

    def safe_call(self):
        return self.context.name
and the zcml for this:
<adapter
    for="*"
    provides="plone.stringinterp.interfaces.IStringSubstitution"
    factory=".stringinterp.contact_email"
    name="contact_email"
    />

<adapter
    for="*"
    provides="plone.stringinterp.interfaces.IStringSubstitution"
    factory=".stringinterp.contact_name"
    name="contact_name"
    />
So why use this mechanism instead of something like PloneFormGen? In my case, the site administrator is used to changing email templates for various events (object created, published, etc) from the Content Rules panel, so why not keep everything simple and consistent?

Comments