Send mail on a workflow transition

Shows how to send an email when a workflow transition is triggered, for example to notify content owners that their document has been rejected.

The example below shows how to create a script called reject_notification which is triggered on the reject workflow transition in order to send an email to the content submitter explaining that their document was rejected. It can of course be adapted to send different messages on different transitions easily.

Workflow Configuration:

  • In the ZMI goto portal_workflow
  • click the scripts tab
  • add a new Script (Python)
  • id: reject_notification
  • parameter list: sti
  • body:
        obj=sti.object
        creator = obj.Creator()
        history = sti.getHistory()
        wf_tool = context.portal_workflow
    
        mMsg = """
        Your submission was rejected.
        The url was %s.
        The reason was %s.
        """
    
        member = context.portal_membership.getMemberById(creator)
        creator = {'member':member,
                   'id':member.getId(),
                   'fullname':member.getProperty('fullname', 'Fullname missing'),
                   'email':member.getProperty('email', None)}
    
        actorid = wf_tool.getInfoFor(obj, 'actor')
        actor = context.portal_membership.getMemberById(actorid)
        reviewer = {'member':actor,
                    'id':actor.getId(),
                    'fullname':actor.getProperty('fullname', 'Fullname missing'),
                    'email':actor.getProperty('email', None)}
    
        mTo = creator['email']
        mFrom = reviewer['email']
        mSubj = 'Your item has transitioned'
        obj_url = obj.absolute_url() #use portal_url + relative_url
        comments = wf_tool.getInfoFor(obj, 'comments')
    
        message = mMsg % (obj_url, comments)
        context.MailHost.send(message, mTo, mFrom, mSubj)
    
  • click save

Now we have added the script that does the transition. We want to wire this script to the reject transition.

  • click the transitions tab in the ZMI
  • click the reject transition
  • in the Script (after) drop down select the reject_notification
  • click Save

Now whenever you reject a piece of content it will email the person who created the content. It will also say say it came from the person who rejected the document.

Cannot get it to work

Posted by rejog at Nov 19, 2004 04:25 AM
When I try this as written I get this error:

  Module None, line 14, in reject_notification
   - <PythonScript at /test/portal_workflow/test_workflow2/scripts/reject_notification>
   - Line 14
AttributeError: 'NoneType' object has no attribute 'getId'

I'm mystified. Ideas anyone?

Answer found

Posted by rejog at Dec 13, 2004 04:43 AM
Once I authenticated as a Plone user and not the Admin user (which is a Zope user), it worked fine.

Re: Answer Found

Posted by Mike Takahashi at Aug 04, 2005 10:15 PM
Yes, this is very important to note. If you use your admin Zope user when logged into Plone, it is not a user of Plone. The call that uses getId() is from portal_membership:

context.portal_membership.getMemberById(creator).getId()

BUT, The Zope admin is not part of Plone's portal_membership. This is also important to remember when creating other apps/functions that deal with users and groups.
    

cannot save script due to error message

Posted by zmz at Dec 05, 2004 03:18 AM
Errors: invalid syntax (line 1)
ie
    obj=sti.object
Have had little luck with cut and paste of scrips from how-tos.
Regards david..

Copy/Pasting code

Posted by Dominic Cronin at Feb 06, 2005 06:34 PM
Copy/Pasting code from the browser into the text-edit screen in Zope doesn't work very well at all. What I noticed was that things like quotation marks tend to disappear. This seems to be a very good reason to use the External Editor feature. On my system this is PythonWin, and pasting there works fine, and you can then save directly back to Zope.

Script Error

Posted by nodo at Jan 12, 2005 10:15 PM
I am also getting a script error after copying and pasting the code. It reads:

invalid syntax (Script (Python), line 2)

Any advice please?

regarding errors posted here...

Posted by Yossarian at Mar 11, 2005 09:15 PM
If you haen't figured it out already, you need to remove the extra whitespace at the beginning of each line if you're going to copy and paste.

submit notification

