appy.bin: adapted job.py for Appy >0.8; appy.gen: improved mail notification mechanism.

This commit is contained in:
Gaetan Delannay 2012-05-03 10:51:54 +02:00
parent 9b8064b0cd
commit 459a714b76
11 changed files with 168 additions and 147 deletions

View file

@ -6,6 +6,7 @@ from appy import Object
from appy.gen.layout import Table
from appy.gen.layout import defaultFieldLayouts
from appy.gen.po import PoMessage
from appy.gen.mail import sendNotification
from appy.gen.utils import GroupDescr, Keywords, getClassName, SomeObjects
import appy.pod
from appy.pod.renderer import Renderer
@ -1770,7 +1771,7 @@ class Ref(Type):
if not res: return res
# We add here specific Ref rules for preventing to show the field under
# some inappropriate circumstances.
if (layoutType == 'edit') and self.add: return False
if (layoutType == 'edit') and (self.add or not self.link): return False
if self.isBack:
if layoutType == 'edit': return False
else: return getattr(obj.aq_base, self.name, None)
@ -1873,7 +1874,15 @@ class Ref(Type):
# Insert p_value into it.
uid = value.o.UID()
if uid not in refs:
refs.append(uid)
# Where must we insert the object? At the start? At the end?
if callable(self.add):
add = self.callMethod(obj, self.add)
else:
add = self.add
if add == 'start':
refs.insert(0, uid)
else:
refs.append(uid)
# Update the back reference
if not back: self.back.linkObject(value, obj, back=True)
@ -2125,7 +2134,7 @@ class Pod(Type):
'contact the system administrator.'
DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.'
def __init__(self, validator=None, index=None, default=None,
optional=False, editDefault=False, show='view',
optional=False, editDefault=False, show=('view', 'result'),
page='main', group=None, layouts=None, move=0, indexed=False,
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
@ -2634,7 +2643,7 @@ class Transition:
performed before calling this method). If p_doAction is False, the
action that must normally be executed after the transition has been
triggered will not be executed. If p_doNotify is False, the
notifications (email,...) that must normally be launched after the
email notifications that must normally be launched after the
transition has been triggered will not be launched. If p_doHistory is
False, there will be no trace from this transition triggering in the
workflow history. If p_doSay is False, we consider the transition is
@ -2674,8 +2683,8 @@ class Transition:
msg = ''
if doAction and self.action: msg = self.executeAction(obj, wf)
# Send notifications if needed
if doNotify and self.notify and obj.getTool(True).enableNotifications:
notifier.sendMail(obj.appy(), self, transitionName, wf)
if doNotify and self.notify and obj.getTool(True).mailEnabled:
sendNotification(obj.appy(), self, transitionName, wf)
# Return a message to the user if needed
if not doSay or (transitionName == '_init_'): return
if not msg: msg = 'Changes saved.' # XXX Translate

117
gen/mail.py Normal file
View file

