| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (c) 2012 by PloneGov |
|---|
| 4 | # |
|---|
| 5 | # GNU General Public License (GPL) |
|---|
| 6 | # |
|---|
| 7 | # This program is free software; you can redistribute it and/or |
|---|
| 8 | # modify it under the terms of the GNU General Public License |
|---|
| 9 | # as published by the Free Software Foundation; either version 2 |
|---|
| 10 | # of the License, or (at your option) any later version. |
|---|
| 11 | # |
|---|
| 12 | # This program is distributed in the hope that it will be useful, |
|---|
| 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 15 | # GNU General Public License for more details. |
|---|
| 16 | # |
|---|
| 17 | # You should have received a copy of the GNU General Public License |
|---|
| 18 | # along with this program; if not, write to the Free Software |
|---|
| 19 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
|---|
| 20 | # 02110-1301, USA. |
|---|
| 21 | # |
|---|
| 22 | |
|---|
| 23 | import os, re, urlparse, os.path, socket, email, cgi |
|---|
| 24 | from appy.shared.diff import HtmlDiff |
|---|
| 25 | from email.MIMEMultipart import MIMEMultipart |
|---|
| 26 | from email.MIMEBase import MIMEBase |
|---|
| 27 | from email.MIMEText import MIMEText |
|---|
| 28 | from email import Encoders |
|---|
| 29 | from appy.shared.xml_parser import XmlMarshaller |
|---|
| 30 | from DateTime import DateTime |
|---|
| 31 | from AccessControl import getSecurityManager, Unauthorized |
|---|
| 32 | from Products.CMFCore.utils import getToolByName |
|---|
| 33 | from Products.MailHost.MailHost import MailHostError |
|---|
| 34 | from Products.Archetypes.Marshall import Marshaller |
|---|
| 35 | from Products.CMFCore.permissions import View, AccessContentsInformation, \ |
|---|
| 36 | ModifyPortalContent, ReviewPortalContent, DeleteObjects |
|---|
| 37 | import Products.PloneMeeting |
|---|
| 38 | from Products.PloneMeeting.config import * |
|---|
| 39 | from Products.PloneMeeting import PloneMeetingError |
|---|
| 40 | from Products.PloneMeeting.interfaces import * |
|---|
| 41 | import logging |
|---|
| 42 | logger = logging.getLogger('PloneMeeting') |
|---|
| 43 | |
|---|
| 44 | # PloneMeetingError-related constants ------------------------------------------ |
|---|
| 45 | WRONG_INTERFACE_NAME = 'Wrong interface name "%s". You must specify the full ' \ |
|---|
| 46 | 'interface package name.' |
|---|
| 47 | WRONG_INTERFACE_PACKAGE = 'Could not find package "%s".' |
|---|
| 48 | WRONG_INTERFACE = 'Interface "%s" not found in package "%s".' |
|---|
| 49 | |
|---|
| 50 | # ------------------------------------------------------------------------------ |
|---|
| 51 | monthsIds = {1: 'month_jan', 2: 'month_feb', 3: 'month_mar', 4: 'month_apr', |
|---|
| 52 | 5: 'month_may', 6: 'month_jun', 7: 'month_jul', 8: 'month_aug', |
|---|
| 53 | 9: 'month_sep', 10: 'month_oct', 11: 'month_nov', 12: 'month_dec'} |
|---|
| 54 | |
|---|
| 55 | weekdaysIds = {0: 'weekday_sun', 1: 'weekday_mon', 2: 'weekday_tue', |
|---|
| 56 | 3: 'weekday_wed', 4: 'weekday_thu', 5: 'weekday_fri', |
|---|
| 57 | 6:'weekday_sat'} |
|---|
| 58 | |
|---|
| 59 | adaptables = { |
|---|
| 60 | 'MeetingItem' : {'method':'getItem', 'interface':IMeetingItemCustom}, |
|---|
| 61 | 'Meeting' : {'method':'getMeeting', 'interface':IMeetingCustom}, |
|---|
| 62 | # No (condition or action) workflow-related adapters are defined for the |
|---|
| 63 | # following content types; only a Custom adapter. |
|---|
| 64 | 'MeetingCategory': {'method':None, 'interface':IMeetingCategoryCustom}, |
|---|
| 65 | 'ExternalApplication': |
|---|
| 66 | {'method':None, 'interface':IExternalApplicationCustom}, |
|---|
| 67 | 'MeetingConfig': {'method':None, 'interface':IMeetingConfigCustom}, |
|---|
| 68 | 'MeetingFile': {'method':None, 'interface':IMeetingFileCustom}, |
|---|
| 69 | 'MeetingFileType': {'method':None, 'interface':IMeetingFileTypeCustom}, |
|---|
| 70 | 'MeetingGroup': {'method':None, 'interface':IMeetingGroupCustom}, |
|---|
| 71 | 'PodTemplate': {'method':None, 'interface':IPodTemplateCustom}, |
|---|
| 72 | 'ToolPloneMeeting': {'method':None, 'interface':IToolPloneMeetingCustom}, |
|---|
| 73 | 'MeetingUser': {'method':None, 'interface':IMeetingUserCustom}, |
|---|
| 74 | } |
|---|
| 75 | |
|---|
| 76 | def getInterface(interfaceName): |
|---|
| 77 | '''Gets the interface named p_interfaceName.''' |
|---|
| 78 | elems = interfaceName.split('.') |
|---|
| 79 | if len(elems) < 2: |
|---|
| 80 | raise PloneMeetingError(WRONG_INTERFACE_NAME % interfaceName) |
|---|
| 81 | interfaceName = elems[len(elems)-1] |
|---|
| 82 | packageName = '' |
|---|
| 83 | for elem in elems[:-1]: |
|---|
| 84 | if not packageName: |
|---|
| 85 | point = '' |
|---|
| 86 | else: |
|---|
| 87 | point = '.' |
|---|
| 88 | packageName += '%s%s' % (point, elem) |
|---|
| 89 | try: |
|---|
| 90 | exec 'import %s' % packageName |
|---|
| 91 | exec 'res = %s.%s' % (packageName, interfaceName) |
|---|
| 92 | return res |
|---|
| 93 | except ImportError: |
|---|
| 94 | raise PloneMeetingError(WRONG_INTERFACE_PACKAGE % packageName) |
|---|
| 95 | except AttributeError: |
|---|
| 96 | raise PloneMeetingError(WRONG_INTERFACE % (interfaceName, packageName)) |
|---|
| 97 | |
|---|
| 98 | def getWorkflowAdapter(obj, conditions): |
|---|
| 99 | '''Gets the adapter, for a PloneMeeting object that proposes methods that |
|---|
| 100 | may be used as workflow conditions (if p_conditions is True) or actions |
|---|
| 101 | (if p_condition is False).''' |
|---|
| 102 | tool = getToolByName(obj, TOOL_ID) |
|---|
| 103 | meetingConfig = tool.getMeetingConfig(obj) |
|---|
| 104 | interfaceMethod = adaptables[obj.meta_type]['method'] |
|---|
| 105 | if conditions: |
|---|
| 106 | interfaceMethod += 'Conditions' |
|---|
| 107 | else: |
|---|
| 108 | interfaceMethod += 'Actions' |
|---|
| 109 | exec 'interfaceLongName = meetingConfig.%sInterface()' % interfaceMethod |
|---|
| 110 | return getInterface(interfaceLongName)(obj) |
|---|
| 111 | |
|---|
| 112 | def getCustomAdapter(obj): |
|---|
| 113 | '''Tries to get the custom adapter for a PloneMeeting object. If no adapter |
|---|
| 114 | is defined, returns the object.''' |
|---|
| 115 | res = obj |
|---|
| 116 | theInterface = adaptables[obj.meta_type]['interface'] |
|---|
| 117 | try: |
|---|
| 118 | res = theInterface(obj) |
|---|
| 119 | except TypeError: |
|---|
| 120 | pass |
|---|
| 121 | return res |
|---|
| 122 | |
|---|
| 123 | methodTypes = ('FSPythonScript', 'FSControllerPythonScript', 'instancemethod') |
|---|
| 124 | def getCurrentMeetingObject(context): |
|---|
| 125 | '''What is the object currently published by Plone ?''' |
|---|
| 126 | obj = context.REQUEST.get('PUBLISHED') |
|---|
| 127 | className = obj.__class__.__name__ |
|---|
| 128 | if not (className in ('Meeting', 'MeetingItem')): |
|---|
| 129 | if className in methodTypes: |
|---|
| 130 | # We are changing the state of an element. We must then check the |
|---|
| 131 | # referer |
|---|
| 132 | refererUrl = context.REQUEST.get('HTTP_REFERER') |
|---|
| 133 | referer = urlparse.urlparse(refererUrl)[2] |
|---|
| 134 | if referer.endswith('_view') or referer.endswith('_edit'): |
|---|
| 135 | referer = os.path.dirname(referer) |
|---|
| 136 | # We add the portal path if necessary |
|---|
| 137 | # (in case Apache rewrites the uri for example) |
|---|
| 138 | portal_path = context.portal_url.getPortalPath() |
|---|
| 139 | if not referer.startswith(portal_path): |
|---|
| 140 | # The rewrite rule has modified the URL. First, remove any |
|---|
| 141 | # added URL prefix. |
|---|
| 142 | if referer.find('/Members/') != -1: |
|---|
| 143 | referer = referer[referer.index('/Members/'):] |
|---|
| 144 | # Then, add the real portal as URL prefix. |
|---|
| 145 | referer = portal_path + referer |
|---|
| 146 | res = context.portal_catalog(path=referer) |
|---|
| 147 | if res: |
|---|
| 148 | obj = res[0].getObject() |
|---|
| 149 | else: |
|---|
| 150 | # Check the parent (if it has sense) |
|---|
| 151 | if hasattr(obj, 'getParentNode'): |
|---|
| 152 | obj = obj.getParentNode() |
|---|
| 153 | if not (obj.__class__.__name__ in ('Meeting', 'MeetingItem')): |
|---|
| 154 | obj = None |
|---|
| 155 | else: |
|---|
| 156 | # It can be a method with attribute im_class |
|---|
| 157 | obj = None |
|---|
| 158 | return obj |
|---|
| 159 | |
|---|
| 160 | def getOsTempFolder(): |
|---|
| 161 | tmp = '/tmp' |
|---|
| 162 | if os.path.exists(tmp) and os.path.isdir(tmp): |
|---|
| 163 | res = tmp |
|---|
| 164 | elif os.environ.has_key('TMP'): |
|---|
| 165 | res = os.environ['TMP'] |
|---|
| 166 | elif os.environ.has_key('TEMP'): |
|---|
| 167 | res = os.environ['TEMP'] |
|---|
| 168 | else: |
|---|
| 169 | raise "Sorry, I can't find a temp folder on your machine." |
|---|
| 170 | return res |
|---|
| 171 | |
|---|
| 172 | # How to know if a Kupu field is "empty" --------------------------------------- |
|---|
| 173 | KUPU_EMPTY_VALUES = ('<p></p>', '<p> </p>', '<p><br/></p>', '<p><br /></p>', |
|---|
| 174 | '<p><br /><br /></p>', '<p>Â </p>', '<p><b> </b></p>', |
|---|
| 175 | '<p><b></b></p>', '<p/>', '<p />', '<p> </p>') |
|---|
| 176 | # The 2 '<p> </p>' are different (2 different blank chars) |
|---|
| 177 | KEEP_WITH_NEXT_STYLES = {'para': 'pmParaKeepWithNext', |
|---|
| 178 | 'item': 'podItemKeepWithNext'} |
|---|
| 179 | |
|---|
| 180 | def kupuFieldIsEmpty(kupuContent): |
|---|
| 181 | if not kupuContent or (kupuContent.strip() in KUPU_EMPTY_VALUES): |
|---|
| 182 | return True |
|---|
| 183 | |
|---|
| 184 | def fieldIsEmpty(name, obj, useParamValue=False, value=None): |
|---|
| 185 | '''If field named p_name on p_obj empty ? The method checks emptyness of |
|---|
| 186 | given p_value if p_useParamValue is True instead.''' |
|---|
| 187 | field = obj.getField(name) |
|---|
| 188 | if useParamValue: |
|---|
| 189 | value = value |
|---|
| 190 | else: |
|---|
| 191 | value = field.get(obj) |
|---|
| 192 | widgetName = field.widget.getName() |
|---|
| 193 | if widgetName == 'RichWidget': |
|---|
| 194 | return kupuFieldIsEmpty(value) |
|---|
| 195 | elif widgetName == 'BooleanWidget': |
|---|
| 196 | return value == None |
|---|
| 197 | else: |
|---|
| 198 | return not value |
|---|
| 199 | |
|---|
| 200 | def kupuEquals(fieldContent, valueFromDomParsing): |
|---|
| 201 | '''Is the content of Kupu field p_fieldContent equal to |
|---|
| 202 | p_valueFromDomString? The latter is not a str but unicode; |
|---|
| 203 | differences like <br/> and <br /> and some whitespace are |
|---|
| 204 | ignored.''' |
|---|
| 205 | v1 = fieldContent.strip().replace('<br />', '<br/>') |
|---|
| 206 | v2 = valueFromDomParsing.encode('utf-8') |
|---|
| 207 | if (v1 == '<p></p>') and (v2 == '<p/>'): |
|---|
| 208 | res = True |
|---|
| 209 | else: |
|---|
| 210 | res = (v1 == v2) |
|---|
| 211 | return res |
|---|
| 212 | |
|---|
| 213 | def checkPermission(permission, obj): |
|---|
| 214 | '''We must call getSecurityManager() each time we need to check a |
|---|
| 215 | permission.''' |
|---|
| 216 | sm = getSecurityManager() |
|---|
| 217 | return sm.checkPermission(permission, obj) |
|---|
| 218 | |
|---|
| 219 | # Mail sending machinery ------------------------------------------------------- |
|---|
| 220 | class EmailError(Exception): pass |
|---|
| 221 | SENDMAIL_ERROR = 'Error while sending mail: %s.' |
|---|
| 222 | ENCODING_ERROR = 'Encoding error while sending mail: %s.' |
|---|
| 223 | MAILHOST_ERROR = 'Error with the MailServer while sending mail: %s.' |
|---|
| 224 | |
|---|
| 225 | def _getEmailAddress(name, email, encoding='utf-8'): |
|---|
| 226 | '''Creates a full email address from a p_name and p_email.''' |
|---|
| 227 | res = email |
|---|
| 228 | if name: res = name.decode(encoding) + ' <%s>' % email |
|---|
| 229 | return res.encode(encoding) |
|---|
| 230 | |
|---|
| 231 | def _sendMail(obj, body, recipients, fromAddress, subject, format, |
|---|
| 232 | attachments=None): |
|---|
| 233 | '''Sends a mail. p_mto can be a single email or a list of emails.''' |
|---|
| 234 | # Hide the whole list of recipients if we must send the mail to many. |
|---|
| 235 | bcc = None |
|---|
| 236 | if not isinstance(recipients, basestring): |
|---|
| 237 | bcc = recipients |
|---|
| 238 | recipients = fromAddress |
|---|
| 239 | # Construct the data structures for the attachments if relevant |
|---|
| 240 | if attachments: |
|---|
| 241 | msg = MIMEMultipart() |
|---|
| 242 | msg.attach( MIMEText(body) ) |
|---|
| 243 | body = msg |
|---|
| 244 | for fileName, fileContent in attachments: |
|---|
| 245 | part = MIMEBase('application', 'octet-stream') |
|---|
| 246 | if hasattr(fileContent, 'data'): |
|---|
| 247 | # It is a File instance coming from the DB (frozen doc) |
|---|
| 248 | payLoad = '' |
|---|
| 249 | data = fileContent.data |
|---|
| 250 | while data is not None: |
|---|
| 251 | payLoad += data.data |
|---|
| 252 | data = data.next |
|---|
| 253 | else: |
|---|
| 254 | payLoad = fileContent |
|---|
| 255 | part.set_payload(payLoad) |
|---|
| 256 | Encoders.encode_base64(part) |
|---|
| 257 | part.add_header('Content-Disposition', |
|---|
| 258 | 'attachment; filename="%s"' % fileName) |
|---|
| 259 | body.attach(part) |
|---|
| 260 | try: |
|---|
| 261 | obj.MailHost.secureSend(body, recipients, fromAddress, subject,mbcc=bcc, |
|---|
| 262 | subtype=format, charset='utf-8') |
|---|
| 263 | except socket.error, sg: |
|---|
| 264 | raise EmailError(SENDMAIL_ERROR % str(sg)) |
|---|
| 265 | except UnicodeDecodeError, ue: |
|---|
| 266 | raise EmailError(ENCODING_ERROR % str(ue)) |
|---|
| 267 | except MailHostError, mhe: |
|---|
| 268 | raise EmailError(MAILHOST_ERROR % str(mhe)) |
|---|
| 269 | except Exception, e: |
|---|
| 270 | raise EmailError(SENDMAIL_ERROR % str(e)) |
|---|
| 271 | |
|---|
| 272 | def sendMail(recipients, obj, event, attachments=None, mapping={}): |
|---|
| 273 | '''Sends a mail related to p_event that occurred on p_obj to |
|---|
| 274 | p_recipients. If p_recipients is None, the mail is sent to |
|---|
| 275 | the system administrator.''' |
|---|
| 276 | # Do not sent any mail if mail mode is "deactivated". |
|---|
| 277 | tool = obj.portal_plonemeeting |
|---|
| 278 | cfg = tool.getMeetingConfig(obj) or tool.getActiveConfigs()[0] |
|---|
| 279 | mailMode = cfg.getMailMode() |
|---|
| 280 | if mailMode == 'deactivated': return |
|---|
| 281 | enc = obj.portal_properties.site_properties.getProperty('default_charset') |
|---|
| 282 | # Compute user name |
|---|
| 283 | pms = obj.portal_membership |
|---|
| 284 | userName = pms.getAuthenticatedMember().id |
|---|
| 285 | userInfo = pms.getMemberById(userName) |
|---|
| 286 | if userInfo.getProperty('fullname'): |
|---|
| 287 | userName = userInfo.getProperty('fullname').decode(enc) |
|---|
| 288 | # Compute list of MeetingGroups for this user |
|---|
| 289 | userGroups = ', '.join([g.Title() for g in tool.getGroups()]) |
|---|
| 290 | # Create the message parts |
|---|
| 291 | d = 'PloneMeeting' |
|---|
| 292 | portal = obj.portal_url.getPortalObject() |
|---|
| 293 | portalUrl = tool.getPublicUrl().strip() |
|---|
| 294 | if not portalUrl: portalUrl = portal.absolute_url() |
|---|
| 295 | if mapping: translationMapping = mapping |
|---|
| 296 | else: translationMapping = {} |
|---|
| 297 | translationMapping.update({ |
|---|
| 298 | 'portalUrl': portalUrl, 'portalTitle': portal.Title().decode(enc), |
|---|
| 299 | 'objectTitle': obj.Title().decode(enc), 'objectUrl': obj.absolute_url(), |
|---|
| 300 | 'meetingTitle': '', 'itemTitle': '', 'user': userName, |
|---|
| 301 | 'objectDavUrl': obj.absolute_url_path(), 'groups': userGroups, |
|---|
| 302 | }) |
|---|
| 303 | if obj.meta_type == 'Meeting': |
|---|
| 304 | translationMapping['meetingTitle'] = obj.Title().decode(enc) |
|---|
| 305 | elif obj.meta_type == 'MeetingItem': |
|---|
| 306 | translationMapping['itemTitle'] = obj.Title().decode(enc) |
|---|
| 307 | translationMapping['lastAnnexTitle'] = '' |
|---|
| 308 | translationMapping['lastAnnexTypeTitle'] = '' |
|---|
| 309 | lastAnnex = obj.getLastInsertedAnnex() |
|---|
| 310 | if lastAnnex: |
|---|
| 311 | translationMapping['lastAnnexTitle'] = lastAnnex.Title().decode(enc) |
|---|
| 312 | translationMapping['lastAnnexTypeTitle'] = \ |
|---|
| 313 | lastAnnex.getMeetingFileType().Title().decode(enc) |
|---|
| 314 | meeting = obj.getMeeting(brain=True) |
|---|
| 315 | if meeting: |
|---|
| 316 | translationMapping['meetingTitle'] = meeting.Title().decode(enc) |
|---|
| 317 | translationMapping['itemNumber'] = obj.getItemNumber( |
|---|
| 318 | relativeTo='meeting') |
|---|
| 319 | # Update the translationMapping with a sub-product-specific |
|---|
| 320 | # translationMapping, that may also define custom mail subject and body. |
|---|
| 321 | customRes = obj.adapted().getSpecificMailContext(event, translationMapping) |
|---|
| 322 | if customRes: |
|---|
| 323 | subject = customRes[0].encode(enc) |
|---|
| 324 | body = customRes[1].encode(enc) |
|---|
| 325 | else: |
|---|
| 326 | _ = obj.translate |
|---|
| 327 | subjectLabel = '%s_mail_subject' % event |
|---|
| 328 | subject = _(subjectLabel, domain=d, mapping=translationMapping) |
|---|
| 329 | subject = subject.encode(enc) |
|---|
| 330 | bodyLabel = '%s_mail_body' % event |
|---|
| 331 | body = _(bodyLabel, domain=d, mapping=translationMapping).encode(enc) |
|---|
| 332 | adminFromAddress = _getEmailAddress( |
|---|
| 333 | portal.getProperty('email_from_name'), |
|---|
| 334 | portal.getProperty('email_from_address'), enc) |
|---|
| 335 | fromAddress = adminFromAddress |
|---|
| 336 | if tool.getFunctionalAdminEmail(): |
|---|
| 337 | fromAddress = _getEmailAddress(tool.getFunctionalAdminName(), |
|---|
| 338 | tool.getFunctionalAdminEmail(), enc) |
|---|
| 339 | if not recipients: recipients = [adminFromAddress] |
|---|
| 340 | if mailMode == 'test': |
|---|
| 341 | # Instead of sending mail, in test mode, we log data about the mailing. |
|---|
| 342 | logger.info('Test mode / we should send mail to %s' % str(recipients)) |
|---|
| 343 | logger.info('Subject is [%s]' % subject) |
|---|
| 344 | logger.info('Body is [%s]' % body) |
|---|
| 345 | return |
|---|
| 346 | # Determine mail format (plain text or HTML) |
|---|
| 347 | mailFormat = 'plain' |
|---|
| 348 | if cfg.getUserParam('mailFormat') == 'html': mailFormat = 'html' |
|---|
| 349 | # Send the mail(s) |
|---|
| 350 | try: |
|---|
| 351 | if not attachments: |
|---|
| 352 | # Send a personalized email for every user. |
|---|
| 353 | for recipient in recipients: |
|---|
| 354 | _sendMail(obj, body, recipient, fromAddress, subject,mailFormat) |
|---|
| 355 | else: |
|---|
| 356 | # Send a single mail with everybody in bcc, for performance reasons |
|---|
| 357 | # (avoid to duplicate the attached file(s)). |
|---|
| 358 | _sendMail(obj, body, recipients, fromAddress, subject, mailFormat, |
|---|
| 359 | attachments) |
|---|
| 360 | except EmailError, ee: |
|---|
| 361 | logger.warn(str(ee)) |
|---|
| 362 | |
|---|
| 363 | def sendMailIfRelevant(obj, event, permissionOrRole, isRole=False, |
|---|
| 364 | customEvent=False, mapping={}): |
|---|
| 365 | '''An p_event just occurred on meeting or item p_obj. If the corresponding |
|---|
| 366 | meeting config specifies that a mail needs to be sent, this function |
|---|
| 367 | will send a mail. The mail subject and body are defined from i18n labels |
|---|
| 368 | that derive from the event name. if p_isRole is True, p_permissionOrRole |
|---|
| 369 | is a role, and the mail will be sent to every user having this role. If |
|---|
| 370 | p_isRole is False, p_permissionOrRole is a permission and the mail will |
|---|
| 371 | be sent to everyone having this permission. Some mapping can be received |
|---|
| 372 | and used afterward in mail subject and mail body translations. |
|---|
| 373 | |
|---|
| 374 | If mail sending is activated (or in test mode) and enabled for this |
|---|
| 375 | event, this method returns True. |
|---|
| 376 | |
|---|
| 377 | A plug-in may use this method for sending custom events that are not |
|---|
| 378 | defined in the MeetingConfig. In this case, you must specify |
|---|
| 379 | p_customEvent = True.''' |
|---|
| 380 | res = False |
|---|
| 381 | tool = obj.portal_plonemeeting |
|---|
| 382 | # If p_isRole is True and the current user has this role, I will not send |
|---|
| 383 | # mail: a MeetingManager is already notified! |
|---|
| 384 | currentUser = obj.portal_membership.getAuthenticatedMember() |
|---|
| 385 | if isRole and currentUser.has_role(permissionOrRole, obj): |
|---|
| 386 | # In this case we don't know if mail is enabled or disabled; we just |
|---|
| 387 | # decide to avoid sending the mail. |
|---|
| 388 | return |
|---|
| 389 | cfg = tool.getMeetingConfig(obj) |
|---|
| 390 | # Do not send the mail if mail mode is "deactivated". |
|---|
| 391 | if cfg.getMailMode() == 'deactivated': return |
|---|
| 392 | # Do not send mail if the (not custom) event is unknown. |
|---|
| 393 | if not customEvent and event not in cfg.getMailItemEvents() and \ |
|---|
| 394 | event not in cfg.getMailMeetingEvents(): |
|---|
| 395 | return |
|---|
| 396 | # Ok, send a mail. Who are the recipients ? |
|---|
| 397 | enc = obj.portal_properties.site_properties.getProperty('default_charset') |
|---|
| 398 | recipients = [] |
|---|
| 399 | adap = obj.adapted() |
|---|
| 400 | if isRole and (permissionOrRole == 'Owner'): |
|---|
| 401 | userIds = [obj.Creator()] |
|---|
| 402 | else: |
|---|
| 403 | userIds = obj.portal_membership.listMemberIds() |
|---|
| 404 | # When using the LDAP plugin, this method does not return all |
|---|
| 405 | # users, nor the cached users! |
|---|
| 406 | for userId in userIds: |
|---|
| 407 | user = obj.acl_users.getUser(userId) |
|---|
| 408 | if not user: continue |
|---|
| 409 | # May happen if someone deletes the user directly in the ZMI. |
|---|
| 410 | userInfo = obj.portal_membership.getMemberById(userId) |
|---|
| 411 | if not userInfo.getProperty('email'): continue |
|---|
| 412 | # Does the user have the corresponding permission on p_obj ? |
|---|
| 413 | if isRole: checkMethod = user.has_role |
|---|
| 414 | else: checkMethod = user.has_permission |
|---|
| 415 | if not checkMethod(permissionOrRole, obj): continue |
|---|
| 416 | recipient = tool.getMailRecipient(userInfo) |
|---|
| 417 | # Must we avoid sending mail to this recipient for some custom reason? |
|---|
| 418 | if not adap.includeMailRecipient(event, userId): continue |
|---|
| 419 | # Has the user unsubscribed to this event in his preferences ? |
|---|
| 420 | itemEvents = cfg.getUserParam('mailItemEvents', userId=userId) |
|---|
| 421 | meetingEvents = cfg.getUserParam('mailMeetingEvents', userId=userId) |
|---|
| 422 | if (event not in itemEvents) and (event not in meetingEvents): continue |
|---|
| 423 | # After all, we will add this guy to the list of recipients. |
|---|
| 424 | recipients.append(recipient) |
|---|
| 425 | sendMail(recipients, obj, event, mapping=mapping) |
|---|
| 426 | return True |
|---|
| 427 | |
|---|
| 428 | def sendAdviceToGiveMailIfRelevant(event): |
|---|
| 429 | '''A transition was fired on an item (in p_event.object). Check here if, |
|---|
| 430 | in the new item state, advices need to be given, that had not to be given |
|---|
| 431 | in the previous item state.''' |
|---|
| 432 | tool = event.object.portal_plonemeeting |
|---|
| 433 | cfg = tool.getMeetingConfig(event.object) |
|---|
| 434 | if 'adviceToGive' not in cfg.getMailItemEvents(): return |
|---|
| 435 | for groupId, adviceInfo in event.object.advices.iteritems(): |
|---|
| 436 | adviceStates = getattr(tool, groupId).getItemAdviceStates(cfg) |
|---|
| 437 | # Ignore advices that must not be given in the current item state |
|---|
| 438 | if event.new_state.id not in adviceStates: continue |
|---|
| 439 | # Ignore advices for which already needed to be given in the previous |
|---|
| 440 | # item state |
|---|
| 441 | if event.old_state.id in adviceStates: continue |
|---|
| 442 | # Send a mail to every person from group _advisers. |
|---|
| 443 | ploneGroup = event.object.acl_users.getGroup('%s_advisers' % groupId) |
|---|
| 444 | for memberId in ploneGroup.getMemberIds(): |
|---|
| 445 | if 'adviceToGive' not in cfg.getUserParam('mailItemEvents', |
|---|
| 446 | userId=memberId): |
|---|
| 447 | continue |
|---|
| 448 | # Send a mail to this guy |
|---|
| 449 | recipient = tool.getMailRecipient(memberId) |
|---|
| 450 | if recipient: |
|---|
| 451 | labelType = adviceInfo['optional'] and \ |
|---|
| 452 | 'advice_optional' or 'advice_mandatory' |
|---|
| 453 | type = tool.translate(labelType, domain='PloneMeeting').lower() |
|---|
| 454 | sendMail([recipient], event.object, 'adviceToGive', |
|---|
| 455 | mapping = {'type': type}) |
|---|
| 456 | |
|---|
| 457 | # ------------------------------------------------------------------------------ |
|---|
| 458 | def marshallBrain(res, value): |
|---|
| 459 | '''Custom function for producing the XML version of a brain.''' |
|---|
| 460 | w = res.write |
|---|
| 461 | # Dump a brain. |
|---|
| 462 | w('<Title>'); w(cgi.escape(value.Title)); w('</Title>') |
|---|
| 463 | w('<url>'); w(value.getURL()); w('</url>') |
|---|
| 464 | w('<uid>'); w(value.UID); w('</uid>') |
|---|
| 465 | w('<path>'); w(value.getPath()); w('</path>') |
|---|
| 466 | w('<Creator>'); w(value.Creator); w('</Creator>') |
|---|
| 467 | w('<review_state>'); w(value.review_state); w('</review_state>') |
|---|
| 468 | w('<created type="DateTime">'); w(value.created); w('</created>') |
|---|
| 469 | w('<modified type="DateTime">'); w(value.modified); w('</modified>') |
|---|
| 470 | if value.meta_type == 'Meeting': |
|---|
| 471 | # We must also include the meeting date |
|---|
| 472 | dateIndex = value.portal_catalog.Indexes['getDate'] |
|---|
| 473 | meetingDate = dateIndex.getEntryForObject(value.getRID()) |
|---|
| 474 | w('<Date type="DateTime">'); w(meetingDate); w('</Date>') |
|---|
| 475 | elif value.meta_type == 'MeetingItem': |
|---|
| 476 | w('<Title2>'); w(value.getTitle2); w('</Title2>') |
|---|
| 477 | |
|---|
| 478 | marshallParams = {'conversionFunctions': {'mybrains': marshallBrain}, |
|---|
| 479 | 'objectType': 'archetype'} |
|---|
| 480 | |
|---|
| 481 | class HubSessionsMarshaller(Marshaller, XmlMarshaller): |
|---|
| 482 | '''Abstract marshaller used as base class for marshalling PloneMeeting |
|---|
| 483 | objects (meetings, items, configs,...).''' |
|---|
| 484 | marshallContentType = 'text/xml; charset="utf-8"' |
|---|
| 485 | frozableTypes = ('Meeting', 'MeetingItem') |
|---|
| 486 | workflowableTypes = ('Meeting', 'MeetingItem') |
|---|
| 487 | |
|---|
| 488 | def demarshall(self, instance, data, **kwargs): |
|---|
| 489 | raise 'Unmarshalling is not implemented yet!' |
|---|
| 490 | |
|---|
| 491 | def marshall(self, instance, **kwargs): |
|---|
| 492 | '''Produces a XML version of p_instance.''' |
|---|
| 493 | action = instance.REQUEST.get('do', None) |
|---|
| 494 | if action: |
|---|
| 495 | # In this case, we are not requested to produce the XML version of |
|---|
| 496 | # p_instance; we need to call a method named "action" and return the |
|---|
| 497 | # XML version of the method result. |
|---|
| 498 | try: |
|---|
| 499 | instance.restrictedTraverse(action) |
|---|
| 500 | methodRes = getattr(instance, action)() |
|---|
| 501 | res = XmlMarshaller.marshall(self, methodRes, **marshallParams) |
|---|
| 502 | except Unauthorized, u: |
|---|
| 503 | res = XmlMarshaller.marshall(self, str(u)) |
|---|
| 504 | except AttributeError, ae: |
|---|
| 505 | res = XmlMarshaller.marshall(self, 'Attribute error: '+str(ae)) |
|---|
| 506 | else: |
|---|
| 507 | res = XmlMarshaller.marshall(self, instance, **marshallParams) |
|---|
| 508 | return (self.marshallContentType, len(res), res) |
|---|
| 509 | |
|---|
| 510 | def marshallSpecificElements(self, instance, res): |
|---|
| 511 | '''Marshalls URLs of documents that were generated in the DB from |
|---|
| 512 | p_instance and a given POD template.''' |
|---|
| 513 | if instance.meta_type in self.frozableTypes: |
|---|
| 514 | mConfig = instance.portal_plonemeeting.getMeetingConfig(instance) |
|---|
| 515 | podTemplatesFolder = getattr(mConfig, TOOL_FOLDER_POD_TEMPLATES) |
|---|
| 516 | res.write('<frozenDocuments type="list">') |
|---|
| 517 | for podTemplate in podTemplatesFolder.objectValues(): |
|---|
| 518 | objectFolder = instance.getParentNode() |
|---|
| 519 | docId = podTemplate.getDocumentId(instance) |
|---|
| 520 | if hasattr(objectFolder.aq_base, docId) and \ |
|---|
| 521 | podTemplate.isApplicable(instance): |
|---|
| 522 | docObject = getattr(objectFolder, docId) |
|---|
| 523 | res.write('<doc type="object">') |
|---|
| 524 | self.dumpField(res, 'id', docId) |
|---|
| 525 | self.dumpField(res, 'title', docObject.Title()) |
|---|
| 526 | self.dumpField(res, 'templateId', podTemplate.id) |
|---|
| 527 | self.dumpField( |
|---|
| 528 | res, 'templateFormat', podTemplate.getPodFormat()) |
|---|
| 529 | self.dumpField(res, 'data', docObject, fieldType='file') |
|---|
| 530 | res.write('</doc>') |
|---|
| 531 | res.write('</frozenDocuments>') |
|---|
| 532 | wft = instance.portal_workflow |
|---|
| 533 | workflows = wft.getWorkflowsFor(instance) |
|---|
| 534 | if instance.meta_type in self.workflowableTypes: |
|---|
| 535 | # Dump workflow history |
|---|
| 536 | res.write('<workflowHistory type="list">') |
|---|
| 537 | if workflows: |
|---|
| 538 | history = instance.workflow_history[workflows[0].id] |
|---|
| 539 | for event in history: |
|---|
| 540 | res.write('<event type="object">') |
|---|
| 541 | for k, v in event.iteritems(): self.dumpField(res, k, v) |
|---|
| 542 | res.write('</event>') |
|---|
| 543 | res.write('</workflowHistory>') |
|---|
| 544 | if workflows and (workflows[0].id == 'plonemeeting_activity_workflow'): |
|---|
| 545 | # Add the object state |
|---|
| 546 | objectState = wft.getInfoFor(instance, 'review_state') |
|---|
| 547 | self.dumpField(res, 'active', objectState == 'active') |
|---|
| 548 | |
|---|
| 549 | # ------------------------------------------------------------------------------ |
|---|
| 550 | def addRecurringItemsIfRelevant(meeting, transition): |
|---|
| 551 | '''Sees in the meeting config linked to p_meeting if the triggering of |
|---|
| 552 | p_transition must lead to the insertion of some recurring items in |
|---|
| 553 | p_meeting.''' |
|---|
| 554 | recItems = [] |
|---|
| 555 | meetingConfig = meeting.portal_plonemeeting.getMeetingConfig(meeting) |
|---|
| 556 | for item in meetingConfig.getItems(usage=('as_recurring_item')): |
|---|
| 557 | if item.getMeetingTransitionInsertingMe() == transition: |
|---|
| 558 | recItems.append(item) |
|---|
| 559 | if recItems: |
|---|
| 560 | meeting.addRecurringItems(recItems) |
|---|
| 561 | |
|---|
| 562 | # ------------------------------------------------------------------------------ |
|---|
| 563 | defaultPermissions = (View, AccessContentsInformation, ModifyPortalContent, |
|---|
| 564 | DeleteObjects) |
|---|
| 565 | # I wanted to put permission "ReviewPortalContent" among defaultPermissions, |
|---|
| 566 | # but if I do this, it generates an error when calling "manage_permission" in |
|---|
| 567 | # method "clonePermissions" (see below). I've noticed that in several |
|---|
| 568 | # PloneMeeting standard workflows (meeting_workflow, meetingitem_workflow, |
|---|
| 569 | # meetingfile_workflow, etc), although this permission is declared as a |
|---|
| 570 | # managed permission, when you go in the ZMI to consult the actual |
|---|
| 571 | # permissions that are set on objects governed by those workflows, the |
|---|
| 572 | # permission "Review portal content" does not appear in the list at all. |
|---|
| 573 | |
|---|
| 574 | def clonePermissions(srcObj, destObj, permissions=defaultPermissions): |
|---|
| 575 | '''This method applies on p_destObj the same values for p_permissions |
|---|
| 576 | than those that apply for p_srcObj, according to workflow on |
|---|
| 577 | p_srcObj. p_srcObj may be an item or a meeting.''' |
|---|
| 578 | wfTool = srcObj.portal_workflow |
|---|
| 579 | srcWorkflows = wfTool.getWorkflowsFor(srcObj) |
|---|
| 580 | if not srcWorkflows: return |
|---|
| 581 | srcWorkflow = srcWorkflows[0] |
|---|
| 582 | for permission in permissions: |
|---|
| 583 | if permission in srcWorkflow.permissions: |
|---|
| 584 | # Get the roles this permission is given to for srcObj in its |
|---|
| 585 | # current state. |
|---|
| 586 | srcStateDef = getattr(srcWorkflow.states, srcObj.queryState()) |
|---|
| 587 | permissionInfo = srcStateDef.getPermissionInfo(permission) |
|---|
| 588 | destObj.manage_permission(permission, permissionInfo['roles'], |
|---|
| 589 | acquire=permissionInfo['acquired']) |
|---|
| 590 | # Reindex object because permissions are catalogued. |
|---|
| 591 | destObj.reindexObject(idxs=['allowedRolesAndUsers']) |
|---|
| 592 | |
|---|
| 593 | # ------------------------------------------------------------------------------ |
|---|
| 594 | coreFieldNames = ('id', 'title', 'description') |
|---|
| 595 | def getCustomSchemaFields(baseSchema, completedSchema, cols): |
|---|
| 596 | '''The Archetypes schema of any PloneMeeting content type can be extended |
|---|
| 597 | through the "pm_updates.py mechanism". This function returns the list of |
|---|
| 598 | fields that have been added by a sub-product by checking differences |
|---|
| 599 | between the p_baseSchema and the p_completedSchema.''' |
|---|
| 600 | baseFieldNames = baseSchema._fields |
|---|
| 601 | res = [] |
|---|
| 602 | for field in completedSchema.fields(): |
|---|
| 603 | fieldName = field.getName() |
|---|
| 604 | if fieldName.endswith('2'): continue |
|---|
| 605 | if (fieldName not in coreFieldNames) and \ |
|---|
| 606 | (fieldName not in baseFieldNames) and \ |
|---|
| 607 | (field.schemata != 'metadata'): |
|---|
| 608 | res.append(field) |
|---|
| 609 | if cols and (cols > 1): |
|---|
| 610 | # I need to group fields in sub-lists (cols is the number of fields by |
|---|
| 611 | # sublist). |
|---|
| 612 | newRes = [] |
|---|
| 613 | row = [] |
|---|
| 614 | for elem in res: |
|---|
| 615 | if len(row) == cols: |
|---|
| 616 | newRes.append(row) |
|---|
| 617 | row = [] |
|---|
| 618 | row.append(elem) |
|---|
| 619 | # Complete the last unfinished line if required. |
|---|
| 620 | if row: |
|---|
| 621 | while len(row) < cols: row.append(None) |
|---|
| 622 | newRes.append(row) |
|---|
| 623 | res = newRes |
|---|
| 624 | return res |
|---|
| 625 | |
|---|
| 626 | # ------------------------------------------------------------------------------ |
|---|
| 627 | def allowManagerToCreateIn(folder): |
|---|
| 628 | '''Allows me (Manager) to create meeting and items in p_folder.''' |
|---|
| 629 | folder.manage_permission(ADD_CONTENT_PERMISSIONS['MeetingItem'], |
|---|
| 630 | ('Manager', 'MeetingMember',), acquire=0) |
|---|
| 631 | folder.manage_permission(ADD_CONTENT_PERMISSIONS['Meeting'], |
|---|
| 632 | ('Manager', 'MeetingManager',), acquire=0) |
|---|
| 633 | |
|---|
| 634 | def disallowManagerToCreateIn(folder): |
|---|
| 635 | '''Disallows me (Manager) to create meeting and items in p_folder.''' |
|---|
| 636 | folder.manage_permission(ADD_CONTENT_PERMISSIONS['MeetingItem'], |
|---|
| 637 | ('MeetingMember',), acquire=0) |
|---|
| 638 | folder.manage_permission(ADD_CONTENT_PERMISSIONS['Meeting'], |
|---|
| 639 | ('MeetingManager',), acquire=0) |
|---|
| 640 | |
|---|
| 641 | # ------------------------------------------------------------------------------ |
|---|
| 642 | def getDateFromRequest(day, month, year, start): |
|---|
| 643 | '''This method produces a DateTime instance from info coming from a request. |
|---|
| 644 | p_hour and p_month may be ommitted. p_start is a bool indicating if the |
|---|
| 645 | date will be used as start date or end date; this will allow us to know |
|---|
| 646 | how to fill p_hour and p_month if they are ommitted. If _year is |
|---|
| 647 | ommitted, we will return a date near the Big bang (if p_start is True) |
|---|
| 648 | or near the Apocalypse (if p_start is False). p_day, p_month and p_year |
|---|
| 649 | are required to be valid string representations of integers.''' |
|---|
| 650 | # Determine day |
|---|
| 651 | if not day.strip() or (day == '00'): |
|---|
| 652 | if start: day = 1 |
|---|
| 653 | else: day = 30 |
|---|
| 654 | else: day = int(day) |
|---|
| 655 | # Determine month |
|---|
| 656 | if not month.strip() or (month == '00'): |
|---|
| 657 | if start: month = 1 |
|---|
| 658 | else: month = 12 |
|---|
| 659 | else: month = int(month) |
|---|
| 660 | if (month == 2) and (day == 30): day = 28 |
|---|
| 661 | # Determine year |
|---|
| 662 | if not year.strip() or (year == '0000'): |
|---|
| 663 | if start: year=1980 |
|---|
| 664 | else: year=3000 |
|---|
| 665 | else: |
|---|
| 666 | year = int(year) |
|---|
| 667 | try: |
|---|
| 668 | res = DateTime('%d/%d/%d' % (month, day, year)) |
|---|
| 669 | except DateTime.DateError: |
|---|
| 670 | # The date entered by the user is invalid. Take a default date. |
|---|
| 671 | if start: res = DateTime('1980/01/01') |
|---|
| 672 | else: res = DateTime('3000/12/31') |
|---|
| 673 | return res |
|---|
| 674 | |
|---|
| 675 | # ------------------------------------------------------------------------------ |
|---|
| 676 | def getDateFromDelta(aDate, delta): |
|---|
| 677 | '''This function returns a DateTime instance, which is computed from a |
|---|
| 678 | reference DateTime instance p_aDate to which a p_delta is applied. |
|---|
| 679 | A p_delta is a string having the form '<deltaDays>-<hour>:<minutes>, |
|---|
| 680 | where: |
|---|
| 681 | - 'deltaDays' is a positive or negative integer indicating the number of |
|---|
| 682 | days to add/remove; |
|---|
| 683 | - 'hour' and 'minutes' is the hour and minutes to set for the |
|---|
| 684 | computed date. It means that the hour and minutes of p_aDate are |
|---|
| 685 | ignored. |
|---|
| 686 | ''' |
|---|
| 687 | days, hour = delta.split('.') |
|---|
| 688 | return DateTime('%s %s' % ((aDate + int(days)).strftime('%Y/%m/%d'), hour)) |
|---|
| 689 | |
|---|
| 690 | # ------------------------------------------------------------------------------ |
|---|
| 691 | from AccessControl import ClassSecurityInfo |
|---|
| 692 | from App.class_init import InitializeClass |
|---|
| 693 | |
|---|
| 694 | class FakeMeetingUser: |
|---|
| 695 | '''Used as a replacement for a MeetingUser, ie: |
|---|
| 696 | * when the real MeetingUser has been deleted, |
|---|
| 697 | * when we need to get replacement-related info concatenated from several |
|---|
| 698 | MeetingUser instances. |
|---|
| 699 | ''' |
|---|
| 700 | security = ClassSecurityInfo() |
|---|
| 701 | def __init__(self, id, user=None, replacement=None): |
|---|
| 702 | self.id = id |
|---|
| 703 | if not user: return |
|---|
| 704 | bilingual = user.getField('duty2') |
|---|
| 705 | # MeetingUser instance p_user is replaced by a p_replacement user. |
|---|
| 706 | self.title = user.Title() |
|---|
| 707 | if bilingual: self.title2 = user.getTitle2() |
|---|
| 708 | self.duty = replacement.getReplacementDuty() |
|---|
| 709 | if bilingual: self.duty2 = replacement.getReplacementDuty2() |
|---|
| 710 | # If this person replaces another one, self.duty above is the duty of |
|---|
| 711 | # the person as replacement for the other one. We keep its original |
|---|
| 712 | # duty in self.originalDuty below. |
|---|
| 713 | self.originalDuty = user.getDuty() |
|---|
| 714 | if bilingual: self.originalDuty2 = user.getDuty2() |
|---|
| 715 | |
|---|
| 716 | security.declarePublic('getId') |
|---|
| 717 | def getId(self): return self.id |
|---|
| 718 | |
|---|
| 719 | security.declarePublic('Title') |
|---|
| 720 | def Title(self): return getattr(self, 'title', '') |
|---|
| 721 | |
|---|
| 722 | security.declarePublic('getDuty') |
|---|
| 723 | def getDuty(self, original=False): |
|---|
| 724 | if not original: return getattr(self, 'duty', '') |
|---|
| 725 | return getattr(self, 'originalDuty', '') |
|---|
| 726 | |
|---|
| 727 | security.declarePublic('getBilingual') |
|---|
| 728 | def getBilingual(self, name, force=1, sep='-'): |
|---|
| 729 | '''Gets the bilingual content of field named p_name (mimics |
|---|
| 730 | MeetingUser.getBilingual).''' |
|---|
| 731 | if force == 1: return getattr(self, name, '') |
|---|
| 732 | elif force == 2: return getattr(self, name+'2', '') |
|---|
| 733 | elif force == 'all': |
|---|
| 734 | return '%s%s%s' % (getattr(self, name, ''), sep, |
|---|
| 735 | getattr(self, name+'2', '')) |
|---|
| 736 | InitializeClass(FakeMeetingUser) |
|---|
| 737 | |
|---|
| 738 | def getMeetingUsers(obj, fieldName, theObjects=False, includeDeleted=True, |
|---|
| 739 | meetingForRepls=None): |
|---|
| 740 | '''Gets the meeting users defined on a given p_obj (item or meeting) within |
|---|
| 741 | a given p_fieldName. Here's the meaning of the remaining params: |
|---|
| 742 | * theObjects If True, the method will return MeetingUser instances |
|---|
| 743 | instead of MeetingUser IDs (False value is used for |
|---|
| 744 | Archetypes getters. |
|---|
| 745 | * includeDeleted (works only when p_theObjects is True) |
|---|
| 746 | If True, the method will return a FakeMeetingUser |
|---|
| 747 | instance for every MeetingUser that has been deleted. |
|---|
| 748 | * meetingForRepls (works only when p_theObjects is True) |
|---|
| 749 | If given, it is a Meeting instance; it means that we |
|---|
| 750 | need to take care of user replacements as defined on |
|---|
| 751 | this meeting. In this case we will return |
|---|
| 752 | a FakeMeetingUser instance for every replaced user, whose |
|---|
| 753 | "duty" will be initialized with the replacement duty of |
|---|
| 754 | the original user.''' |
|---|
| 755 | res = obj.getField(fieldName).get(obj) |
|---|
| 756 | if not theObjects: return res |
|---|
| 757 | cfg = obj.portal_plonemeeting.getMeetingConfig(obj) |
|---|
| 758 | newRes = [] |
|---|
| 759 | for id in res: |
|---|
| 760 | mUser = getattr(cfg.meetingusers, id, None) |
|---|
| 761 | if not mUser: |
|---|
| 762 | if includeDeleted: newRes.append(FakeMeetingUser(id)) |
|---|
| 763 | else: |
|---|
| 764 | if not meetingForRepls: # Simply add the MeetingUser to the result |
|---|
| 765 | newRes.append(mUser) |
|---|
| 766 | else: |
|---|
| 767 | newRes.append(mUser.getForUseIn(meetingForRepls)) |
|---|
| 768 | return newRes |
|---|
| 769 | |
|---|
| 770 | # ------------------------------------------------------------------------------ |
|---|
| 771 | class NightWork: |
|---|
| 772 | '''This class represents a task to perform by night, executed by method |
|---|
| 773 | tool.nightlife.''' |
|---|
| 774 | def __init__(self, action, type, params={}): |
|---|
| 775 | # If p_type is 'notification', this night work represents some task to |
|---|
| 776 | # achieve after a notification has been received by an external system. |
|---|
| 777 | # In this case, p_action is the name of the received event; method |
|---|
| 778 | # "tool.adapted().onNotify" will be called (see interfaces.py). If |
|---|
| 779 | # p_type is 'method', the night work consists in calling a method whose |
|---|
| 780 | # "path" in Zope is given in p_action, relative to the Plone site. |
|---|
| 781 | # For example, if p_action is "tool_plonemeeting.someextapp.sayHello", |
|---|
| 782 | # method "sayHello" will be called on the external application whose id |
|---|
| 783 | # is "someextapp" in the HS tool. |
|---|
| 784 | self.action = action |
|---|
| 785 | # As explained above, 2 types of nightworks can exist: events and |
|---|
| 786 | # method calls. |
|---|
| 787 | self.type = type |
|---|
| 788 | # One can specify, as a dict, parameters to give to the called method |
|---|
| 789 | # or onNotify. |
|---|
| 790 | self.params = params |
|---|
| 791 | |
|---|
| 792 | def perform(self, tool): |
|---|
| 793 | '''Performs the nightwork.''' |
|---|
| 794 | p = self.params |
|---|
| 795 | w = logger.warn |
|---|
| 796 | w('Executing nightwork...') |
|---|
| 797 | if self.type == 'notification': |
|---|
| 798 | w('=> tool.onNotify("%s", "%s")...' % (p['objectUrl'],p['event'])) |
|---|
| 799 | tool.adapted().onNotify(p['objectUrl'], p['event']) |
|---|
| 800 | elif self.type == 'method': |
|---|
| 801 | w('=> %s... Params: %s' % (self.action, self.params)) |
|---|
| 802 | exec 'tool.getParentNode().%s(**p)' % self.action |
|---|
| 803 | w('Nightwork done.') |
|---|
| 804 | |
|---|
| 805 | def matches(self, kw): |
|---|
| 806 | '''Returns True if this nightWork matches criteria in dict p_kw.''' |
|---|
| 807 | res = True |
|---|
| 808 | for name, value in kw.iteritems(): |
|---|
| 809 | if name in self.__dict__: |
|---|
| 810 | res = res and (getattr(self, name) == value) |
|---|
| 811 | elif self.params and (name in self.params): |
|---|
| 812 | res = res and (self.params.get(name) == value) |
|---|
| 813 | else: return |
|---|
| 814 | return res |
|---|
| 815 | |
|---|
| 816 | # ------------------------------------------------------------------------------ |
|---|
| 817 | mainTypes = ('MeetingItem', 'Meeting', 'MeetingFile') |
|---|
| 818 | def getFieldContent(obj, name, force=None, sep='-'): |
|---|
| 819 | '''Returns the content of p_field on p_obj. If content if available in |
|---|
| 820 | 2 languages, return the one that corresponds to user language, excepted |
|---|
| 821 | if p_force is integer 1 or 2: in this case it returns content in language |
|---|
| 822 | 1 or 2. If p_force is "all", it returns the content in both languages, |
|---|
| 823 | separated with p_sep.''' |
|---|
| 824 | global mainTypes |
|---|
| 825 | if force: |
|---|
| 826 | if force == 1: return obj.getField(name).get(obj) |
|---|
| 827 | elif force == 2: return obj.getField(name+'2').get(obj) |
|---|
| 828 | elif force == 'all': |
|---|
| 829 | return '%s%s%s' % (obj.getField(name).get(obj), sep, |
|---|
| 830 | obj.getField(name+'2').get(obj)) |
|---|
| 831 | field = obj.getField(name) |
|---|
| 832 | # Is content of this field bilingual? |
|---|
| 833 | tool = obj.portal_plonemeeting |
|---|
| 834 | adaptations = tool.getModelAdaptations() |
|---|
| 835 | if obj.meta_type in mainTypes: |
|---|
| 836 | bilingual = 'secondLanguage' in adaptations |
|---|
| 837 | else: |
|---|
| 838 | bilingual = 'secondLanguageCfg' in adaptations |
|---|
| 839 | if not bilingual: return field.get(obj) |
|---|
| 840 | else: |
|---|
| 841 | # Get the name of the 2 languages |
|---|
| 842 | languages = tool.getAvailableInterfaceLanguages().split(',')[:2] |
|---|
| 843 | userLanguage = tool.getUserLanguage() |
|---|
| 844 | if (userLanguage not in languages) or (userLanguage == languages[0]): |
|---|
| 845 | return field.get(obj) |
|---|
| 846 | else: |
|---|
| 847 | return obj.getField(field.getName()+'2').get(obj) |
|---|
| 848 | |
|---|
| 849 | def getFieldVersion(obj, name, changes): |
|---|
| 850 | '''Returns the content of field p_name on p_obj. If p_changes is True, |
|---|
| 851 | historical modifications of field content are highlighted.''' |
|---|
| 852 | lastVersion = obj.getField(name).get(obj) |
|---|
| 853 | if not changes: return lastVersion |
|---|
| 854 | # Return cumulative diff between successive versions of field |
|---|
| 855 | res = None |
|---|
| 856 | lastEvent = None |
|---|
| 857 | for event in obj.workflow_history[obj.getWorkflowName()]: |
|---|
| 858 | if (event['action'] == '_datachange_') and (name in event['changes']): |
|---|
| 859 | if res == None: |
|---|
| 860 | # We have found the first version of the field |
|---|
| 861 | res = event['changes'][name] |
|---|
| 862 | else: |
|---|
| 863 | # We need to produce the difference between current result and |
|---|
| 864 | # this version. |
|---|
| 865 | iMsg, dMsg = getHistoryTexts(obj, lastEvent) |
|---|
| 866 | comparator = HtmlDiff(res, event['changes'][name], iMsg, dMsg) |
|---|
| 867 | res = comparator.get() |
|---|
| 868 | lastEvent = event |
|---|
| 869 | # Now we need to compare the result with the current version. |
|---|
| 870 | iMsg, dMsg = getHistoryTexts(obj, lastEvent) |
|---|
| 871 | comparator = HtmlDiff(res, lastVersion, iMsg, dMsg) |
|---|
| 872 | return comparator.get() |
|---|
| 873 | |
|---|
| 874 | # ------------------------------------------------------------------------------ |
|---|
| 875 | def getLastEvent(obj, transition, notBefore='transfer'): |
|---|
| 876 | '''Returns, from the workflow history of p_obj, the event that corresponds |
|---|
| 877 | to the most recent triggering of p_transition (=its name). p_transition |
|---|
| 878 | can be a list of names: in this case, it returns the event about the most |
|---|
| 879 | recently triggered transition (ie, accept, refuse or delay). If |
|---|
| 880 | p_notBefore is given, it corresponds to a kind of start transition for |
|---|
| 881 | the search: we will not search in the history preceding the last |
|---|
| 882 | triggering of this transition. This is useful when history of an item |
|---|
| 883 | is the combined history of this item from several sites, and we want |
|---|
| 884 | to search only within history of the "last" site, so we want to ignore |
|---|
| 885 | everything that occurrred before the last "transfer" transition.''' |
|---|
| 886 | history = obj.workflow_history[obj.getWorkflowName()] |
|---|
| 887 | i = len(history)-1 |
|---|
| 888 | while i >= 0: |
|---|
| 889 | event = history[i] |
|---|
| 890 | if notBefore and (event['action'] == notBefore): return |
|---|
| 891 | if isinstance(transition, basestring): |
|---|
| 892 | condition = event['action'] == transition |
|---|
| 893 | else: |
|---|
| 894 | condition = event['action'] in transition |
|---|
| 895 | if condition: return event |
|---|
| 896 | i -= 1 |
|---|
| 897 | |
|---|
| 898 | # ------------------------------------------------------------------------------ |
|---|
| 899 | # History-related functions |
|---|
| 900 | # ------------------------------------------------------------------------------ |
|---|
| 901 | def rememberPreviousData(obj, name=None): |
|---|
| 902 | '''This method is called before updating p_obj and remembers, for every |
|---|
| 903 | historized field (or only for p_name if explicitly given), the previous |
|---|
| 904 | value. Result is a dict ~{s_fieldName: previousFieldValue}~''' |
|---|
| 905 | res = {} |
|---|
| 906 | cfg = obj.portal_plonemeeting.getMeetingConfig(obj) |
|---|
| 907 | isItem = obj.meta_type == 'MeetingItem' |
|---|
| 908 | # Do nothing if the object is not in a state when historization is enabled. |
|---|
| 909 | if isItem: meth = cfg.getRecordItemHistoryStates |
|---|
| 910 | else: meth = cfg.getRecordMeetingHistoryStates |
|---|
| 911 | if obj.queryState() not in meth(): return res |
|---|
| 912 | # Store in res the values currently stored on p_obj. |
|---|
| 913 | if isItem: historized = cfg.getHistorizedItemAttributes() |
|---|
| 914 | else: historized = cfg.getHistorizedMeetingAttributes() |
|---|
| 915 | if name: |
|---|
| 916 | if name in historized: res[name] = obj.getField(name).get(obj) |
|---|
| 917 | else: |
|---|
| 918 | for name in historized: res[name] = obj.getField(name).get(obj) |
|---|
| 919 | return res |
|---|
| 920 | |
|---|
| 921 | def addDataChange(obj, previousData=None): |
|---|
| 922 | '''This method adds a "data change" event in the object history. If the |
|---|
| 923 | previous data are not given in p_previousData, we look for it in |
|---|
| 924 | obj._v_previousData.''' |
|---|
| 925 | if previousData == None: |
|---|
| 926 | previousData = getattr(obj, '_v_previousData', None) |
|---|
| 927 | if not previousData: return |
|---|
| 928 | # Remove from p_previousData values that were not changed or that were empty |
|---|
| 929 | for name in previousData.keys(): |
|---|
| 930 | field = obj.getField(name) |
|---|
| 931 | oldValue = previousData[name] |
|---|
| 932 | if isinstance(oldValue, basestring): oldValue = oldValue.strip() |
|---|
| 933 | newValue = field.get(obj) |
|---|
| 934 | if isinstance(newValue, basestring): newValue = newValue.strip() |
|---|
| 935 | if oldValue == newValue: |
|---|
| 936 | del previousData[name] |
|---|
| 937 | if not previousData: return |
|---|
| 938 | # Add an event in the history |
|---|
| 939 | userId = obj.portal_membership.getAuthenticatedMember().getId() |
|---|
| 940 | event = {'action': '_datachange_', 'actor': userId, 'time': DateTime(), |
|---|
| 941 | 'comments': '', 'review_state': obj.queryState(), |
|---|
| 942 | 'changes': previousData} |
|---|
| 943 | if hasattr(obj, '_v_previousData'): del obj._v_previousData |
|---|
| 944 | # Add the event to the history |
|---|
| 945 | obj.workflow_history[obj.getWorkflowName()] += (event,) |
|---|
| 946 | |
|---|
| 947 | def hasHistory(obj, fieldName=None): |
|---|
| 948 | '''Has p_obj an history? If p_fieldName is specified, the question is: has |
|---|
| 949 | p_obj an history for field p_fieldName?''' |
|---|
| 950 | wfName = obj.getWorkflowName() |
|---|
| 951 | if hasattr(obj.aq_base, 'workflow_history') and obj.workflow_history and \ |
|---|
| 952 | (wfName in obj.workflow_history): |
|---|
| 953 | for event in obj.workflow_history[wfName]: |
|---|
| 954 | if not fieldName: |
|---|
| 955 | condition = event['action'] |
|---|
| 956 | else: |
|---|
| 957 | condition = (event['action'] == '_datachange_') and \ |
|---|
| 958 | (fieldName in event['changes']) |
|---|
| 959 | if condition: return True |
|---|
| 960 | |
|---|
| 961 | def findNewValue(obj, name, history, stopIndex): |
|---|
| 962 | '''This function tries to find a more recent version of value of field |
|---|
| 963 | p_name on p_obj. It first tries to find it in history[:stopIndex+1]. If |
|---|
| 964 | it does not find it there, it returns the current value on p_obj.''' |
|---|
| 965 | i = stopIndex+1 |
|---|
| 966 | while (i-1) >= 0: |
|---|
| 967 | i -= 1 |
|---|
| 968 | if history[i]['action'] != '_datachange_': continue |
|---|
| 969 | if name not in history[i]['changes']: continue |
|---|
| 970 | # We have found it! |
|---|
| 971 | return history[i]['changes'][name] |
|---|
| 972 | return obj.getField(name).get(obj) |
|---|
| 973 | |
|---|
| 974 | def getHistoryTexts(obj, event): |
|---|
| 975 | '''Returns a tuple (insertText, deleteText) containing texts to show on, |
|---|
| 976 | respectively, inserted and deleted chunks of text.''' |
|---|
| 977 | userName = obj.portal_plonemeeting.getUserName(event['actor']) |
|---|
| 978 | mapping = {'userName': userName.decode('utf-8')} |
|---|
| 979 | res = [] |
|---|
| 980 | for type in ('insert', 'delete'): |
|---|
| 981 | msg = obj.translate('history_%s' % type, mapping=mapping, |
|---|
| 982 | domain='PloneMeeting') |
|---|
| 983 | date = obj.portal_plonemeeting.formatDate(event['time'], |
|---|
| 984 | short=True, withHour=True) |
|---|
| 985 | msg = '%s: %s' % (date, msg) |
|---|
| 986 | res.append(msg.encode('utf-8')) |
|---|
| 987 | return res |
|---|
| 988 | |
|---|
| 989 | def getHistory(obj, startNumber=0, batchSize=5): |
|---|
| 990 | '''Returns the history for this object, sorted in reverse order (most |
|---|
| 991 | recent change first) if p_reverse is True.''' |
|---|
| 992 | res = [] |
|---|
| 993 | history = list(obj.workflow_history[obj.getWorkflowName()][1:]) |
|---|
| 994 | history.reverse() |
|---|
| 995 | stopIndex = startNumber + batchSize - 1 |
|---|
| 996 | i = -1 |
|---|
| 997 | while (i+1) < len(history): |
|---|
| 998 | i += 1 |
|---|
| 999 | # Keep only events in range startNumber:startNumber+batchSize |
|---|
| 1000 | if i < startNumber: continue |
|---|
| 1001 | if i > stopIndex: break |
|---|
| 1002 | event = history[i] |
|---|
| 1003 | if event['action'] == '_datachange_': |
|---|
| 1004 | # We take a copy, because we will modify it. |
|---|
| 1005 | event = history[i].copy() |
|---|
| 1006 | event['changes'] = {} |
|---|
| 1007 | for name, oldValue in history[i]['changes'].iteritems(): |
|---|
| 1008 | widgetName = obj.getField(name).widget.getName() |
|---|
| 1009 | if widgetName == 'RichWidget': |
|---|
| 1010 | if kupuFieldIsEmpty(oldValue): |
|---|
| 1011 | val = '-' |
|---|
| 1012 | else: |
|---|
| 1013 | newValue = findNewValue(obj, name, history, i-1) |
|---|
| 1014 | # Compute the diff between oldValue and newValue |
|---|
| 1015 | iMsg, dMsg = getHistoryTexts(obj, event) |
|---|
| 1016 | comparator = HtmlDiff(oldValue, newValue, iMsg, dMsg) |
|---|
| 1017 | val = comparator.get() |
|---|
| 1018 | event['changes'][name] = val |
|---|
| 1019 | elif widgetName == 'BooleanWidget': |
|---|
| 1020 | label = oldValue and 'Yes' or 'No' |
|---|
| 1021 | event['changes'][name] = obj.translate(label) |
|---|
| 1022 | elif widgetName == 'TextAreaWidget': |
|---|
| 1023 | val = oldValue.replace('\r', '').replace('\n', '<br/>') |
|---|
| 1024 | event['changes'][name] = val |
|---|
| 1025 | elif widgetName == 'SelectionWidget': |
|---|
| 1026 | allValues = getattr(obj, obj.getField(name).vocabulary)() |
|---|
| 1027 | val = allValues.getValue(oldValue or '') |
|---|
| 1028 | event['changes'][name] = val or '-' |
|---|
| 1029 | elif widgetName == 'MultiSelectionWidget': |
|---|
| 1030 | allValues = getattr(obj, obj.getField(name).vocabulary)() |
|---|
| 1031 | val = [allValues.getValue(v) for v in oldValue] |
|---|
| 1032 | if not val: val = '-' |
|---|
| 1033 | else: val = '<br/>'.join(val) |
|---|
| 1034 | event['changes'][name] = val |
|---|
| 1035 | else: |
|---|
| 1036 | event['changes'][name] = oldValue |
|---|
| 1037 | res.append(event) |
|---|
| 1038 | return {'events': res, 'totalNumber': len(history)} |
|---|
| 1039 | |
|---|
| 1040 | # ------------------------------------------------------------------------------ |
|---|
| 1041 | def setFieldFromAjax(obj, fieldName, newValue): |
|---|
| 1042 | '''Sets on p_obj the content of a field whose name is p_fieldName and whose |
|---|
| 1043 | new value is p_fieldValue. This method is called by Ajax pages.''' |
|---|
| 1044 | field = obj.getField(fieldName) |
|---|
| 1045 | # Keep old value, we might need to historize it. |
|---|
| 1046 | previousData = rememberPreviousData(obj, fieldName) |
|---|
| 1047 | field.set(obj, newValue) |
|---|
| 1048 | # Potentially store it in object history |
|---|
| 1049 | if previousData: addDataChange(obj, previousData) |
|---|
| 1050 | # Update the last modification date |
|---|
| 1051 | obj.modification_date = DateTime() |
|---|
| 1052 | # Apply XHTML transforms when relevant |
|---|
| 1053 | obj.transformAllRichTextFields(onlyField=fieldName) |
|---|
| 1054 | obj.reindexObject() |
|---|
| 1055 | |
|---|
| 1056 | # ------------------------------------------------------------------------------ |
|---|
| 1057 | class ZCTextIndexInfo: |
|---|
| 1058 | '''Silly class used for storing information about a ZCTextIndex.''' |
|---|
| 1059 | lexicon_id = "plone_lexicon" |
|---|
| 1060 | index_type = 'Okapi BM25 Rank' |
|---|
| 1061 | |
|---|
| 1062 | def updateIndexes(ploneSite, indexInfo, logger): |
|---|
| 1063 | '''This method creates or updates, in a p_ploneSite, definitions of indexes |
|---|
| 1064 | in its portal_catalog, based on index-related information given in |
|---|
| 1065 | p_indexInfo. p_indexInfo is a dictionary of the form |
|---|
| 1066 | {s_indexName:s_indexType}. Here are some examples of index types: |
|---|
| 1067 | "FieldIndex", "ZCTextIndex", "DateIndex".''' |
|---|
| 1068 | catalog = ploneSite.portal_catalog |
|---|
| 1069 | zopeCatalog = catalog._catalog |
|---|
| 1070 | for indexName, indexType in indexInfo.iteritems(): |
|---|
| 1071 | # If this index already exists but with a different type, remove it. |
|---|
| 1072 | if (indexName in zopeCatalog.indexes): |
|---|
| 1073 | oldType = zopeCatalog.indexes[indexName].__class__.__name__ |
|---|
| 1074 | if oldType != indexType: |
|---|
| 1075 | catalog.delIndex(indexName) |
|---|
| 1076 | logger.info('Existing index "%s" of type "%s" was removed:'\ |
|---|
| 1077 | ' we need to recreate it with type "%s".' % \ |
|---|
| 1078 | (indexName, oldType, indexType)) |
|---|
| 1079 | if indexName not in zopeCatalog.indexes: |
|---|
| 1080 | # We need to create this index |
|---|
| 1081 | if indexType != 'ZCTextIndex': |
|---|
| 1082 | catalog.addIndex(indexName, indexType) |
|---|
| 1083 | else: |
|---|
| 1084 | catalog.addIndex(indexName,indexType,extra=ZCTextIndexInfo) |
|---|
| 1085 | # Indexing database content based on this index. |
|---|
| 1086 | catalog.reindexIndex(indexName, ploneSite.REQUEST) |
|---|
| 1087 | logger.info('Created index "%s" of type "%s"...' % \ |
|---|
| 1088 | (indexName, indexType)) |
|---|
| 1089 | |
|---|
| 1090 | # ------------------------------------------------------------------------------ |
|---|
| 1091 | tokens = ( ('<li', -1), ('</li>', 5)) |
|---|
| 1092 | crlf = ('\r', '\n') |
|---|
| 1093 | def formatXhtmlFieldForAppy(value): |
|---|
| 1094 | '''p_value is a string containing XHTML code. This code must follow some |
|---|
| 1095 | rules to be Appy-compliant (ie, for appy.shared.XhtmlDiff to work |
|---|
| 1096 | properly).''' |
|---|
| 1097 | global tokens |
|---|
| 1098 | global crlf |
|---|
| 1099 | for token, delta in tokens: |
|---|
| 1100 | res = [] |
|---|
| 1101 | i = 0 # Where I am in p_value |
|---|
| 1102 | while i < len(value): |
|---|
| 1103 | j = value.find(token, i) |
|---|
| 1104 | if j == -1: |
|---|
| 1105 | # No more occurrences. Dump the end of p_value in the result. |
|---|
| 1106 | res.append(value[i:]) |
|---|
| 1107 | break |
|---|
| 1108 | res.append(value[i:j]) |
|---|
| 1109 | deltaj = j+delta |
|---|
| 1110 | if (delta < 0) and (j > 0) and (value[deltaj] not in crlf): |
|---|
| 1111 | res.append('\n') |
|---|
| 1112 | i = j + len(token) |
|---|
| 1113 | res.append(value[j:i]) |
|---|
| 1114 | if (delta > 0) and (j > 0) and (value[deltaj] not in crlf): |
|---|
| 1115 | res.append('\n') |
|---|
| 1116 | value = ''.join(res) |
|---|
| 1117 | return value |
|---|
| 1118 | # ------------------------------------------------------------------------------ |
|---|