Posted by Yossarian at Mar 15, 2005 05:29 PM
By the way, anybody know how to do this for a visible > submit transition? In other words, to send an e-mail to the reviewer to go and review the document?

Thanks

me too

Posted by mindfire at Apr 04, 2005 07:06 PM
I am also looking for an easy way to do this

This is covered in The Definitive Guide to Plone

Posted by Rik Panganiban at Apr 27, 2005 09:13 PM

how to modify this script to notify reviewers of submissions

Posted by Steve K. at Dec 02, 2006 09:23 PM
Here's how I modified this script to notify reviewers of pending content submissions, using the standard plone_workflow in Plone 2.5.1...

1 change the text of mMsg to something more appropriate (e.g. "Content has been submitted for your review" instead of "Your submission was rejected.").

2 change

    <code>mTo = creator['email']
    mFrom = reviewer['email']</code>

    to

    <code>mTo = reviewer['email']
    mFrom = creator['email']</code>

    so the email goes to the reviewer instead of the author.

3 rename the script (e.g. submit_notification).

4 set the script to be triggered by the submit transition:

    * in your workflow (e.g. portal_workflow/plone_workflow in the ZMI), click the Transitions tab

    * then click the Submit transition,

    * then select your script in the Script(after) drop-down menu.

5 as noted by Rene in the comment below, create a proxy role for the script so all authors have permissions for Mailhost (otherwise I found authors with only the Member role are sent to a login screen after submitting and the script is not triggered):

    * go to the script in the ZMI (e.g. portal_workflow/plone_workflow/scripts/submit_notification)

    * click the Proxy tab

    * select Manager from Proxy Roles and save changes.

Permissions required

Posted by Rene Pijlman at Jun 19, 2005 05:05 PM
This script requires the permission 'Use mailhost services'.

There are two possibilities:
1. Assume or require that the user who fires the transition has this permission
2. Assign a proxy role to the script that has this permission

Parameter name

Posted by Rene Pijlman at Jun 19, 2005 05:08 PM
In The Definitive Guide to Plone the parameter is called 'state_change' instead of 'sti'.

"What's in a name?" you say. True, but this may be a stumbler for newbies who follow this howto and copy code from the book.

Perhaps this howto can make this dependency between configuration and code a little clearer.

Exceptions

Posted by Rene Pijlman at Jun 19, 2005 05:10 PM
An exception while sending mail will cause the transition to fail. This may or may not be what you want.

If it's not, add exception handling to the script:

try:
   # code here
except Exception:
   pass

global variable context not defined

Posted by rchristie at Aug 16, 2005 05:09 PM
I get this weired error. when the script is called. plone doesnt get the context resolved. i tried using 'here' instead of 'context' but error is not changed.
any help ?

same here

Posted by Rob Lineberger at Aug 23, 2006 04:35 PM
I get the global name 'constant' is not defined error too. I'll post here if a solution presents itself.

self works

Posted by Rob Lineberger at Aug 23, 2006 05:24 PM
My code was on the file system as part of an AT-based product. If I replace "context" with "self," it works. I suppose that makes sense because context is not within the portal for filesystem code.

Getting strange error at owner = mtool.getMemberById(creator)

Posted by Juergen Holzer at Apr 27, 2006 02:35 PM
Hi!

I get an error in this script when i hit this line:
owner = mtool.getMemberById(creator)

The error is:
AttributeError: Creatorstartswith

What do i have 2 change to make this script work?

Thnx!

portal_workflow "scripts" tab

Posted by Nancy Donnelly at Sep 12, 2006 01:32 PM
Plone 2.1.4 does not have a "scripts" tab in portal_workflow.

portal_workflow "scripts" tab

Posted by William Lesguillier at Sep 18, 2006 04:55 PM
When you are in portal_workflow, click on the "Contents" tab and select a worflow then you'll see the script tab.

Loged out?

Posted by Aiste Kesminaite at Oct 04, 2006 01:38 PM
Hm... if I use this I get redirected to a log in form though I'm still logged in.
All the permissions seem to be fine.

