appy.bin: adapted job.py for Appy >0.8; appy.gen: improved mail notification mechanism.
This commit is contained in:
parent
9b8064b0cd
commit
459a714b76
26
bin/job.py
26
bin/job.py
|
@ -6,12 +6,13 @@
|
||||||
<ZopeAdmin> is the userName of the Zope administrator for this instance.
|
<ZopeAdmin> is the userName of the Zope administrator for this instance.
|
||||||
<PloneInstancePath> is the path, within Zope, to the Plone Site object (if
|
<PloneInstancePath> is the path, within Zope, to the Plone Site object (if
|
||||||
not at the root of the Zope hierarchy, use '/' as
|
not at the root of the Zope hierarchy, use '/' as
|
||||||
folder separator);
|
folder separator); leave blank if using appy.gen > 0.8
|
||||||
|
|
||||||
<ApplicationName> is the name of the Appy application. If it begins with
|
<ApplicationName> is the name of the Appy application. If it begins with
|
||||||
"path=", it does not represent an Appy application, but
|
"path=", it does not represent an Appy application, but
|
||||||
the path, within <PloneInstancePath>, to any Zope object
|
the path, within <PloneInstancePath>, to any Zope object
|
||||||
(use '/' as folder separator)
|
(use '/' as folder separator); leave blank if using
|
||||||
|
appy.gen > 0.8;
|
||||||
|
|
||||||
<ToolMethodName> is the name of the method to call on the tool in this
|
<ToolMethodName> is the name of the method to call on the tool in this
|
||||||
Appy application, or the method to call on the arbitrary
|
Appy application, or the method to call on the arbitrary
|
||||||
|
@ -21,7 +22,7 @@
|
||||||
are supported). Several arguments must be separated by '*'.
|
are supported). Several arguments must be separated by '*'.
|
||||||
|
|
||||||
Note that you can also specify several commands, separated with
|
Note that you can also specify several commands, separated with
|
||||||
semicolons (";"). This scripts performes a single commit after all commands
|
semicolons (";"). This scripts performs a single commit after all commands
|
||||||
have been executed.
|
have been executed.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -58,18 +59,21 @@ else:
|
||||||
if not hasattr(user, 'aq_base'):
|
if not hasattr(user, 'aq_base'):
|
||||||
user = user.__of__(app.acl_users)
|
user = user.__of__(app.acl_users)
|
||||||
newSecurityManager(None, user)
|
newSecurityManager(None, user)
|
||||||
# Get the Plone site
|
# Find the root object.
|
||||||
ploneSite = app # Initialised with the Zope root object.
|
rootObject = app # Initialised with the Zope root object.
|
||||||
|
if plonePath:
|
||||||
for elem in plonePath.split('/'):
|
for elem in plonePath.split('/'):
|
||||||
ploneSite = getattr(ploneSite, elem)
|
rootObject = getattr(rootObject, elem)
|
||||||
# If we are in a Appy application, the object on which we will call the
|
# If we are in a Appy application, the object on which we will call the
|
||||||
# method is the tool within this application.
|
# method is the config object on this root object.
|
||||||
if not appName.startswith('path='):
|
if not appName:
|
||||||
|
targetObject = rootObject.data.appy()
|
||||||
|
elif not appName.startswith('path='):
|
||||||
objectName = 'portal_%s' % appName.lower()
|
objectName = 'portal_%s' % appName.lower()
|
||||||
targetObject = getattr(ploneSite, objectName).appy()
|
targetObject = getattr(rootObject, objectName).appy()
|
||||||
else:
|
else:
|
||||||
# It can be any object within the Plone site.
|
# It can be any object.
|
||||||
targetObject = ploneSite
|
targetObject = rootObject
|
||||||
for elem in appName[5:].split('/'):
|
for elem in appName[5:].split('/'):
|
||||||
targetObject = getattr(targetObject, elem)
|
targetObject = getattr(targetObject, elem)
|
||||||
# Execute the method on the target object
|
# Execute the method on the target object
|
||||||
|
|
|
@ -6,6 +6,7 @@ from appy import Object
|
||||||
from appy.gen.layout import Table
|
from appy.gen.layout import Table
|
||||||
from appy.gen.layout import defaultFieldLayouts
|
from appy.gen.layout import defaultFieldLayouts
|
||||||
from appy.gen.po import PoMessage
|
from appy.gen.po import PoMessage
|
||||||
|
from appy.gen.mail import sendNotification
|
||||||
from appy.gen.utils import GroupDescr, Keywords, getClassName, SomeObjects
|
from appy.gen.utils import GroupDescr, Keywords, getClassName, SomeObjects
|
||||||
import appy.pod
|
import appy.pod
|
||||||
from appy.pod.renderer import Renderer
|
from appy.pod.renderer import Renderer
|
||||||
|
@ -1770,7 +1771,7 @@ class Ref(Type):
|
||||||
if not res: return res
|
if not res: return res
|
||||||
# We add here specific Ref rules for preventing to show the field under
|
# We add here specific Ref rules for preventing to show the field under
|
||||||
# some inappropriate circumstances.
|
# 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 self.isBack:
|
||||||
if layoutType == 'edit': return False
|
if layoutType == 'edit': return False
|
||||||
else: return getattr(obj.aq_base, self.name, None)
|
else: return getattr(obj.aq_base, self.name, None)
|
||||||
|
@ -1873,6 +1874,14 @@ class Ref(Type):
|
||||||
# Insert p_value into it.
|
# Insert p_value into it.
|
||||||
uid = value.o.UID()
|
uid = value.o.UID()
|
||||||
if uid not in refs:
|
if uid not in refs:
|
||||||
|
# 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)
|
refs.append(uid)
|
||||||
# Update the back reference
|
# Update the back reference
|
||||||
if not back: self.back.linkObject(value, obj, back=True)
|
if not back: self.back.linkObject(value, obj, back=True)
|
||||||
|
@ -2125,7 +2134,7 @@ class Pod(Type):
|
||||||
'contact the system administrator.'
|
'contact the system administrator.'
|
||||||
DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.'
|
DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.'
|
||||||
def __init__(self, validator=None, index=None, default=None,
|
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,
|
page='main', group=None, layouts=None, move=0, indexed=False,
|
||||||
searchable=False, specificReadPermission=False,
|
searchable=False, specificReadPermission=False,
|
||||||
specificWritePermission=False, width=None, height=None,
|
specificWritePermission=False, width=None, height=None,
|
||||||
|
@ -2634,7 +2643,7 @@ class Transition:
|
||||||
performed before calling this method). If p_doAction is False, the
|
performed before calling this method). If p_doAction is False, the
|
||||||
action that must normally be executed after the transition has been
|
action that must normally be executed after the transition has been
|
||||||
triggered will not be executed. If p_doNotify is False, the
|
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
|
transition has been triggered will not be launched. If p_doHistory is
|
||||||
False, there will be no trace from this transition triggering in the
|
False, there will be no trace from this transition triggering in the
|
||||||
workflow history. If p_doSay is False, we consider the transition is
|
workflow history. If p_doSay is False, we consider the transition is
|
||||||
|
@ -2674,8 +2683,8 @@ class Transition:
|
||||||
msg = ''
|
msg = ''
|
||||||
if doAction and self.action: msg = self.executeAction(obj, wf)
|
if doAction and self.action: msg = self.executeAction(obj, wf)
|
||||||
# Send notifications if needed
|
# Send notifications if needed
|
||||||
if doNotify and self.notify and obj.getTool(True).enableNotifications:
|
if doNotify and self.notify and obj.getTool(True).mailEnabled:
|
||||||
notifier.sendMail(obj.appy(), self, transitionName, wf)
|
sendNotification(obj.appy(), self, transitionName, wf)
|
||||||
# Return a message to the user if needed
|
# Return a message to the user if needed
|
||||||
if not doSay or (transitionName == '_init_'): return
|
if not doSay or (transitionName == '_init_'): return
|
||||||
if not msg: msg = 'Changes saved.' # XXX Translate
|
if not msg: msg = 'Changes saved.' # XXX Translate
|
||||||
|
|
117
gen/mail.py
Normal file
117
gen/mail.py
Normal 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)
|
||||||
|
# ------------------------------------------------------------------------------
|
|
@ -1356,6 +1356,8 @@ class BaseMixin:
|
||||||
elif format == 'js':
|
elif format == 'js':
|
||||||
res = text.replace('\r\n', '').replace('\n', '')
|
res = text.replace('\r\n', '').replace('\n', '')
|
||||||
res = res.replace("'", "\\'")
|
res = res.replace("'", "\\'")
|
||||||
|
elif format == 'text':
|
||||||
|
res = text.replace('<br/>', '\n')
|
||||||
else:
|
else:
|
||||||
res = text
|
res = text
|
||||||
return res
|
return res
|
||||||
|
|
19
gen/model.py
19
gen/model.py
|
@ -209,10 +209,10 @@ toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns',
|
||||||
'enableAdvancedSearch', 'numberOfSearchColumns',
|
'enableAdvancedSearch', 'numberOfSearchColumns',
|
||||||
'searchFields', 'optionalFields', 'showWorkflow',
|
'searchFields', 'optionalFields', 'showWorkflow',
|
||||||
'showAllStatesInPhase')
|
'showAllStatesInPhase')
|
||||||
defaultToolFields = ('title', 'users', 'groups', 'translations', 'pages',
|
defaultToolFields = ('title', 'unoEnabledPython','openOfficePort',
|
||||||
'enableNotifications', 'unoEnabledPython','openOfficePort',
|
'numberOfResultsPerPage', 'mailHost', 'mailEnabled',
|
||||||
'numberOfResultsPerPage', 'listBoxesMaximumWidth',
|
'mailFrom', 'appyVersion', 'users', 'groups',
|
||||||
'appyVersion')
|
'translations', 'pages')
|
||||||
|
|
||||||
class Tool(ModelClass):
|
class Tool(ModelClass):
|
||||||
# In a ModelClass we need to declare attributes in the following list.
|
# In a ModelClass we need to declare attributes in the following list.
|
||||||
|
@ -222,11 +222,12 @@ class Tool(ModelClass):
|
||||||
# Tool attributes
|
# Tool attributes
|
||||||
title = gen.String(show=False, page=gen.Page('main', show=False))
|
title = gen.String(show=False, page=gen.Page('main', show=False))
|
||||||
def validPythonWithUno(self, value): pass # Real method in the wrapper
|
def validPythonWithUno(self, value): pass # Real method in the wrapper
|
||||||
unoEnabledPython = gen.String(group="connectionToOpenOffice",
|
unoEnabledPython = gen.String(validator=validPythonWithUno)
|
||||||
validator=validPythonWithUno)
|
openOfficePort = gen.Integer(default=2002)
|
||||||
openOfficePort = gen.Integer(default=2002, group="connectionToOpenOffice")
|
|
||||||
numberOfResultsPerPage = gen.Integer(default=30)
|
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')
|
appyVersion = gen.String(show=False, layouts='f')
|
||||||
# Ref(User) will maybe be transformed into Ref(CustomUserClass).
|
# Ref(User) will maybe be transformed into Ref(CustomUserClass).
|
||||||
users = gen.Ref(User, multiplicity=(0,None), add=True, link=False,
|
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,
|
pages = gen.Ref(Page, multiplicity=(0,None), add=True, link=False,
|
||||||
show='view', back=gen.Ref(attribute='toTool3', show=False),
|
show='view', back=gen.Ref(attribute='toTool3', show=False),
|
||||||
page=gen.Page('pages', show='view'))
|
page=gen.Page('pages', show='view'))
|
||||||
enableNotifications = gen.Boolean(default=True,
|
|
||||||
page=gen.Page('notifications', show=False))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _appy_clean(klass):
|
def _appy_clean(klass):
|
||||||
|
|
105
gen/notifier.py
105
gen/notifier.py
|
@ -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
|
|
||||||
# ------------------------------------------------------------------------------
|
|
|
@ -60,7 +60,8 @@ img {border: 0}
|
||||||
.portletSep { border-top: 1px solid #5F7983; margin-top: 2px;}
|
.portletSep { border-top: 1px solid #5F7983; margin-top: 2px;}
|
||||||
.portletPage { font-style: italic; }
|
.portletPage { font-style: italic; }
|
||||||
.portletGroup { font-variant: small-caps; font-weight: bold; font-style: normal;
|
.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;}
|
.phase { border-style: dashed; border-width: thin; padding: 4px 0.6em 5px 1em;}
|
||||||
.phaseSelected { background-color: #F4F5F6; }
|
.phaseSelected { background-color: #F4F5F6; }
|
||||||
.content { padding: 14px 14px 9px 15px;}
|
.content { padding: 14px 14px 9px 15px;}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
@ -64,17 +64,17 @@
|
||||||
<tal:expanded define="group searchOrGroup;
|
<tal:expanded define="group searchOrGroup;
|
||||||
expanded python: request.get(group['labelId'], 'collapsed') == 'expanded'">
|
expanded python: request.get(group['labelId'], 'collapsed') == 'expanded'">
|
||||||
<tal:comment replace="nothing">Group name</tal:comment>
|
<tal:comment replace="nothing">Group name</tal:comment>
|
||||||
<dt class="portletAppyItem portletGroup">
|
<dt class="portletGroup">
|
||||||
<img align="left" style="cursor:pointer"
|
<img align="left" style="cursor:pointer; margin-right: 3px"
|
||||||
tal:attributes="id python: '%s_img' % group['labelId'];
|
tal:attributes="id python: '%s_img' % group['labelId'];
|
||||||
src python:test(expanded, 'ui/collapse.gif', 'ui/expand.gif');
|
src python:test(expanded, 'ui/collapse.gif', 'ui/expand.gif');
|
||||||
onClick python:'toggleCookie(\'%s\')' % group['labelId']"/>
|
onClick python:'toggleCookie(\'%s\')' % group['labelId']"/>
|
||||||
<span tal:replace="group/label"/>
|
<span tal:replace="group/label"/>
|
||||||
</dt>
|
</dt>
|
||||||
<tal:comment replace="nothing">Group searches</tal:comment>
|
<tal:comment replace="nothing">Group searches</tal:comment>
|
||||||
<span tal:attributes="id group/labelId;
|
<span tal:attributes="id group/labelId;
|
||||||
style python:test(expanded, 'display:block', 'display:none')">
|
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']);
|
<a tal:attributes="href python: '%s?className=%s&search=%s' % (queryUrl, rootClass, search['name']);
|
||||||
title search/descr;
|
title search/descr;
|
||||||
class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
|
class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
|
||||||
|
|
|
@ -129,7 +129,6 @@
|
||||||
canWrite python: not appyType['isBack'] and contextObj.allows(appyType['writePermission']);
|
canWrite python: not appyType['isBack'] and contextObj.allows(appyType['writePermission']);
|
||||||
showPlusIcon python: contextObj.mayAddReference(fieldName, folder);
|
showPlusIcon python: contextObj.mayAddReference(fieldName, folder);
|
||||||
atMostOneRef python: (appyType['multiplicity'][1] == 1) and (len(objs)<=1);
|
atMostOneRef python: (appyType['multiplicity'][1] == 1) and (len(objs)<=1);
|
||||||
label python: contextObj.translate('label', field=appyType);
|
|
||||||
addConfirmMsg python: appyType['addConfirm'] and _('%s_addConfirm' % appyType['labelId']) or '';
|
addConfirmMsg python: appyType['addConfirm'] and _('%s_addConfirm' % appyType['labelId']) or '';
|
||||||
navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)">
|
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
|
<tal:comment replace="nothing">Display a simplified widget if maximum number of
|
||||||
referenced objects is 1.</tal:comment>
|
referenced objects is 1.</tal:comment>
|
||||||
<table><tr valign="top">
|
<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:comment replace="nothing">If there is no object...</tal:comment>
|
||||||
<tal:noObject condition="not:objs">
|
<tal:noObject condition="not:objs">
|
||||||
<td tal:content="python: _('no_ref')"></td>
|
<td tal:content="python: _('no_ref')"></td>
|
||||||
|
|
|
@ -46,9 +46,7 @@ class AbstractWrapper(object):
|
||||||
elif name == 'url': return self.o.absolute_url()
|
elif name == 'url': return self.o.absolute_url()
|
||||||
elif name == 'state': return self.o.State()
|
elif name == 'state': return self.o.State()
|
||||||
elif name == 'stateLabel':
|
elif name == 'stateLabel':
|
||||||
o = self.o
|
return self.o.translate(self.o.getWorkflowLabel())
|
||||||
appName = o.getProductConfig().PROJECTNAME
|
|
||||||
return o.translate(o.getWorkflowLabel(), domain=appName)
|
|
||||||
elif name == 'history':
|
elif name == 'history':
|
||||||
o = self.o
|
o = self.o
|
||||||
key = o.workflow_history.keys()[0]
|
key = o.workflow_history.keys()[0]
|
||||||
|
|
Loading…
Reference in a new issue