@ -0,0 +1,117 @@
'''This package contains functions for sending email notifications.'''
import smtplib
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email import Encoders
from email.Header import Header
from appy.shared.utils import sequenceTypes
# ------------------------------------------------------------------------------
def sendMail(tool, to, subject, body, attachments=None):
'''Sends a mail, via p_tool.mailHost, to p_to (a single email address or a
list of email addresses).'''
# Just log things if mail is disabled
fromAddress = tool.mailFrom
if not tool.mailEnabled:
tool.log('Mail disabled: should send mail from %s to %s.' % \
(fromAddress, str(to)))
tool.log('Subject: %s' % subject)
tool.log('Body: %s' % body)
if attachments:
tool.log('%d attachment(s).' % len(attachments))
return
tool.log('Sending mail from %s to %s (subject: %s).' % \
(fromAddress, str(to), subject))
# Create the base MIME message
body = MIMEText(body, 'plain', 'utf-8')
if attachments:
msg = MIMEMultipart()
msg.attach( body )
else:
msg = body
# Add the header values
msg['Subject'] = Header(subject, 'utf-8')
msg['From'] = fromAddress
if isinstance(to, basestring):
msg['To'] = to
else:
if len(to) == 1:
msg['To'] = to[0]
else:
msg['To'] = fromAddress
msg['Bcc'] = ', '.join(to)
to = fromAddress
# Add attachments
if attachments:
for fileName, fileContent in attachments:
part = MIMEBase('application', 'octet-stream')
if hasattr(fileContent, 'data'):
# It is a File instance coming from the database
data = fileContent.data
if isinstance(data, basestring):
payLoad = data
else:
payLoad = ''
while data is not None:
payLoad += data.data
data = data.next
else:
payLoad = fileContent
part.set_payload(payLoad)
Encoders.encode_base64(part)
part.add_header('Content-Disposition',
'attachment; filename="%s"' % fileName)
msg.attach(part)
# Send the email
try:
mh = smtplib.SMTP(tool.mailHost)
mh.sendmail(fromAddress, [to], msg.as_string())
mh.quit()
except smtplib.SMTPException, e:
tool.log('Mail sending failed: %s' % str(e))
# ------------------------------------------------------------------------------
def sendNotification(obj, transition, transitionName, workflow):
'''Sends mail about p_transition named p_transitionName, that has been
triggered on p_obj that is controlled by p_workflow.'''
from appy.gen.descriptors import WorkflowDescriptor
wfName = WorkflowDescriptor.getWorkflowName(workflow.__class__)
zopeObj = obj.o
tool = zopeObj.getTool()
mailInfo = transition.notify(workflow, obj)
if not mailInfo[0]: return # Send a mail to nobody.
# mailInfo may be one of the following:
# (to,)
# (to, cc)
# (to, mailSubject, mailBody)
# (to, cc, mailSubject, mailBody)
# "to" and "cc" maybe simple strings (one simple string = one email
# address or one role) or sequences of strings.
# Determine mail subject and body.
if len(mailInfo) <= 2:
# The user didn't mention mail body and subject. We will use those
# defined from i18n labels.
wfHistory = zopeObj.getHistory()
labelPrefix = '%s_%s' % (wfName, transitionName)
tName = obj.translate(labelPrefix)
keys = {'siteUrl': tool.getPath('/').absolute_url(),
'siteTitle': tool.getAppName(),
'objectUrl': zopeObj.absolute_url(),
'objectTitle': zopeObj.Title(),
'transitionName': tName,
'transitionComment': wfHistory[0]['comments']}
mailSubject = obj.translate(labelPrefix + '_mail_subject', keys)
mailBody = obj.translate(labelPrefix + '_mail_body', keys)
else:
mailSubject = mailInfo[-1]
mailBody = mailInfo[-2]
# Determine "to" and "cc".
to = mailInfo[0]
cc = []
if (len(mailInfo) in (2,4)) and mailInfo[1]: cc = mailInfo[1]
if type(to) not in sequenceTypes: to = [to]
if type(cc) not in sequenceTypes: cc = [cc]
# Send the mail
sendMail(tool.appy(), to, mailSubject, mailBody)
# ------------------------------------------------------------------------------

View file

@ -1356,6 +1356,8 @@ class BaseMixin:
elif format == 'js':
res = text.replace('\r\n', '').replace('\n', '')
res = res.replace("'", "\\'")
elif format == 'text':
res = text.replace('<br/>', '\n')
else:
res = text
return res

View file