What could be wrong?

Including comments in the email

Posted by Raja sekhar at Nov 03, 2006 06:46 AM
When i used the code above it works well sending email but with out the comments.

if I replace
comments = wf_tool.getInfoFor(obj, 'comments')
with
comments = sti.kwargs.get('comment', '')
it works.


Including comments in the email

Posted by Raja sekhar at Nov 03, 2006 06:46 AM
When i used the code above it works well sending email but with out the comments.

if I replace
comments = wf_tool.getInfoFor(obj, 'comments')
with
comments = sti.kwargs.get('comment', '')
it works.


Why this how-to doesn't works?

Posted by Thierry CERETTO at Nov 21, 2006 09:25 AM
Hi,

(Zope 2.8.6-final, python 2.3.5, win32, Plone 2.1.3)

I followed this how-to step by step but it doesn't work : not email send, not error message, nothing... :((

1) I paste python provided above in "Python Script" object in "Portal_Workflow"

2) I put "sti" in parameter field of "Python Script"
Questions about this parameter :
- what is this parameter?
- for what is used it?

3) In transition "reject" I selected the script (after).

In plone, I have rejected content who use the workflow but nothing...

I checked email address : it's OK (to be sure, I use MY address for my Plone test user and I can send a page with "send page" fonction of Plone).

I put "Manager" role in "proxy" script but no more success...

In Zope, I update security setting...

I have just spent a few hours on this, help, please!

Thanks for your help,

Thierry

MailHostError

Posted by Anthony Devine at Feb 11, 2007 09:55 PM
I have tried using the altered code to send a confirmation message to the reviewer when my workflow gets to a certain state. However when I run it I get the error message.
MailHostError No message recipients designated. Are there any suggestions as to why this has happened

MailHostError

Posted by Michael Graf at Feb 20, 2007 04:52 PM
Maybe you use LDAP to login, be sure that your user have an email-adress ;-)

Proxy roles, workflow scripts and Plone 2.5.2/Zope 2.9.6

Posted by Michael Baltaks at Mar 26, 2007 10:19 PM
I spent some time solving this one. You now need Manager role access to read state_change.new_state, state_change.old_state and state_change.transition, but Manager role set as proxy doesn't work. This seems related to this problem: http://www.zope.org/Collectors/Zope/1779. There is a part workaround here: http://mail.zope.org/[…]/022152.html, but it only helps for some of the state_change attributes. Fortunately I was able to read state_change.status["review_state"] to replace state_change.new_state.getId().

Here is my not proxied script:

## Script (Python) "sendWorkflowNotification"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind subpath=traverse_subpath
##parameters=state_change
##title=User part of email notification
##
object = getattr(state_change, 'object')
state = state_change
comment=state_change.kwargs.get('comment', 'No comments')
context.proxied_sendWorkflowNotification(object, state, comment)


And the proxied script:

## Script (Python) "proxied_sendWorkflowNotification"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind subpath=traverse_subpath
##parameters=object, state, comment
##title=Heavy lifting of notification
##
mship = context.portal_membership
mhost = context.MailHost

ADUsers = context.acl_users.LDAPMulti.acl_users

#users = context.plone_utils.getInheritedLocalRoles(object)

#new_state = state.new_state.getId()
# Plone workflows use variable called "review_state" to store state id
# of the object state
new_state = state.status["review_state"]

if (new_state == 'Reviewing'):
    notify_role = 'Member'
    awaiting = 'review'
elif (new_state == 'Scrub'):
    notify_role = 'Reviewer'
    awaiting = 'scrub'
else:
    notify_role = 'Manager'
    awaiting = 'debugging'

local_users = object.computeRoleMap()
group_users = ADUsers.getGroupedUsers([(notify_role, notify_role)])

users = []

for u in group_users:
    users.append(u.id)

for u in local_users:
    if (notify_role in u["local"]):
        users.append(u["id"])

# the message format, %s will be filled in from data
message = """
From: %s
To: %s
Subject: CTO Office item awaiting %s - %s

%s

%s

%s

URL: %s
"""

