appy.bin: adapted job.py for Appy >0.8; appy.gen: improved mail notification mechanism.
This commit is contained in:
		
							parent
							
								
									9b8064b0cd
								
							
						
					
					
						commit
						459a714b76
					
				
					 11 changed files with 168 additions and 147 deletions
				
			
		|  | @ -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
									
								
							
							
						
						
									
										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': | ||||
|             res = text.replace('\r\n', '').replace('\n', '') | ||||
|             res = res.replace("'", "\\'") | ||||
|         elif format == 'text': | ||||
|             res = text.replace('<br/>', '\n') | ||||
|         else: | ||||
|             res = text | ||||
|         return res | ||||
|  |  | |||
							
								
								
									
										19
									
								
								gen/model.py
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								gen/model.py
									
										
									
									
									
								
							|  | @ -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): | ||||
|  |  | |||
							
								
								
									
										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;} | ||||
| .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 | 
|  | @ -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']"/>  | ||||
|                                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> | ||||
|  |  | |||
|  | @ -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)<=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> | ||||
|  |  | |||
|  | @ -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] | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Gaetan Delannay
						Gaetan Delannay