@ -209,10 +209,10 @@ toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns',
'enableAdvancedSearch', 'numberOfSearchColumns',
'searchFields', 'optionalFields', 'showWorkflow',
'showAllStatesInPhase')
defaultToolFields = ('title', 'users', 'groups', 'translations', 'pages',
'enableNotifications', 'unoEnabledPython','openOfficePort',
'numberOfResultsPerPage', 'listBoxesMaximumWidth',
'appyVersion')
defaultToolFields = ('title', 'unoEnabledPython','openOfficePort',
'numberOfResultsPerPage', 'mailHost', 'mailEnabled',
'mailFrom', 'appyVersion', 'users', 'groups',
'translations', 'pages')
class Tool(ModelClass):
# In a ModelClass we need to declare attributes in the following list.
@ -222,11 +222,12 @@ class Tool(ModelClass):
# Tool attributes
title = gen.String(show=False, page=gen.Page('main', show=False))
def validPythonWithUno(self, value): pass # Real method in the wrapper
unoEnabledPython = gen.String(group="connectionToOpenOffice",
validator=validPythonWithUno)
openOfficePort = gen.Integer(default=2002, group="connectionToOpenOffice")
unoEnabledPython = gen.String(validator=validPythonWithUno)
openOfficePort = gen.Integer(default=2002)
numberOfResultsPerPage = gen.Integer(default=30)
listBoxesMaximumWidth = gen.Integer(default=100)
mailHost = gen.String(default='localhost:25')
mailEnabled = gen.Boolean(default=False)
mailFrom = gen.String(default='info@appyframework.org')
appyVersion = gen.String(show=False, layouts='f')
# Ref(User) will maybe be transformed into Ref(CustomUserClass).
users = gen.Ref(User, multiplicity=(0,None), add=True, link=False,
@ -246,8 +247,6 @@ class Tool(ModelClass):
pages = gen.Ref(Page, multiplicity=(0,None), add=True, link=False,
show='view', back=gen.Ref(attribute='toTool3', show=False),
page=gen.Page('pages', show='view'))
enableNotifications = gen.Boolean(default=True,
page=gen.Page('notifications', show=False))
@classmethod
def _appy_clean(klass):

View file

@ -1,105 +0,0 @@
'''This package contains functions for sending email notifications.'''
# ------------------------------------------------------------------------------
def getEmailAddress(name, email, encoding='utf-8'):
'''Creates a full email address from a p_name and p_email.'''
res = email
if name: res = name.decode(encoding) + ' <%s>' % email
return res
def convertRolesToEmails(users, portal):
'''p_users is a list of emails and/or roles. This function returns the same
list, where all roles have been expanded to emails of users having this
role (more precisely, users belonging to the group Appy created for the
given role).'''
res = []
for mailOrRole in users:
if mailOrRole.find('@') != -1:
# It is an email. Append it directly to the result.
res.append(mailOrRole)
else:
# It is a role. Find the corresponding group (Appy creates
# one group for every role defined in the application).
groupId = mailOrRole + '_group'
group = portal.acl_users.getGroupById(groupId)
if group:
for user in group.getAllGroupMembers():
userMail = user.getProperty('email')
if userMail and (userMail not in res):
res.append(userMail)
return res
# ------------------------------------------------------------------------------
SENDMAIL_ERROR = 'Error while sending mail: %s.'
ENCODING_ERROR = 'Encoding error while sending mail: %s.'
import socket
from appy.shared.utils import sequenceTypes
from appy.gen.descriptors import WorkflowDescriptor
def sendMail(obj, transition, transitionName, workflow):
'''Sends mail about p_transition that has been triggered on p_obj that is
controlled by p_workflow.'''
wfName = WorkflowDescriptor.getWorkflowName(workflow.__class__)
zopeObj = obj.o
tool = zopeObj.getTool()
mailInfo = transition.notify(workflow, obj)
if not mailInfo[0]: return # Send a mail to nobody.
# mailInfo may be one of the following:
# (to,)
# (to, cc)
# (to, mailSubject, mailBody)
# (to, cc, mailSubject, mailBody)
# "to" and "cc" maybe simple strings (one simple string = one email
# address or one role) or sequences of strings.
# Determine mail subject and body.
if len(mailInfo) <= 2:
# The user didn't mention mail body and subject. We will use those
# defined from i18n labels.
wfHistory = zopeObj.getHistory()
labelPrefix = '%s_%s' % (wfName, transitionName)
tName = obj.translate(labelPrefix)
keys = {'siteUrl': tool.getPath('/').absolute_url(),
'siteTitle': tool.getAppName(),
'objectUrl': zopeObj.absolute_url(),
'objectTitle': zopeObj.Title(),
'transitionName': tName,
'transitionComment': wfHistory[0]['comments']}
mailSubject = obj.translate(labelPrefix + '_mail_subject', keys)
mailBody = obj.translate(labelPrefix + '_mail_body', keys)
else:
mailSubject = mailInfo[-1]
mailBody = mailInfo[-2]
# Determine "to" and "cc".
to = mailInfo[0]
cc = []
if (len(mailInfo) in (2,4)) and mailInfo[1]: cc = mailInfo[1]
if type(to) not in sequenceTypes: to = [to]
if type(cc) not in sequenceTypes: cc = [cc]
# Among "to" and "cc", convert all roles to concrete email addresses
to = convertRolesToEmails(to, portal)
cc = convertRolesToEmails(cc, portal)
# Determine "from" address
enc= portal.portal_properties.site_properties.getProperty('default_charset')
fromAddress = getEmailAddress(
portal.getProperty('email_from_name'),
portal.getProperty('email_from_address'), enc)
# Send the mail
i = 0
for recipient in to:
i += 1
try:
if i != 1: cc = []
portal.MailHost.secureSend(mailBody.encode(enc),
recipient.encode(enc), fromAddress.encode(enc),
mailSubject.encode(enc), mcc=cc, charset='utf-8')
except socket.error, sg:
obj.log(SENDMAIL_ERROR % str(sg), type='warning')
break
except UnicodeDecodeError, ue:
obj.log(ENCODING_ERROR % str(ue), type='warning')
break
except Exception, e:
obj.log(SENDMAIL_ERROR % str(e), type='warning')
break
# ------------------------------------------------------------------------------

View file

@ -60,7 +60,8 @@ img {border: 0}
.portletSep { border-top: 1px solid #5F7983; margin-top: 2px;}
.portletPage { font-style: italic; }
.portletGroup { font-variant: small-caps; font-weight: bold; font-style: normal;
margin: 0 0 0.2em 0; }
margin-top: 0.1em }
.portletSearch { font-size: 90%; font-style: italic; padding-left: 1em}
.phase { border-style: dashed; border-width: thin; padding: 4px 0.6em 5px 1em;}
.phaseSelected { background-color: #F4F5F6; }
.content { padding: 14px 14px 9px 15px;}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

View file

@ -64,20 +64,20 @@
<tal:expanded define="group searchOrGroup;
expanded python: request.get(group['labelId'], 'collapsed') == 'expanded'">
<tal:comment replace="nothing">Group name</tal:comment>
<dt class="portletAppyItem portletGroup">
<img align="left" style="cursor:pointer"
<dt class="portletGroup">
<img align="left" style="cursor:pointer; margin-right: 3px"
tal:attributes="id python: '%s_img' % group['labelId'];
src python:test(expanded, 'ui/collapse.gif', 'ui/expand.gif');
onClick python:'toggleCookie(\'%s\')' % group['labelId']"/>&nbsp;
onClick python:'toggleCookie(\'%s\')' % group['labelId']"/>
<span tal:replace="group/label"/>
</dt>
<tal:comment replace="nothing">Group searches</tal:comment>
<span tal:attributes="id group/labelId;
style python:test(expanded, 'display:block', 'display:none')">
<dt class="portletAppyItem portletSearch portletGroupItem" tal:repeat="search group/searches">
<dt class="portletSearch" tal:repeat="search group/searches">
<a tal:attributes="href python: '%s?className=%s&search=%s' % (queryUrl, rootClass, search['name']);
title search/descr;
class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
title search/descr;
class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
tal:content="structure search/label"></a>
</dt>
</span>

View file

@ -129,7 +129,6 @@
canWrite python: not appyType['isBack'] and contextObj.allows(appyType['writePermission']);
showPlusIcon python: contextObj.mayAddReference(fieldName, folder);
atMostOneRef python: (appyType['multiplicity'][1] == 1) and (len(objs)&lt;=1);
label python: contextObj.translate('label', field=appyType);
addConfirmMsg python: appyType['addConfirm'] and _('%s_addConfirm' % appyType['labelId']) or '';
navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)">
@ -142,9 +141,6 @@
<tal:comment replace="nothing">Display a simplified widget if maximum number of
referenced objects is 1.</tal:comment>
<table><tr valign="top">
<td><span class="appyLabel" tal:condition="python: not innerRef and not appyType['link']"
tal:content="structure label"></span></td>
<tal:comment replace="nothing">If there is no object...</tal:comment>
<tal:noObject condition="not:objs">
<td tal:content="python: _('no_ref')"></td>

View file

@ -46,9 +46,7 @@ class AbstractWrapper(object):
elif name == 'url': return self.o.absolute_url()
elif name == 'state': return self.o.State()
elif name == 'stateLabel':
o = self.o
appName = o.getProductConfig().PROJECTNAME
return o.translate(o.getWorkflowLabel(), domain=appName)
return self.o.translate(self.o.getWorkflowLabel())
elif name == 'history':
o = self.o
key = o.workflow_history.keys()[0]