authorid = mship.getAuthenticatedMember().getUserName()
author = ADUsers.getUserById(authorid).fullname
note = author + " has submitted this item for " + awaiting + ", with comment: " \
+ comment

for userid in users:
    user = ADUsers.getUserById(userid)
    if user.email:
        msg = message % (
             context.email_from_address,
             user.email,
             awaiting,
             object.TitleOrId(),
             note,
             object.TitleOrId(),
             object.Description(),
             object.absolute_url()
             )
        mhost.send(msg)

property access api for users

Posted by Kapil Thangavelu at Apr 25, 2007 03:53 PM
you should be using

user.getProperty('email')

user.email is an implementation detail thats not guaranteed to work.

Sending mail to all users

Posted by Anthony Devine at Apr 09, 2007 02:38 PM
Is there any way of getting that above code and sending an email to all users with a certain permission, instead of just 1 reviewer? I have tried the mail.py script in defin guide to plone but when I run it, it comes up asking me to enter in user name and password when I already have all of the correct permissions.

Make called email script and External Method

Posted by Ken Wasetis at ContextualCorp.com at Jun 04, 2007 04:09 PM
I think that if you make your called script (the one actually getting the email addresses and sending the email) an external method, that it'll execute outside the Zope guards and will not think your user doesn't have the appropriate permissions.

Getting around permissions problems with an external method

Posted by Russell Miller at Jun 27, 2007 03:28 PM
I was consistently being asked for username and password whenever I tried to submit or reject, and ultimately the transition would fail with this error: "You are not allowed to access 'object' in this context".

After a lot of trial and error with scripts and suggestions from this how-to, the following configuration is now working for me.

I created a python script (placing it in the scripts folder of the workflow) called submit_redirect, the entirety of which is:

context.submitNotification(state_change, context)

It takes the parameter "state_change".

Then I rewrote (slightly) the how-to script to suit my own purposes, and added it to the scripts folder as an external method called submitNotification:

def submitNotification(self, state, context):

   obj = state.object
   creator = obj.Creator()
   history = state.getHistory()
   wf_tool = context.portal_workflow

   mMsg = """
This is an automated submission notification. You may reply
to the sender of this message if you have questions.

Website content has been submitted for review.
The content was submitted by %s.
The url is %s.

To review this page, use this login form:
%s
   """

   member = context.portal_membership.getMemberById(creator)
   creator = {'member':member,
   'id':member.getId(),
   'fullname':member.getProperty('fullname', 'Fullname missing'),
   'email':member.getProperty('email', None)
   }

   actorid = wf_tool.getInfoFor(obj, 'actor')
   actor = context.portal_membership.getMemberById(actorid)
   reviewer = {'member':actor,
   'id':actor.getId(),
   'fullname':actor.getProperty('fullname', 'Fullname missing'),
   'email':actor.getProperty('email', None)}

   mTo = reviewer['email']
   mFrom = creator['email']
   mSubj = 'Website content awaits review'
   obj_url = obj.absolute_url() #use portal_url + relative_url
   obj_url_login = obj.absolute_url() + "/login_form"
   creatorName= creator['fullname']

   message = mMsg % (creatorName, obj_url, obj_url_login)
   context.MailHost.send(message, mTo, mFrom, mSubj)

There are no proxy permissions set on either script. (I couldn't make the proxies work for me.)

I set up a similar pair of scripts for the reject transition. (Because I find this less confusing than trying to write an all-purpose transition script.)

Help With This?

Posted by Jason Silver at Sep 26, 2007 08:01 PM
I've followed these "how to" instructions on my test server, and everything works perfectly! I copied the changes over to the live server, and it doesn't work at all.

When using the "How To" example I get the common Insufficient Privileges error message. The script is set to run Proxy as manager.

So I tried the other examples listed below... and I think I may have reached the summit of my own abilities. I have a couple of questions:

How would I do this? :
  "...and added it to the scripts folder as an external method called submitNotification: ..."

I tried just creating another script and pasting your contents into that, but I receive the following error:

TypeError: submit_redirect() takes no arguments (1 given)

Thanks,
Jason

Adding an external method

Posted by Russell Miller at Sep 26, 2007 08:30 PM
To add an external method to the scripts folder you need to save it as a file (in this case called submitNotification.py) and copy it to the Extensions directory of your instance. (In my case, this was: /opt/Plone-2.5.2/zeocluster/client1/Extensions.) Now when you select "External Method" from the drop-down Add menu of the scripts folder, supply data as follows:

Id: submitNotification
Title: [anything you want]
Module Name: submitNotification
Function Name: submitNotification

Plone looks in the Extensions folder and finds submitNotification.py, and so allows you to add the external method.

This is how it worked for me, anyway. (My source for how to add an external method was the Plone Book, starting on page 148 -- 6.1.2.2 Using External Method Objects.)

Name Error - Context Not Defined

Posted by Jason Silver at Sep 27, 2007 08:31 PM
Thank you so much for taking the time to help me with this. I'm a lot closer-- I can feel it! :)

I'm now receiving an error when I attempt to add the external method:

    NameError: name 'context' is not defined

Thanks again for your assistance,
Jason

I Found This-- didn't help a lot

Posted by Jason Silver at Sep 27, 2007 08:53 PM
I found some suggestions on this thread about context errors...

http://starship.python.net/[…]/001235.html

Didn't help me though... any thoughts?

Jason

look up about 2/3 of the page :)

Posted by Rob Lineberger at Sep 29, 2007 03:03 AM
As I said a year ago (and technically, the post script to the trizpug thread you mentioned ):

My code was on the file system as part of an AT-based product. If I replace "context" with "self," it works. I suppose that makes sense because context is not within the portal for filesystem code.

No Argument, or Permission Errors

Posted by Jason Silver at Oct 01, 2007 01:48 PM
Thanks for the help you've all given me so far. I really appreciate it!

I've created the Python script on the file system (submitNotification.py) as instructed, and everything seems to be set up correctly, but I still get an error.

There have been suggestions to change context to self-- and when I do this, I am still not getting it to work (Error: submit_redirect() takes no arguments (1 given)).

Here's my submitNotification.py script:

<code>
def submitNotification(self, state, context):
        obj = state.object
        creator = obj.Creator()
        history = state.getHistory()
        wf_tool = context.portal_workflow

mMsg = """
This is an automated submission notification. You may reply
to the sender of this message if you have questions.

Website content has been submitted for review.
The content was submitted by %s.
The url is %s.

To review this page, use this login form:
%s
"""

member = context.portal_membership.getMemberById(creator)
creator = {'member':member,
'id':member.getId(),
'fullname':member.getProperty('fullname', 'Fullname missing'),
'email':member.getProperty('email', None)
}

actorid = wf_tool.getInfoFor(obj, 'actor')
actor = context.portal_membership.getMemberById(actorid)
reviewer = {'member':actor,
'id':actor.getId(),
'fullname':actor.getProperty('fullname', 'Fullname missing'),
'email':actor.getProperty('email', None)}

mTo = reviewer['email']
mFrom = creator['email']
mSubj = 'Website content awaits review'
obj_url = obj.absolute_url() #use portal_url + relative_url
obj_url_login = obj.absolute_url() + "/login_form"
creatorName= creator['fullname']

message = mMsg % (creatorName, obj_url, obj_url_login)
context.MailHost.send(message, mTo, mFrom, mSubj)
</code>

I've tried changing every occurrence of context to self, but this creates new errors because then self is in there twice. So I removed the duplicate self, still get errors. I've also just changed context to self in the redirect script-- still have errors.

I went back to the first script and changed context to self in that one, but still have permissions errors. The scripts are supposedly running in proxy mode of manager. I'm also the owner, so I don't know why there'd be a permission error in the first place?

It's perplexing to me, since it works on the test server and not on the main server.

Thank you again,
Jason Silver

This is a confusing problem

Posted by Rob Lineberger at Oct 16, 2007 03:14 PM
This is what I would do in your shoes. You say you've done this, and I believe you, but try changing the def to:
def submitNotification(self):
Then replace all instances of "context" with "self".
After that, restart zope and then re-install your product. Sometimes it "remembers" additional parameters.
Also, if you have a metadata file associated with this script, check if bind context and bind state are messing you up. I *think* state is passed automatically if this is a .cpy and not a .py.

Really Wanted This to Work

Posted by Jason Silver at Oct 16, 2007 04:34 PM
Thanks Rob, I was so close... really thought you might have solved it here. You may have found the problem, but I don't understand all of the solution (the last couple lines)

I'm still getting the same error:

Site error
his site encountered an error trying to fulfill your request. The errors were:
Error Type
    AttributeError
Error Value
    submitNotification
Request made at
    2007/10/16 12:22:56.899 GMT-4


To review, let me go over the steps to be sure I'm not missing anything.

1. I created a script (Python) in the ZMI
   a. named submit_redirect
   b. Titled Submission Redirect
   c. Parameter list: state_change
   d. Content: (single line) context.submitNotification(state_change, self)
      (I tried changing context to self here, but that didn't work)
2. In a terminal, I created a file in Extensions
   a. named it submitNotifications.py
   c. with the following content (bottom of this page)
3. I restarted Zope
3. What product do I reinstall?

Here's the script:

def submitNotification(self):
        obj = state.object
        creator = obj.Creator()
        history = state.getHistory()
        wf_tool = self.portal_workflow

mMsg = """
This is an automated submission notification. You may reply
to the sender of this message if you have questions.

Website content has been submitted for review.
The content was submitted by %s.
The url is %s.

To review this page, use this login form:
%s
"""

member = self.portal_membership.getMemberById(creator)
creator = {'member':member,
'id':member.getId(),
'fullname':member.getProperty('fullname', 'Fullname missing'),
'email':member.getProperty('email', None)
}

actorid = wf_tool.getInfoFor(obj, 'actor')
actor = self.portal_membership.getMemberById(actorid)
reviewer = {'member':actor,
'id':actor.getId(),
'fullname':actor.getProperty('fullname', 'Fullname missing'),
'email':actor.getProperty('email', None)}
    
mTo = reviewer['email']
mFrom = creator['email']
mSubj = 'Website content awaits review'
obj_url = obj.absolute_url() #use portal_url + relative_url
obj_url_login = obj.absolute_url() + "/login_form"
creatorName= creator['fullname']
    
message = mMsg % (creatorName, obj_url, obj_url_login)
self.MailHost.send(message, mTo, mFrom, mSubj)

Thank you, this method worked for me

Posted by Bryan White at Sep 29, 2008 09:05 PM
Using the external method and redirect script as outlined above worked perfectly for me - thank you. This is with Plone 2.5.x.

Works with Plone 3?

Posted by David Godin at Feb 13, 2008 03:54 PM
Hi,

Great tip! Does this still work with Plone 3.0.x?

Plone 3 has built in content rules support.

Posted by Silvio Tomatis at Apr 14, 2008 12:59 PM
Check this post
http://maurits.vanrees.org/[…]/content-rules
about content rules in Plone 3.
No more python required for most basic tasks (sending email, moving content, notifying the user or logging).
You can define your own actions and conditions if you need them.
See http://plone.org/[…]/

Insufficient Privileges

Posted by Riffat at Aug 11, 2008 07:34 AM
I am using plone 3. I do the same above steps to costomize the default plone workflow. select the portal_workflow then simple_publications (which is the default workflow for plone 3) then scripts and add python script ad mentioned above set the proxy server as Manager and attach the script reject_notification to the reject transition.now when a user under role contributor add a news item and manager rejects then this error
Insufficient Privileges
"You do not have sufficient privileges to view this page. If you believe you are receiving this message in error, contact the site administration. "
help me!!!!!!!!!!