def persist(data): if isinstance(data, dict): data = PersistentMapping(data) for key, value in data.items(): data[key] = persist(value) elif isinstance(data, list): return PersistentList(map(persist, data)) else: # Usually we got basestrings, or integer here, so do nothing. pass return data
def checkBasicOps(self): from persistent.mapping import PersistentMapping m = PersistentMapping({'x': 1}, a=2, b=3) m['name'] = 'bob' self.assertEqual(m['name'], "bob") self.assertEqual(m.get('name', 42), "bob") self.assert_('name' in m) try: m['fred'] except KeyError: pass else: self.fail("expected KeyError") self.assert_('fred' not in m) self.assertEqual(m.get('fred'), None) self.assertEqual(m.get('fred', 42), 42) keys = m.keys() keys.sort() self.assertEqual(keys, ['a', 'b', 'name', 'x']) values = m.values() values.sort() self.assertEqual(values, [1, 2, 3, 'bob']) items = m.items() items.sort() self.assertEqual(items, [('a', 2), ('b', 3), ('name', 'bob'), ('x', 1)]) keys = list(m.iterkeys()) keys.sort() self.assertEqual(keys, ['a', 'b', 'name', 'x']) values = list(m.itervalues()) values.sort() self.assertEqual(values, [1, 2, 3, 'bob']) items = list(m.iteritems()) items.sort() self.assertEqual(items, [('a', 2), ('b', 3), ('name', 'bob'), ('x', 1)])
def make_persistent(data): if isinstance(data, dict) or \ isinstance(data, PersistentMapping): data = PersistentMapping(data) for key, value in data.items(): value = make_persistent(value) data[key] = value elif isinstance(data, list) or \ isinstance(data, PersistentList): new_data = PersistentList() for item in data: new_data.append(make_persistent(item)) data = new_data return data
def checkBasicOps(self): from persistent.mapping import PersistentMapping m = PersistentMapping({"x": 1}, a=2, b=3) m["name"] = "bob" self.assertEqual(m["name"], "bob") self.assertEqual(m.get("name", 42), "bob") self.assert_("name" in m) try: m["fred"] except KeyError: pass else: self.fail("expected KeyError") self.assert_("fred" not in m) self.assertEqual(m.get("fred"), None) self.assertEqual(m.get("fred", 42), 42) keys = m.keys() keys.sort() self.assertEqual(keys, ["a", "b", "name", "x"]) values = m.values() values.sort() self.assertEqual(values, [1, 2, 3, "bob"]) items = m.items() items.sort() self.assertEqual(items, [("a", 2), ("b", 3), ("name", "bob"), ("x", 1)]) keys = list(m.iterkeys()) keys.sort() self.assertEqual(keys, ["a", "b", "name", "x"]) values = list(m.itervalues()) values.sort() self.assertEqual(values, [1, 2, 3, "bob"]) items = list(m.iteritems()) items.sort() self.assertEqual(items, [("a", 2), ("b", 3), ("name", "bob"), ("x", 1)])
class NotificationTool(UniqueObject, SimpleItem, PropertyManager): """Main notification tool.""" id = ID title = TITLE meta_type = META_TYPE manage_options = (PropertyManager.manage_options + SimpleItem.manage_options) ## Extra subscriptions extra_subscriptions_enabled = False extra_subscriptions_recursive = True ## Debug mode debug_mode = False ## Ignore rules ignore_rules = DEFAULT_IGNORE_RULES ## Item creation item_creation_notification_enabled = False on_item_creation_users = [] on_item_creation_mail_template = [] ## Item modification item_modification_notification_enabled = False on_item_modification_users = [] on_item_modification_mail_template = [] ## Item removal item_removal_notification_enabled = False on_item_removal_users = [] on_item_removal_mail_template = [] ## Workflow transition wf_transition_notification_enabled = False on_wf_transition_users = [] on_wf_transition_mail_template = [] ## Member registration member_registration_notification_enabled = False on_member_registration_users = [] on_member_registration_mail_template = [] ## Member modification member_modification_notification_enabled = False on_member_modification_users = [] on_member_modification_mail_template = [] ## Discussion item creation discussion_item_creation_notification_enabled = False on_discussion_item_creation_users = [] on_discussion_item_creation_mail_template = [] _properties = ({'id': 'extra_subscriptions_enabled', 'label': 'Enable extra subscriptions', 'mode': 'w', 'type': 'boolean'}, {'id': 'extra_subscriptions_recursive', 'label': 'Toggle recursive mode for extra subscriptions', 'mode': 'w', 'type': 'boolean'}, {'id': 'debug_mode', 'label': 'Toggle debug mode', 'mode': 'w', 'type': 'boolean'}, {'id': 'ignore_rules', 'label': 'Rules (ignore)', 'mode': 'w', 'type': 'lines'}, {'id': 'item_creation_notification_enabled', 'label': 'Enable item creation notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_item_creation_users', 'label': 'Rules on item creation (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_item_creation_mail_template', 'label': 'Rules on item creation (mail template)', 'mode': 'w', 'type': 'lines'}, {'id': 'item_modification_notification_enabled', 'label': 'Enable item modification notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_item_modification_users', 'label': 'Rules on item modification (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_item_modification_mail_template', 'label': 'Rules on item modification (mail template)', 'mode': 'w', 'type': 'lines'}, {'id': 'item_removal_notification_enabled', 'label': 'Enable item removal notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_item_removal_users', 'label': 'Rules on item removal (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_item_removal_mail_template', 'label': 'Rules on item removal (mail template)', 'mode': 'w', 'type': 'lines'}, {'id': 'wf_transition_notification_enabled', 'label': 'Enable workflow transition notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_wf_transition_users', 'label': 'Rules on workflow transition (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_wf_transition_mail_template', 'label': 'Rules on workflow transition (mail template)', 'mode': 'w', 'type': 'lines'}, {'id': 'member_registration_notification_enabled', 'label': 'Enable member registration notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_member_registration_users', 'label': 'Rules on member registration (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_member_registration_mail_template', 'label': 'Rules on member registration (mail template)', 'mode': 'w', 'type': 'lines'}, {'id': 'member_modification_notification_enabled', 'label': 'Enable member modification notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_member_modification_users', 'label': 'Rules on member modification (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_member_modification_mail_template', 'label': 'Rules on member modification (mail template)', 'mode': 'w', 'type': 'lines'}, {'id': 'discussion_item_creation_notification_enabled', 'label': 'Enable discussion item creation notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_discussion_item_creation_users', 'label': 'Rules on discussion item creation (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_discussion_item_creation_mail_template', 'label': 'Rules on discussion item creation (mail template)', 'mode': 'w', 'type': 'lines'}, ) security = ClassSecurityInfo() decPrivate = security.declarePrivate decProtected = security.declareProtected decPublic = security.declarePublic def __init__(self, *args, **kwargs): self._uid_to_path = PersistentMapping() self._subscriptions = PersistentMapping() ################################################################# ## Notification handlers ######################## decPrivate('onItemCreation') def onItemCreation(self, obj): """Handler called when an item is created. It returns the number of mails which have been sent. **Warning:** this handler is not called when a discussion item is added. In this case, ``onDiscussionItemCreation()`` is called instead. """ if not self.getProperty('item_creation_notification_enabled'): return 0 if self.ignoreNotification(obj): return 0 extra_bindings = getBasicBindings(obj) return self._handlerHelper(obj, 'item_creation', extra_bindings, extra_bindings, extra_bindings) decPrivate('onItemModification') def onItemModification(self, obj): """Handler called when an item is modified. It returns the number of mails which have been sent. """ if not self.getProperty('item_modification_notification_enabled'): return 0 if self.ignoreNotification(obj): return 0 extra_bindings = getBasicBindings(obj) extra_bindings.update({'current': obj, 'previous': getPreviousVersion(obj)}) return self._handlerHelper(obj, 'item_modification', extra_bindings, extra_bindings, extra_bindings) decPrivate('onItemRemoval') def onItemRemoval(self, obj): """Handler called when an item is removed. It returns the number of mails which have been sent. """ if not self.getProperty('item_removal_notification_enabled'): return 0 if self.ignoreNotification(obj): return 0 extra_bindings = getBasicBindings(obj) return self._handlerHelper(obj, 'item_removal', extra_bindings, extra_bindings, extra_bindings) decPrivate('onWorkflowTransition') def onWorkflowTransition(self, obj, action): """Handler called when a workflow transition is triggered. It returns the number of mails which have been sent. """ if not self.getProperty('wf_transition_notification_enabled'): return 0 if self.ignoreNotification(obj): return 0 wtool = getToolByName(self, 'portal_workflow') comments = wtool.getInfoFor(obj, 'comments') extra_bindings = getBasicBindings(obj) extra_bindings.update({'transition': action, 'comments': comments, 'previous_state': getPreviousWorkflowState(obj)}) return self._handlerHelper(obj, 'wf_transition', extra_bindings, extra_bindings, extra_bindings) decPrivate('onMemberRegistration') def onMemberRegistration(self, member, properties): """Handler called when a new portal member has been registered. It returns the number of mails which have been sent. """ if not self.getProperty('member_registration_notification_enabled'): return 0 if self.ignoreNotification(member): return 0 if properties is None: properties = {} ## FIXME: How could it be? (Damien) current_user = getSecurityManager().getUser() extra_bindings = getBasicBindings(member) extra_bindings.update({'current_user': current_user, 'member': member, 'properties': properties, 'event': 'registration'}) return self._handlerHelper(member, 'member_registration', extra_bindings, extra_bindings, extra_bindings) decPrivate('onMemberModification') def onMemberModification(self, member): """Handler called when a member changes his/her properties. It returns the number of mails which have been sent. """ ## FIXME: this should go away when we rely on the appropriate ## event. ## This method can also be called when the member is ## registered. We have to check that. stack = inspect.stack() ## 1st item is ourself ## 2nd item is 'CMFCore.MemberDataTool.notifyMemberModified()' ## 3rd item is 'CMFCore.MemberDataTool.setMemberProperties()' ## 4th item is what we want to check: it is either 'addMember' ## or 'setProperties()' caller = stack[3][3] if caller != 'setProperties': return 0 if not self.getProperty('member_modification_notification_enabled'): return 0 if self.ignoreNotification(member): return 0 ## FIXME: what is the purpose of the following lines? (Damien) memberdata = getToolByName(self, 'portal_memberdata') properties = {} for key, value in memberdata.propertyItems(): properties[key] = value current_user = getSecurityManager().getUser() extra_bindings = getBasicBindings(None) extra_bindings.update({'current_user': current_user, 'member': member, 'properties': properties, 'event': 'modification'}) return self._handlerHelper(member, 'member_modification', extra_bindings, extra_bindings, extra_bindings) decPrivate('onDiscussionItemCreation') def onDiscussionItemCreation(self, discussion_item): """Handler called when a discussion item is created. It returns the number of mails which have been sent. """ if not self.getProperty('discussion_item_creation_notification_enabled'): return 0 if self.ignoreNotification(discussion_item): return 0 ## We add two bindings to disambiguate the meaning of 'here' ## in the mail template and the rules: 'discussion_item' and ## 'discussed_item'. discussed_item = discussion_item while discussed_item.meta_type == discussion_item.meta_type: discussed_item = discussed_item.aq_inner.aq_parent.aq_parent extra_bindings = getBasicBindings(discussed_item) extra_bindings.update({'discussion_item': discussion_item, 'discussed_item': discussed_item}) return self._handlerHelper(discussion_item, 'discussion_item_creation', extra_bindings, extra_bindings, extra_bindings) def _handlerHelper(self, obj, what, get_users_extra_bindings, mail_template_extra_bindings, mail_template_options): """An helper method for ``on*()`` handlers. It returns the number of mails which have been sent. """ self._updateSubscriptionMapping(obj) users_by_label = self.getUsersToNotify(obj, what, get_users_extra_bindings) if self.isExtraSubscriptionsEnabled(): users = users_by_label.get('', []) users.extend(self.getExtraSubscribersOf(self._getPath(obj)).items()) users_by_label[''] = users n_sent = 0 for label, users in users_by_label.items(): users = self.removeUnAuthorizedSubscribers(users, obj) mail_template_extra_bindings['label'] = label for user, how in users: # Fetch the delivery utilities the user requested, then use # that to notify for h in how: try: delivery = getUtility(INotificationDelivery, h) except: # The method is not known, or the third party # product that provided it was uninstalled LOG.warning("Could not look up INotificationDelivery "\ "utility named '%s'", h) continue n_sent += delivery.notify(obj, user, what, label, get_users_extra_bindings, mail_template_extra_bindings, mail_template_options) return n_sent ################################################################# ################################################################# ## Utility methods ############################### decPrivate('ignoreNotification') def ignoreNotification(self, obj): """Return whether notification have been set to be ignored for ``obj``. """ ec = getExpressionContext(obj) for match_expr in self.getProperty('ignore_rules', ()): try: if self._match(match_expr, ec): return True except ConflictError: raise except: LOG.error("Error in 'ignore_rules' rule "\ "('%s') for '%s'", match_expr, obj.absolute_url(1), exc_info=True) return False decPrivate('getUsersToNotify') def getUsersToNotify(self, obj, what, ec_bindings=None): """Return a mapping from label to a list of user/how tuples, based on the passed ``what`` and ``ob``. ``what`` is one of the implemented notifications (*item_modification*, *wf_transition*, etc.). ``how`` is a string that says which delivery method to use. ``ec_bindings`` is a mapping which is injected into the expression context of the expression of the rules. """ rules = self.getProperty('on_%s_users' % what, None) if rules is None: raise NotImplementedError, \ "Notification on '%s' is not implemented." % what ec = getExpressionContext(obj, ec_bindings) users_by_label = {} ignore_next_rules = False for rule in rules: try: match_expr, users_expr = rule.split(RULE_DELIMITER, 1) parts = users_expr.split(RULE_DELIMITER) label = '' how = ('mail',) if len(parts) > 1: users_expr, label = parts[:2] if len(parts) > 2: how = tuple([p.strip() for p in parts[2:]]) except ValueError: LOG.error("'%s' is not a valid rule "\ "('on_%s_users' on '%s')", rule, what, obj.absolute_url(1)) continue match_expr = match_expr.strip() users_expr = users_expr.strip() label = label.strip() users = users_by_label.get(label, []) try: if not self._match(match_expr, ec): continue except ConflictError: raise except: LOG.error("Error in 'on_%s_users' rule "\ "('%s') for '%s'", what, match_expr, obj.absolute_url(1), exc_info=True) continue if users_expr == '*': users.extend([(u, how) for u in self.getAllUsers()]) ignore_next_rules = True else: try: users.extend([(u, how) for u in Expression(users_expr)(ec)]) except ConflictError: raise except: LOG.error("Error in 'on_%s_users' rule "\ "('%s') for '%s'", what, users_expr, obj.absolute_url(1), exc_info=True) users_by_label[label] = users if ignore_next_rules: break return users_by_label decPrivate('getTemplate') def getTemplate(self, obj, what, ec_bindings=None): """Return the template to notify for the ``what`` of an object ``obj``, ``what`` being one of the implemented notification ("*item_modification*", "*wf_transition*", etc.), or ``None`` if none could be found. ``ec_bindings`` is a mapping which is injected into the expression context of the expression of the rules. """ rules = self.getProperty('on_%s_mail_template' % what, None) if rules is None: raise NotImplementedError, \ 'Notification on "%s" is not implemented.' ec = getExpressionContext(obj, ec_bindings) template = None for rule in rules: try: match_expr, template_expr = rule.split(RULE_DELIMITER) match_expr, template_expr = match_expr.strip(), template_expr.strip() except ValueError: LOG.error("'%s' is not a valid rule "\ "('on_%s_mail_template' on '%s')", rule, what, obj.absolute_url(1)) continue match_expr = match_expr.strip() template_expr = template_expr.strip() try: if not self._match(match_expr, ec): continue except ConflictError: raise except: LOG.error("Error in 'on_%s_mail_template' rule "\ "('%s') for '%s'", what, match_expr, obj.absolute_url(1), exc_info=True) continue try: template = Expression(template_expr)(ec) except ConflictError: raise except: LOG.error("Error in 'on_%s_mail_template' rule "\ "('%s') for '%s'", what, template_expr, obj.absolute_url(1), exc_info=True) continue if type(template) == StringType: template = obj.restrictedTraverse(template, None) if template is not None: break return template decPrivate('getAllUsers') def getAllUsers(self): """Return a list of all user ids of the portal. **Warning:** this method may be costly if you rely on an external (non ZODB) user source. Use it at your own risk. """ mtool = getToolByName(self, 'portal_membership') return mtool.listMemberIds() decPrivate('removeUnAuthorizedSubscribers') def removeUnAuthorizedSubscribers(self, subscribers, obj): """Return users from ``subscribers`` who are authorized to view ``obj``. """ portal = getToolByName(self, 'portal_url').getPortalObject() mtool = getToolByName(self, 'portal_membership') filtered_subscribers = [] for subscriber, notifymethod in subscribers: if self._anonymousShouldBeNotified(obj): filtered_subscribers.append((subscriber, notifymethod)) else: ## We use '_huntUser()' and not 'mtool.getMemberById()' ## because the latter would provide a wrapped user ## object, with a specific context where the user is ## not allowed to view 'obj'. member = mtool._huntUser(str(subscriber), portal) if member is not None: if member.has_permission('View', obj): filtered_subscribers.append((subscriber, notifymethod)) return filtered_subscribers def _match(self, expr, ec): """Return ``True`` if ``expr`` returns something which can be evaluated to ``True`` in the expression context (``ec``) or if ``expr`` is "*". """ if expr == '*': return True expr = Expression(expr) return bool(expr(ec)) def _getPath(self, obj): """Return path of ``obj``. A slash (``/``) is appended to the path if the object is folderish. The returned path is relative to the portal object. """ utool = getToolByName(self, 'portal_url') path = utool.getRelativeContentURL(obj) path = '/' + path if not getattr(obj.aq_base, 'isPrincipiaFolderish', False): return path if path[-1] != '/': path += '/' return path def _getParents(self, path): """Get the parents of the item corresponding to ``path`` and return their respective path. Parents are returned from ``path`` to the portal root object. """ if path == '/': return [] if path[-1] == '/': path = path[:-1] parent = path[:path.rfind('/') + 1] parents = [parent] parents.extend(self._getParents(parent)) return tuple(parents) def _getUID(self, obj): """Return UID of the object.""" if not IATContentType.providedBy(obj): return None portal_uidhandler = getToolByName(self, 'portal_uidhandler') uid = portal_uidhandler.queryUid(obj, None) if uid is None: ## Not yet registered uid = portal_uidhandler.register(obj) return uid def _anonymousShouldBeNotified(self, obj): """Return whether anonymous users should be notified, i.e. whether anonymous users can view ``obj``. """ return 'Anonymous' in rolesForPermissionOn('View', obj) ################################################################# ################################################################# ## Extra subscriptions settings ############################### decProtected('View', 'isExtraSubscriptionsEnabled') def isExtraSubscriptionsEnabled(self): """Return whether extra subscriptions are enabled.""" return self.getProperty('extra_subscriptions_enabled') decProtected('View', 'isExtraSubscriptionsRecursive') def isExtraSubscriptionsRecursive(self): """Return whether extra subscriptions are recursive. Note that this method does not check whether extra subscriptions are enabled or not. """ return self.getProperty('extra_subscriptions_recursive') ################################################################# ################################################################# ## Extra subscriptions logic ############################ def _updateSubscriptionMapping(self, obj): """Update subscription mapping.""" uid = self._getUID(obj) if not uid: return path = self._getPath(obj) known_path = self._uid_to_path.get(uid) if known_path != path: self._uid_to_path[uid] = path if known_path is not None: ## We have old informations for this object for key, value in self._subscriptions.items(): if key.startswith(known_path): new_key = path + key[len(known_path) : ] self._subscriptions[new_key] = value del self._subscriptions[key] decPublic('currentUserHasSubscribePermission') def currentUserHasSubscribePermissionOn(self, obj): """Return whether the current user is allowed to subscribe to or unsubscribe from ``obj``. """ if not IATContentType.providedBy(obj) and not \ IPloneSiteRoot.providedBy(obj): return False mtool = getToolByName(self, 'portal_membership') return mtool.checkPermission(SUBSCRIBE_PERMISSION, obj) decPublic('subscribeTo') def subscribeTo(self, obj, email=None, how=['mail']): """Subscribe ``email`` (or the current user if ``email`` is None) to ``obj``. You can pass the methods by which the user should be notified as a tuple using the ``how`` keyword argument. """ if not self.isExtraSubscriptionsEnabled(): raise DisabledFeature if not self.currentUserHasSubscribePermissionOn(obj): raise Unauthorized elif email is not None: if not EMAIL_REGEXP.match(email): raise InvalidEmailAddress ## FIXME: an anonymous user would like to subscribe ## his/her address. This has not yet been implemented, so ## we raise an exception. raise NotImplementedError else: self._updateSubscriptionMapping(obj) path = self._getPath(obj) subscribers = self._subscriptions.get(path, {}) user = getSecurityManager().getUser().getId() subscribers[user] = tuple(how) self._subscriptions[path] = subscribers decPublic('unSubscribeFrom') def unSubscribeFrom(self, obj, email=None): """Unsubscribe ``email`` (or the current user if ``email`` is ``None``) from ``obj``. """ if not self.isExtraSubscriptionsEnabled(): raise DisabledFeature if not self.currentUserHasSubscribePermissionOn(obj): raise Unauthorized elif email is not None: if not EMAIL_REGEXP.match(email): raise InvalidEmailAddress ## FIXME: an anonymous user would like to unsubscribe ## his/her address. This has not yet been implemented, so ## we raise an exception. raise NotImplementedError else: self._updateSubscriptionMapping(obj) path = self._getPath(obj) subscribers = self._subscriptions.get(path, {}) user = getSecurityManager().getUser().getId() try: del subscribers[user] self._subscriptions[path] = subscribers except KeyError: pass ## User was not subscribed. decPublic('unSubscribeFromObjectAbove') def unSubscribeFromObjectAbove(self, obj, email=None): """Find folderish items above ``obj`` and unsubscribe ``email`` (or the current user if ``email`` is ``None``) from the first one (s)he is subscribed to. If ``user`` is subscribed to ``obj``, this method is equivalent to ``unSubscribeFrom(obj, user)``. """ if not self.isExtraSubscriptionsEnabled(): raise DisabledFeature if not self.currentUserHasSubscribePermissionOn(obj): raise Unauthorized elif email is not None: if not EMAIL_REGEXP.match(email): raise InvalidEmailAddress ## FIXME: an anonymous user would like to unsubscribe ## his/her address. This has not yet been implemented, so ## we raise an exception. raise NotImplementedError else: self._updateSubscriptionMapping(obj) utool = getToolByName(obj, 'portal_url') portal = utool.getPortalObject() portal_container = portal.aq_inner.aq_parent while obj != portal_container: if self.isSubscribedTo(obj, as_if_not_recursive=True): self.unSubscribeFrom(obj) break obj = obj.aq_parent decPublic('isSubscribedTo') def isSubscribedTo(self, obj, email=None, as_if_not_recursive=False): """Return whether ``email`` (or the current user if ``email`` is ``None``) is subscribed to ``obj``. If ``as_if_not_recursive`` is ``True``, this method acts as if the recursive mode was off. """ if not self.isExtraSubscriptionsEnabled(): raise DisabledFeature if email is None: ## Yes, 'email' is actually the id of the current user. email = getSecurityManager().getUser().getId() self._updateSubscriptionMapping(obj) path = self._getPath(obj) subscribers = self.getExtraSubscribersOf(path, as_if_not_recursive) return subscribers.has_key(email) decPrivate('getExtraSubscribersOf') def getExtraSubscribersOf(self, path, as_if_not_recursive=False): """Return users or email addresses which are subscribed to the given path. This method returns a mapping of users to tuples of notification methods, that is, each user can subscribe to be notified in more than one way. If ``as_if_not_recursive`` is ``True``, this method acts as if the recursive mode was off. """ subscribers = self._subscriptions.get(path, {}).copy() if self.isExtraSubscriptionsRecursive() and \ not as_if_not_recursive: if path[-1] == '/': path = path[:-1] i = path.rfind('/') if i != -1: parent = path[:i + 1] subscribers.update(self.getExtraSubscribersOf(parent)) return subscribers
def testTheWorld(self): # Test constructors u = PersistentMapping() u0 = PersistentMapping(l0) u1 = PersistentMapping(l1) u2 = PersistentMapping(l2) uu = PersistentMapping(u) uu0 = PersistentMapping(u0) uu1 = PersistentMapping(u1) uu2 = PersistentMapping(u2) class OtherMapping: def __init__(self, initmapping): self.__data = initmapping def items(self): return self.__data.items() v0 = PersistentMapping(OtherMapping(u0)) vv = PersistentMapping([(0, 0), (1, 1)]) # Test __repr__ eq = self.assertEqual eq(str(u0), str(l0), "str(u0) == str(l0)") eq(repr(u1), repr(l1), "repr(u1) == repr(l1)") eq( ` u2 `, ` l2 `, "`u2` == `l2`") # Test __cmp__ and __len__ def mycmp(a, b): r = cmp(a, b) if r < 0: return -1 if r > 0: return 1 return r all = [l0, l1, l2, u, u0, u1, u2, uu, uu0, uu1, uu2] for a in all: for b in all: eq(mycmp(a, b), mycmp(len(a), len(b)), "mycmp(a, b) == mycmp(len(a), len(b))") # Test __getitem__ for i in range(len(u2)): eq(u2[i], i, "u2[i] == i") # Test get for i in range(len(u2)): eq(u2.get(i), i, "u2.get(i) == i") eq(u2.get(i, 5), i, "u2.get(i, 5) == i") for i in min(u2) - 1, max(u2) + 1: eq(u2.get(i), None, "u2.get(i) == None") eq(u2.get(i, 5), 5, "u2.get(i, 5) == 5") # Test __setitem__ uu2[0] = 0 uu2[1] = 100 uu2[2] = 200 # Test __delitem__ del uu2[1] del uu2[0] try: del uu2[0] except KeyError: pass else: raise TestFailed("uu2[0] shouldn't be deletable") # Test __contains__ for i in u2: self.failUnless(i in u2, "i in u2") for i in min(u2) - 1, max(u2) + 1: self.failUnless(i not in u2, "i not in u2") # Test update l = {"a": "b"} u = PersistentMapping(l) u.update(u2) for i in u: self.failUnless(i in l or i in u2, "i in l or i in u2") for i in l: self.failUnless(i in u, "i in u") for i in u2: self.failUnless(i in u, "i in u") # Test setdefault x = u2.setdefault(0, 5) eq(x, 0, "u2.setdefault(0, 5) == 0") x = u2.setdefault(5, 5) eq(x, 5, "u2.setdefault(5, 5) == 5") self.failUnless(5 in u2, "5 in u2") # Test pop x = u2.pop(1) eq(x, 1, "u2.pop(1) == 1") self.failUnless(1 not in u2, "1 not in u2") try: u2.pop(1) except KeyError: pass else: raise TestFailed("1 should not be poppable from u2") x = u2.pop(1, 7) eq(x, 7, "u2.pop(1, 7) == 7") # Test popitem items = u2.items() key, value = u2.popitem() self.failUnless((key, value) in items, "key, value in items") self.failUnless(key not in u2, "key not in u2") # Test clear u2.clear() eq(u2, {}, "u2 == {}")
class NotificationTool(UniqueObject, SimpleItem, PropertyManager): """Main notification tool.""" id = ID title = TITLE meta_type = META_TYPE manage_options = (PropertyManager.manage_options + SimpleItem.manage_options) ## Extra subscriptions extra_subscriptions_enabled = False extra_subscriptions_recursive = True ## Debug mode debug_mode = False ## Ignore rules ignore_rules = DEFAULT_IGNORE_RULES ## Item creation item_creation_notification_enabled = False on_item_creation_users = [] on_item_creation_mail_template = [] ## Item modification item_modification_notification_enabled = False on_item_modification_users = [] on_item_modification_mail_template = [] ## Workflow transition wf_transition_notification_enabled = False on_wf_transition_users = [] on_wf_transition_mail_template = [] ## Member registration member_registration_notification_enabled = False on_member_registration_users = [] on_member_registration_mail_template = [] ## Member modification member_modification_notification_enabled = False on_member_modification_users = [] on_member_modification_mail_template = [] ## Discussion item creation discussion_item_creation_notification_enabled = False on_discussion_item_creation_users = [] on_discussion_item_creation_mail_template = [] _properties = ( { 'id': 'extra_subscriptions_enabled', 'label': 'Enable extra subscriptions', 'mode': 'w', 'type': 'boolean' }, { 'id': 'extra_subscriptions_recursive', 'label': 'Toggle recursive mode for extra subscriptions', 'mode': 'w', 'type': 'boolean' }, { 'id': 'debug_mode', 'label': 'Toggle debug mode', 'mode': 'w', 'type': 'boolean' }, { 'id': 'ignore_rules', 'label': 'Rules (ignore)', 'mode': 'w', 'type': 'lines' }, { 'id': 'item_creation_notification_enabled', 'label': 'Enable item creation notification', 'mode': 'w', 'type': 'boolean' }, { 'id': 'on_item_creation_users', 'label': 'Rules on item creation (users)', 'mode': 'w', 'type': 'lines' }, { 'id': 'on_item_creation_mail_template', 'label': 'Rules on item creation (mail template)', 'mode': 'w', 'type': 'lines' }, { 'id': 'item_modification_notification_enabled', 'label': 'Enable item modification notification', 'mode': 'w', 'type': 'boolean' }, { 'id': 'on_item_modification_users', 'label': 'Rules on item modification (users)', 'mode': 'w', 'type': 'lines' }, { 'id': 'on_item_modification_mail_template', 'label': 'Rules on item modification (mail template)', 'mode': 'w', 'type': 'lines' }, { 'id': 'wf_transition_notification_enabled', 'label': 'Enable workflow transition notification', 'mode': 'w', 'type': 'boolean' }, { 'id': 'on_wf_transition_users', 'label': 'Rules on workflow transition (users)', 'mode': 'w', 'type': 'lines' }, { 'id': 'on_wf_transition_mail_template', 'label': 'Rules on workflow transition (mail template)', 'mode': 'w', 'type': 'lines' }, { 'id': 'member_registration_notification_enabled', 'label': 'Enable member registration notification', 'mode': 'w', 'type': 'boolean' }, { 'id': 'on_member_registration_users', 'label': 'Rules on member registration (users)', 'mode': 'w', 'type': 'lines' }, { 'id': 'on_member_registration_mail_template', 'label': 'Rules on member registration (mail template)', 'mode': 'w', 'type': 'lines' }, { 'id': 'member_modification_notification_enabled', 'label': 'Enable member modification notification', 'mode': 'w', 'type': 'boolean' }, { 'id': 'on_member_modification_users', 'label': 'Rules on member modification (users)', 'mode': 'w', 'type': 'lines' }, { 'id': 'on_member_modification_mail_template', 'label': 'Rules on member modification (mail template)', 'mode': 'w', 'type': 'lines' }, { 'id': 'discussion_item_creation_notification_enabled', 'label': 'Enable discussion item creation notification', 'mode': 'w', 'type': 'boolean' }, { 'id': 'on_discussion_item_creation_users', 'label': 'Rules on discussion item creation (users)', 'mode': 'w', 'type': 'lines' }, { 'id': 'on_discussion_item_creation_mail_template', 'label': 'Rules on discussion item creation (mail template)', 'mode': 'w', 'type': 'lines' }, ) security = ClassSecurityInfo() decPrivate = security.declarePrivate decProtected = security.declareProtected decPublic = security.declarePublic def __init__(self, *args, **kwargs): self._uid_to_path = PersistentMapping() self._subscriptions = PersistentMapping() ################################################################# ## Notification handlers ######################## decPrivate('onItemCreation') def onItemCreation(self, obj): """Handler called when an item is created. It returns the number of mails which have been sent. **Warning:** this handler is not called when a discussion item is added. In this case, ``onDiscussionItemCreation()`` is called instead. """ if not self.getProperty('item_creation_notification_enabled'): return 0 if self.ignoreNotification(obj): return 0 extra_bindings = getBasicBindings(obj) return self._handlerHelper(obj, 'item_creation', extra_bindings, extra_bindings, extra_bindings) decPrivate('onItemModification') def onItemModification(self, obj): """Handler called when an item is modified. It returns the number of mails which have been sent. """ if not self.getProperty('item_modification_notification_enabled'): return 0 if self.ignoreNotification(obj): return 0 extra_bindings = getBasicBindings(obj) extra_bindings.update({ 'current': obj, 'previous': getPreviousVersion(obj) }) return self._handlerHelper(obj, 'item_modification', extra_bindings, extra_bindings, extra_bindings) decPrivate('onWorkflowTransition') def onWorkflowTransition(self, obj, action): """Handler called when a workflow transition is triggered. It returns the number of mails which have been sent. """ if not self.getProperty('wf_transition_notification_enabled'): return 0 if self.ignoreNotification(obj): return 0 wtool = getToolByName(self, 'portal_workflow') comments = wtool.getInfoFor(obj, 'comments') extra_bindings = getBasicBindings(obj) extra_bindings.update({ 'transition': action, 'comments': comments, 'previous_state': getPreviousWorkflowState(obj) }) current_state_display = extra_bindings['current_state'] previous_state_display = extra_bindings['previous_state'] try: wf_def = wtool.getWorkflowsFor(obj) if len(wf_def) > 0: curr_wf = wf_def[0] wf_states = curr_wf.states current_state_display = wf_states[ extra_bindings['current_state']].title if extra_bindings['previous_state'] <> None: previous_state_display = wf_states[ extra_bindings['previous_state']].title else: previous_state_display = "" except AttributeError: pass extra_bindings.update({ 'current_state_title': current_state_display, 'previous_state_title': previous_state_display, }) return self._handlerHelper(obj, 'wf_transition', extra_bindings, extra_bindings, extra_bindings) decPrivate('onMemberRegistration') def onMemberRegistration(self, member, properties): """Handler called when a new portal member has been registered. It returns the number of mails which have been sent. """ if not self.getProperty('member_registration_notification_enabled'): return 0 if self.ignoreNotification(member): return 0 if properties is None: properties = {} ## FIXME: How could it be? (Damien) current_user = getSecurityManager().getUser() extra_bindings = getBasicBindings(member) extra_bindings.update({ 'current_user': current_user, 'member': member, 'properties': properties, 'event': 'registration' }) return self._handlerHelper(member, 'member_registration', extra_bindings, extra_bindings, extra_bindings) decPrivate('onMemberModification') def onMemberModification(self, member): """Handler called when a member changes his/her properties. It returns the number of mails which have been sent. """ ## FIXME: this should go away when we rely on the appropriate ## event. ## This method can also be called when the member is ## registered. We have to check that. stack = inspect.stack() ## 1st item is ourself ## 2nd item is 'CMFCore.MemberDataTool.notifyMemberModified()' ## 3rd item is 'CMFCore.MemberDataTool.setMemberProperties()' ## 4th item is what we want to check: it is either 'addMember' ## or 'setProperties()' caller = stack[3][3] if caller != 'setProperties': return 0 if not self.getProperty('member_modification_notification_enabled'): return 0 if self.ignoreNotification(member): return 0 ## FIXME: what is the purpose of the following lines? (Damien) memberdata = getToolByName(self, 'portal_memberdata') properties = {} for key, value in memberdata.propertyItems(): properties[key] = value current_user = getSecurityManager().getUser() extra_bindings = getBasicBindings(None) extra_bindings.update({ 'current_user': current_user, 'member': member, 'properties': properties, 'event': 'modification' }) return self._handlerHelper(member, 'member_modification', extra_bindings, extra_bindings, extra_bindings) decPrivate('onDiscussionItemCreation') def onDiscussionItemCreation(self, discussion_item): """Handler called when a discussion item is created. It returns the number of mails which have been sent. """ if not self.getProperty( 'discussion_item_creation_notification_enabled'): return 0 if self.ignoreNotification(discussion_item): return 0 ## We add two bindings to disambiguate the meaning of 'here' ## in the mail template and the rules: 'discussion_item' and ## 'discussed_item'. discussed_item = discussion_item while discussed_item.meta_type == discussion_item.meta_type: discussed_item = discussed_item.aq_inner.aq_parent.aq_parent extra_bindings = getBasicBindings(discussed_item) extra_bindings.update({ 'discussion_item': discussion_item, 'discussed_item': discussed_item }) return self._handlerHelper(discussion_item, 'discussion_item_creation', extra_bindings, extra_bindings, extra_bindings) def _handlerHelper(self, obj, what, get_users_extra_bindings, mail_template_extra_bindings, mail_template_options): """An helper method for ``on*()`` handlers. It returns the number of mails which have been sent. """ #import pdb;pdb.set_trace() self._updateSubscriptionMapping(obj) users_by_label = self.getUsersToNotify(obj, what, get_users_extra_bindings) if self.isExtraSubscriptionsEnabled(): users = users_by_label.get('', []) users.extend(self.getExtraSubscribersOf(self._getPath(obj))) users_by_label[''] = users n_sent = 0 for label, users in users_by_label.items(): users = self.removeUnAuthorizedSubscribers(users, obj) #remove user who has initiated the action. author = mail_template_options['author'] users = removeActionInitiatorFromUsers(users, author) addresses = self.getEmailAddresses(users) if not addresses: LOG.warning("No addresses for label '%s' for '%s' "\ "notification of '%s'", label, what, obj.absolute_url(1)) continue mail_template_extra_bindings['label'] = label template = self.getMailTemplate(obj, what, mail_template_extra_bindings) if template is None: LOG.warning("No mail template for label '%s' for "\ "'%s' notification of '%s'", label, what, obj.absolute_url(1)) continue try: message = template(**mail_template_options) except ConflictError: raise except: LOG.error("Cannot evaluate mail template '%s' on '%s' "\ "for '%s' for label '%s'", template.absolute_url(1), obj.absolute_url(1), what, label, exc_info=True) continue n_sent += self.sendNotification(addresses, message) return n_sent ################################################################# ################################################################# ## Utility methods ############################### decPrivate('ignoreNotification') def ignoreNotification(self, obj): """Return whether notification have been set to be ignored for ``obj``. """ ec = getExpressionContext(obj) users = [] for match_expr in self.getProperty('ignore_rules', ()): try: if self._match(match_expr, ec): return True except ConflictError: raise except: LOG.error("Error in 'ignore_rules' rule "\ "('%s') for '%s'", match_expr, obj.absolute_url(1), exc_info=True) return False decPrivate('getUsersToNotify') def getUsersToNotify(self, obj, what, ec_bindings=None): """Return a mapping of list of users to notify by label for the ``what`` of ``obj``, ``what`` being one of the implemented notification (*item_modification*, *wf_transition*, etc.). ``ec_bindings`` is a mapping which is injected into the expression context of the expression of the rules. """ rules = self.getProperty('on_%s_users' % what, None) if rules is None: raise NotImplementedError, \ "Notification on '%s' is not implemented." % what ec = getExpressionContext(obj, ec_bindings) users_by_label = {} ignore_next_rules = False for rule in rules: try: match_expr, users_expr = rule.split(RULE_DELIMITER, 1) if RULE_DELIMITER in users_expr: users_expr, label = users_expr.split(RULE_DELIMITER) else: label = '' except ValueError: LOG.error("'%s' is not a valid rule "\ "('on_%s_users' on '%s')", rule, what, obj.absolute_url(1)) continue match_expr = match_expr.strip() users_expr = users_expr.strip() label = label.strip() users = users_by_label.get(label, []) try: if not self._match(match_expr, ec): continue except ConflictError: raise except: LOG.error("Error in 'on_%s_users' rule "\ "('%s') for '%s'", what, match_expr, obj.absolute_url(1), exc_info=True) continue if users_expr == '*': users.extend(self.getAllUsers()) ignore_next_rules = True else: try: users.extend(Expression(users_expr)(ec)) except ConflictError: raise except: LOG.error("Error in 'on_%s_users' rule "\ "('%s') for '%s'", what, users_expr, obj.absolute_url(1), exc_info=True) users_by_label[label] = users if ignore_next_rules: break return users_by_label decPrivate('getMailTemplate') def getMailTemplate(self, obj, what, ec_bindings=None): """Return the template to notify for the ``what`` of an object ``obj``, ``what`` being one of the implemented notification ("*item_modification*", "*wf_transition*", etc.), or ``None`` if none could be found. ``ec_bindings`` is a mapping which is injected into the expression context of the expression of the rules. """ rules = self.getProperty('on_%s_mail_template' % what, None) if rules is None: raise NotImplementedError, \ 'Notification on "%s" is not implemented.' ec = getExpressionContext(obj, ec_bindings) template = None for rule in rules: try: match_expr, template_expr = rule.split(RULE_DELIMITER) match_expr, template_expr = match_expr.strip( ), template_expr.strip() except ValueError: LOG.error("'%s' is not a valid rule "\ "('on_%s_mail_template' on '%s')", rule, what, obj.absolute_url(1)) continue match_expr = match_expr.strip() template_expr = template_expr.strip() try: if not self._match(match_expr, ec): continue except ConflictError: raise except: LOG.error("Error in 'on_%s_mail_template' rule "\ "('%s') for '%s'", what, match_expr, obj.absolute_url(1), exc_info=True) continue try: template = Expression(template_expr)(ec) except ConflictError: raise except: LOG.error("Error in 'on_%s_mail_template' rule "\ "('%s') for '%s'", what, template_expr, obj.absolute_url(1), exc_info=True) continue if type(template) == StringType: template = obj.restrictedTraverse(template, None) if template is not None: break return template decPrivate('getAllUsers') def getAllUsers(self): """Return a list of all user ids of the portal. **Warning:** this method may be costly if you rely on an external (non ZODB) user source. Use it at your own risk. """ mtool = getToolByName(self, 'portal_membership') return mtool.listMemberIds() decPrivate('removeUnAuthorizedSubscribers') def removeUnAuthorizedSubscribers(self, subscribers, obj): """Return users from ``subscribers`` who are authorized to view ``obj``. """ portal = getToolByName(self, 'portal_url').getPortalObject() mtool = getToolByName(self, 'portal_membership') filtered_subscribers = [] for subscriber in subscribers: if self._anonymousShouldBeNotified(obj): filtered_subscribers.append(subscriber) else: ## We use '_huntUser()' and not 'mtool.getMemberById()' ## because the latter would provide a wrapped user ## object, with a specific context where the user is ## not allowed to view 'obj'. member = mtool._huntUser(str(subscriber), portal) if member is not None: if member.has_permission('View', obj): filtered_subscribers.append(subscriber) return filtered_subscribers decPrivate('getEmailAddresses') def getEmailAddresses(self, users): """Return email addresses of ``users``. For each value in ``users``: - if the value is not an e-mail, suppose it is an user id and try to get the ``email`` property of this user; - remove duplicates; - remove bogus e-mail addresses. """ mtool = getToolByName(self, 'portal_membership') addresses = {} for user in users: member = mtool.getMemberById(str(user)) if member is not None: user = member.getProperty('email', '') if user is None: continue if EMAIL_REGEXP.match(user): addresses[user] = 1 return addresses.keys() decPrivate('sendNotification') def sendNotification(self, addresses, message): """Send ``message`` to all ``addresses``.""" mailhosts = self.superValues(MAIL_HOST_META_TYPES) if not mailhosts: raise MailHostNotFound from Products.MaildropHost import MaildropHost bfound = False for mh in mailhosts: if isinstance(mh, MaildropHost): mailhost = mh bfound = True if bfound == True: break if bfound == False: mailhost = mailhosts[0] ptool = getToolByName(self, 'portal_properties').site_properties encoding = ptool.getProperty('default_charset', 'utf-8') message = encodeMailHeaders(message, encoding) if self.getProperty('debug_mode'): LOG.info('About to send this message to %s: \n%s', addresses, message) n_messages_sent = 0 for address in addresses: this_message = ('To: %s\n' % address) + message this_message = this_message.encode(encoding) try: mailhost.send(this_message) n_messages_sent += 1 except ConflictError: raise except: LOG.error('Error while sending '\ 'notification: \n%s' % this_message, exc_info=True) return n_messages_sent def _match(self, expr, ec): """Return ``True`` if ``expr`` returns something which can be evaluated to ``True`` in the expression context (``ec``) or if ``expr`` is "*". """ if expr == '*': return True expr = Expression(expr) return bool(expr(ec)) def _getPath(self, obj): """Return path of ``obj``. A slash (``/``) is appended to the path if the object is folderish. The returned path is relative to the portal object. """ utool = getToolByName(self, 'portal_url') path = utool.getRelativeContentURL(obj) path = '/' + path if not getattr(obj.aq_base, 'isPrincipiaFolderish', False): return path if path[-1] != '/': path += '/' return path def _getParents(self, path): """Get the parents of the item corresponding to ``path`` and return their respective path. Parents are returned from ``path`` to the portal root object. """ if path == '/': return [] if path[-1] == '/': path = path[:-1] parent = path[:path.rfind('/') + 1] parents = [parent] parents.extend(self._getParents(parent)) return tuple(parents) def _getUID(self, obj): """Return UID of the object.""" portal_uidhandler = getToolByName(self, 'portal_uidhandler') uid = portal_uidhandler.queryUid(obj, None) if uid is None: ## Not yet registered uid = portal_uidhandler.register(obj) return uid def _anonymousShouldBeNotified(self, obj): """Return whether anonymous users should be notified, i.e. whether anonymous users can view ``obj``. """ return 'Anonymous' in rolesForPermissionOn('View', obj) ################################################################# ################################################################# ## Extra subscriptions settings ############################### decProtected('View', 'isExtraSubscriptionsEnabled') def isExtraSubscriptionsEnabled(self): """Return whether extra subscriptions are enabled.""" return self.getProperty('extra_subscriptions_enabled') decProtected('View', 'isExtraSubscriptionsRecursive') def isExtraSubscriptionsRecursive(self): """Return whether extra subscriptions are recursive. Note that this method does not check whether extra subscriptions are enabled or not. """ return self.getProperty('extra_subscriptions_recursive') ################################################################# ################################################################# ## Extra subscriptions logic ############################ def _updateSubscriptionMapping(self, obj): """Update subscription mapping.""" uid = self._getUID(obj) path = self._getPath(obj) known_path = self._uid_to_path.get(uid) if known_path != path: self._uid_to_path[uid] = path if known_path is not None: ## We have old informations for this object for key, value in self._subscriptions.items(): if key.startswith(known_path): new_key = path + key[len(known_path):] self._subscriptions[new_key] = value del self._subscriptions[key] decPublic('currentUserHasSubscribePermission') def currentUserHasSubscribePermissionOn(self, obj): """Return whether the current user is allowed to subscribe to or unsubscribe from ``obj``. """ if not IATContentType.providedBy(obj) and not \ IPloneSiteRoot.providedBy(obj): return False mtool = getToolByName(self, 'portal_membership') return mtool.checkPermission(SUBSCRIBE_PERMISSION, obj) decPublic('subscribeTo') def subscribeTo(self, obj, email=None): """Subscribe ``email`` (or the current user if ``email`` is None) to ``obj``. """ if not self.isExtraSubscriptionsEnabled(): raise DisabledFeature if not self.currentUserHasSubscribePermissionOn(obj): raise Unauthorized elif email is not None: if not EMAIL_REGEXP.match(email): raise InvalidEmailAddress ## FIXME: an anonymous user would like to subscribe ## his/her address. This has not yet been implemented, so ## we raise an exception. raise NotImplementedError else: self._updateSubscriptionMapping(obj) path = self._getPath(obj) subscribers = self._subscriptions.get(path, {}) user = getSecurityManager().getUser().getId() subscribers[user] = 1 self._subscriptions[path] = subscribers decPublic('unSubscribeFrom') def unSubscribeFrom(self, obj, email=None): """Unsubscribe ``email`` (or the current user if ``email`` is ``None``) from ``obj``. """ if not self.isExtraSubscriptionsEnabled(): raise DisabledFeature if not self.currentUserHasSubscribePermissionOn(obj): raise Unauthorized elif email is not None: if not EMAIL_REGEXP.match(email): raise InvalidEmailAddress ## FIXME: an anonymous user would like to unsubscribe ## his/her address. This has not yet been implemented, so ## we raise an exception. raise NotImplementedError else: self._updateSubscriptionMapping(obj) path = self._getPath(obj) subscribers = self._subscriptions.get(path, {}) user = getSecurityManager().getUser().getId() try: del subscribers[user] self._subscriptions[path] = subscribers except KeyError: pass ## User was not subscribed. decPublic('unSubscribeFromObjectAbove') def unSubscribeFromObjectAbove(self, obj, email=None): """Find folderish items above ``obj`` and unsubscribe ``email`` (or the current user if ``email`` is ``None``) from the first one (s)he is subscribed to. If ``user`` is subscribed to ``obj``, this method is equivalent to ``unSubscribeFrom(obj, user)``. """ if not self.isExtraSubscriptionsEnabled(): raise DisabledFeature if not self.currentUserHasSubscribePermissionOn(obj): raise Unauthorized elif email is not None: if not EMAIL_REGEXP.match(email): raise InvalidEmailAddress ## FIXME: an anonymous user would like to unsubscribe ## his/her address. This has not yet been implemented, so ## we raise an exception. raise NotImplementedError else: self._updateSubscriptionMapping(obj) utool = getToolByName(obj, 'portal_url') portal = utool.getPortalObject() portal_container = portal.aq_inner.aq_parent while obj != portal_container: if self.isSubscribedTo(obj, as_if_not_recursive=True): self.unSubscribeFrom(obj) break obj = obj.aq_parent decPublic('isSubscribedTo') def isSubscribedTo(self, obj, email=None, as_if_not_recursive=False): """Return whether ``email`` (or the current user if ``email`` is ``None``) is subscribed to ``obj``. If ``as_if_not_recursive`` is ``True``, this method acts as if the recursive mode was off. """ if not self.isExtraSubscriptionsEnabled(): raise DisabledFeature if email is None: ## Yes, 'email' is actually the id of the current user. email = getSecurityManager().getUser().getId() self._updateSubscriptionMapping(obj) path = self._getPath(obj) subscribers = self.getExtraSubscribersOf(path, as_if_not_recursive) return subscribers.has_key(email) decPrivate('getExtraSubscribersOf') def getExtraSubscribersOf(self, path, as_if_not_recursive=False): """Return users or email addresses which are subscribed to the given path. This method returns a mapping whose keys are the users or email addresses. If ``as_if_not_recursive`` is ``True``, this method acts as if the recursive mode was off. """ subscribers = self._subscriptions.get(path, {}).copy() if path.endswith('/'): j = path.rfind('/') if j != -1: newpath = path[:j] subscribers1 = self._subscriptions.get(newpath, {}).copy() subscribers.update(subscribers1) if self.isExtraSubscriptionsRecursive() and \ not as_if_not_recursive: if path[-1] == '/': path = path[:-1] i = path.rfind('/') if i != -1: parent = path[:i + 1] subscribers.update(self.getExtraSubscribersOf(parent)) return subscribers
class DecaObject(Persistent): """A generic DECA object""" def __init__(self, id=None): Persistent.__init__(self) if id is not None: self.ID = id else: self.ID = uuid.uuid4() self.TemplateName = "" self.Attributes = PersistentMapping() self.TitleAttr = None self.Graphics = None self.IsReflection = False self.Tag = None def copy(self, newID): obj = DecaObject(newID) obj.TemplateName = self.TemplateName obj.TitleAttr = self.TitleAttr for k, v in self.Attributes.items(): obj.Attributes[k] = v obj.Graphics = self.Graphics obj.IsReflection = self.IsReflection obj.Tag = self.Tag return obj def GetTitle(self): """Returns the title of the object. If the Title Attribute defined, it's value will be returned. Else if Tag defined, it's value will be returned. Else the object's ID will be returned. """ if self.TitleAttr is not None: return self.Attributes[self.TitleAttr] if self.Tag is not None and str(self.Tag) != "": return str(self.Tag) return str(self.ID) def SetAttr(self, name, value, location=LOCAL): """Sets the attribute value in desired location. **Note:** if the object isn't the reflect, attribute will be stored locally""" if location == LOCAL: self.Attributes[name] = value # todo: set attribute to the base object if we are reflection def GetAttr(self, name, default=None, priority=LOCAL): """Gets the attribute value. For the reflects the priority may points that the base value must be given. If value absent in desired location other locations will be scanned. Finnaly, the default value will be returned""" result = default if priority == LOCAL: result = self.Attributes.get(name, default) else: # todo: read base object's property if we are reflection pass return result def GetShape(self): # todo: get shape description. Is it necessary? return def GetPropList(self, holder=None): self.propsGrid = holder props = OrderedDict( [ ("Identity", self.ID), ("Template", self.TemplateName), ("Title Attribute", self.TitleAttr), ("Is Reflection", self.IsReflection), ] ) attrs = OrderedDict([]) for k, v in self.Attributes.items(): attrs.update([(k, v)]) result = OrderedDict([(_("Object's properties"), props), (_("Local object's attributes"), attrs)]) return result def ReflectTo(self, dstLayer): if not isinstance(dstLayer, Deca.Layer): dstLayer = Deca.world.GetLayer(str(dstLayer)) if not self.ID in dstLayer.storage.objects.keys(): nwo = self.copy(self.ID) nwo.IsReflection = True dstLayer.AddObject(nwo) return nwo return None def GetEngines(self): def genList(base, dirlist): res = [] for ent in dirlist: if os.path.isdir(os.path.join(base, ent)): # process subdir nb = os.path.join(base, ent) res.append((ent, genList(nb, os.listdir(nb)))) else: ft = os.path.splitext(ent) if ft[1].lower() == ".py" and not ft[0].startswith("_"): res.append(ft[0]) return res # main function pl = [] if self.TemplateName and self.TemplateName != "": epath = os.path.join(Deca.world.EnginesPath, self.TemplateName) if os.path.isdir(epath): pl = genList(epath, os.listdir(epath)) # if Object's engines # if template given return pl def ExecuteEngine(self, name, layer, shape=None, dict=None): item = os.path.join(Deca.world.EnginesPath, self.TemplateName, name + ".py") fl = None if not dict: dict = globals() try: fl = open(item, "r") dict["ActiveDecaLayer"] = layer dict["ActiveShape"] = shape dict["ActiveObject"] = self.ID exec fl in dict finally: if fl: fl.close()
class HasEmail: """Mixin class proving email address(es) and related functions""" def __init__(self): self.__items = [] self.__unconfirmed = PersistentMapping() self.primary_email = None def is_valid_email(cls, email): """Class method returns True if email is valid, or False if it should be rejected. >>> HasEmail.is_valid_email('*****@*****.**') True >>> HasEmail.is_valid_email('*****@*****.**') True >>> HasEmail.is_valid_email('*****@*****.**') True >>> HasEmail.is_valid_email('xyz') False >>> HasEmail.is_valid_email('abc@xyz@foo') False """ global _blocked_domains if email.find('@') == -1: return False if email.count('@') != 1: return False (username, host) = email.split('@') if host in _blocked_domains: return False return True is_valid_email = classmethod(is_valid_email) def add_email(self, email): """Add email to the list. Adds primary if none set.""" email = email.lower() if email not in self.__items: self.__items.append(email) self._p_changed = 1 if self.primary_email is None: self.primary_email = email def add_unconfirmed_email(self, email): """Add new e-mail that has not yet been confirmed. Call confirm_email to move into active list of e-mails. Returns confirmation code that must be given to confirm_email to confirm. """ email = email.lower() if not self.__unconfirmed.has_key(email): self.__unconfirmed[email] = _password_generator.generate(seed=email) return self.__unconfirmed[email] def remove_unconfirmed_email(self, email): email = email.lower() if self.__unconfirmed.has_key(email): del self.__unconfirmed[email] def confirm_email(self, code): """Confirm email with the given code, or return False if invalid code.""" for email, conf_code in self.__unconfirmed.items(): if conf_code == code: self.add_email(email) del self.__unconfirmed[email] self.notify_email_confirmed(email) return email return None def remove_email(self, email): """Remove an e-mail address from the list. Raises KeyError if only one e-mail address left""" email = email.lower() if self.__unconfirmed.has_key(email): return self.remove_unconfirmed_email(email) emails = self.email_list() if len(emails) > 1: self.__items.remove(email) self._p_changed = 1 if email == self.get_primary_email(): self.set_primary_email(self.email_list()[0]) else: raise KeyError def remove_all_emails(self): self.__items = [] self.primary_email = None def has_email(self, email): email = email.lower() return email in self.__items def email_list(self): return self.__items def unconfirmed_email_list(self): return self.__unconfirmed.keys() def set_primary_email(self, email): email = email.lower() if self.has_email(email): self.primary_email = email else: raise ValueError("I don't know email <%s>" % email) def get_primary_email(self): return self.primary_email def notify_email_confirmed(self, email): """Notice that email was just confirmed.""" pass def _consistency_check(self): if self.primary_email is not None: if self.primary_email not in self.__items: raise KeyError, "primary_email not in email list" typecheck_seq(self.__items, str, allow_none=1)
class DecaObject(Persistent): """A generic DECA object""" def __init__(self, id = None): Persistent.__init__(self) if id is not None: self.ID = id else: self.ID = uuid.uuid4() self.TemplateName = "" self.Attributes = PersistentMapping() self.TitleAttr = None self.Graphics = None self.IsReflection = False self.Tag = None def copy(self, newID): obj = DecaObject(newID) obj.TemplateName = self.TemplateName obj.TitleAttr = self.TitleAttr for k,v in self.Attributes.items() : obj.Attributes[k] = v obj.Graphics = self.Graphics obj.IsReflection = self.IsReflection obj.Tag = self.Tag return obj def GetTitle(self): """Returns the title of the object. If the Title Attribute defined, it's value will be returned. Else if Tag defined, it's value will be returned. Else the object's ID will be returned. """ if self.TitleAttr is not None: return self.Attributes[self.TitleAttr] if self.Tag is not None and str(self.Tag) != '': return str(self.Tag) return str(self.ID) def SetAttr(self, name, value, location=LOCAL): """Sets the attribute value in desired location. **Note:** if the object isn't the reflect, attribute will be stored locally""" if location == LOCAL : self.Attributes[name] = value # todo: set attribute to the base object if we are reflection def GetAttr(self, name, default=None, priority=LOCAL): """Gets the attribute value. For the reflects the priority may points that the base value must be given. If value absent in desired location other locations will be scanned. Finnaly, the default value will be returned""" result = default if priority == LOCAL : result = self.Attributes.get(name, default) else: # todo: read base object's property if we are reflection pass return result def GetShape(self): # todo: get shape description. Is it necessary? return def GetPropList(self, holder = None) : self.propsGrid = holder props = OrderedDict([ ('Identity', self.ID), ('Template', self.TemplateName), ('Title Attribute', self.TitleAttr), ('Is Reflection', self.IsReflection), ]) attrs = OrderedDict([]) for k,v in self.Attributes.items(): attrs.update([(k, v)]) result = OrderedDict([ (_("Object's properties"), props), (_("Local object's attributes"), attrs) ]) return result def ReflectTo(self, dstLayer): if not isinstance(dstLayer, Deca.Layer): dstLayer = Deca.world.GetLayer(str(dstLayer)) if not self.ID in dstLayer.storage.objects.keys(): nwo = self.copy(self.ID) nwo.IsReflection = True dstLayer.AddObject(nwo) return nwo return None def GetEngines(self): def genList(base, dirlist): res = [] for ent in dirlist: if os.path.isdir(os.path.join(base, ent)): # process subdir nb = os.path.join(base, ent) res.append( (ent, genList(nb, os.listdir(nb))) ) else: ft = os.path.splitext(ent) if ft[1].lower() == '.py' and not ft[0].startswith('_'): res.append(ft[0]) return res # main function pl = [] if self.TemplateName and self.TemplateName != '' : epath = os.path.join(Deca.world.EnginesPath, self.TemplateName) if os.path.isdir(epath): pl = genList(epath, os.listdir(epath)) # if Object's engines # if template given return pl def ExecuteEngine(self, name, layer, shape = None, dict = None): item = os.path.join(Deca.world.EnginesPath, self.TemplateName, name + '.py') fl = None if not dict: dict = globals() try: fl = open(item, 'r') dict['ActiveDecaLayer'] = layer dict['ActiveShape'] = shape dict['ActiveObject'] = self.ID exec fl in dict finally: if fl : fl.close()
class BaseQuestion(BaseContent): """Base class for survey questions""" immediate_view = "base_edit" global_allow = 0 filter_content_types = 1 allowed_content_types = () include_default_actions = 1 _at_rename_after_creation = True def __init__(self, oid, **kwargs): self.reset() BaseContent.__init__(self, oid, **kwargs) security = ClassSecurityInfo() security.declareProtected(CMFCorePermissions.View, 'getAbstract') def getAbstract(self, **kw): return self.Description() security.declareProtected(CMFCorePermissions.ModifyPortalContent, 'setAbstract') def setAbstract(self, val, **kw): self.setDescription(val) security.declareProtected(CMFCorePermissions.ModifyPortalContent, 'reset') def reset(self): """Remove answers for all users.""" if USE_BTREE: self.answers = OOBTree() else: self.answers = PersistentMapping() security.declareProtected(CMFCorePermissions.ModifyPortalContent, 'resetForUser') def resetForUser(self, userid): """Remove answer for a single user""" if self.answers.has_key(userid): del self.answers[userid] security.declareProtected(CMFCorePermissions.View, 'addAnswer') def addAnswer(self, value, comments=""): """Add an answer and optional comments for a user. This method protects _addAnswer from anonymous users specifying a userid when they vote, and thus apparently voting as another user of their choice. """ # Get hold of the parent survey survey = None ob = self while survey is None: ob = ob.aq_parent if ob.meta_type == 'Survey': survey = ob elif getattr(ob, '_isPortalRoot', False): raise Exception("Could not find a parent Survey.") portal_membership = getToolByName(self, 'portal_membership') if portal_membership.isAnonymousUser() and not survey.getAllowAnonymous(): raise Unauthorized, ("This survey is not available to anonymous users.") # Use the survey to get hold of the appropriate userid userid = survey.getSurveyId() # Call the real method for storing the answer for this user. return self._addAnswer(userid, value, comments) def _addAnswer(self, userid, value, comments=""): """Add an answer and optional comments for a user.""" # We don't let users over-write answers that they've already made. # Their first answer must be explicitly 'reset' before another # answer can be supplied. # XXX this causes problem when survey fails validation # will also cause problem with save function ## if self.answers.has_key(userid): ## # XXX Should this get raised? If so, a more appropriate ## # exception is probably in order. ## msg = "User '%s' has already answered this question. Reset the original response to supply a new answer." ## raise Exception(msg % userid) ## else: self.answers[userid] = PersistentMapping(value=value, comments=comments) if not isinstance(self.answers, (PersistentMapping, OOBTree)): # It must be a standard dictionary from an old install, so # we need to inform the ZODB about the change manually. self.answers._p_changed = 1 security.declareProtected(CMFCorePermissions.View, 'getAnswerFor') def getAnswerFor(self, userid): """Get a specific user's answer""" answer = self.answers.get(userid, {}).get('value', None) if self.getInputType() in ['multipleSelect', 'checkbox']: if type(answer) == 'NoneType': return [] return answer security.declareProtected(CMFCorePermissions.View, 'getCommentsFor') def getCommentsFor(self, userid): """Get a specific user's comments""" return self.answers.get(userid, {}).get('comments', None) security.declareProtected(CMFCorePermissions.View, 'getComments') def getComments(self): """Return a userid, comments mapping""" mlist = [] for k, v in self.answers.items(): mapping = {} mapping['userid'] = k mapping['comments'] = v.get('comments', '') mlist.append(mapping) return mlist
class NotificationTool(UniqueObject, SimpleItem, PropertyManager): """Main notification tool.""" id = ID title = TITLE meta_type = META_TYPE manage_options = (PropertyManager.manage_options + SimpleItem.manage_options) ## Extra subscriptions extra_subscriptions_enabled = False ## FIXME: rename this to something like ## 'restrict_subscriptions_to_authenticated_users' extra_subscriptions_for_authenticated_only = True extra_subscriptions_recursive = True ## Debug settings debug_log_addresses = False ## Ignore rules ignore_rules = DEFAULT_IGNORE_RULES ## Item creation item_creation_notification_enabled = True on_item_creation_users = [] on_item_creation_mail_template = [] ## Item modification item_modification_notification_enabled = True on_item_modification_users = [] on_item_modification_mail_template = [] ## Item deletion item_deletion_notification_enabled = True on_item_deletion_users = [] on_item_deletion_mail_template = [] ## Workflow transition wf_transition_notification_enabled = True on_wf_transition_users = [] on_wf_transition_mail_template = [] ## Member registration member_registration_notification_enabled = True on_member_registration_users = [] on_member_registration_mail_template = [] ## Member modification member_modification_notification_enabled = True on_member_modification_users = [] on_member_modification_mail_template = [] ## Discussion item creation discussion_item_creation_notification_enabled = True on_discussion_item_creation_users = [] on_discussion_item_creation_mail_template = [] _properties = ({'id': 'extra_subscriptions_enabled', 'label': 'Enable extra subscriptions', 'mode': 'w', 'type': 'boolean'}, {'id': 'extra_subscriptions_for_authenticated_only', 'label': 'Enable extra subscriptions only for authenticated users', 'mode': 'w', 'type': 'boolean'}, {'id': 'extra_subscriptions_recursive', 'label': 'Toggle recursive mode for extra subscriptions', 'mode': 'w', 'type': 'boolean'}, {'id': 'debug_log_addresses', 'label': 'Toggle debug mode: log addresses', 'mode': 'w', 'type': 'boolean'}, {'id': 'ignore_rules', 'label': 'Rules (ignore)', 'mode': 'w', 'type': 'lines'}, {'id': 'item_creation_notification_enabled', 'label': 'Enable item creation notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_item_creation_users', 'label': 'Rules on item creation (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_item_creation_mail_template', 'label': 'Rules on item creation (mail template)', 'mode': 'w', 'type': 'lines'}, {'id': 'item_modification_notification_enabled', 'label': 'Enable item modification notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_item_modification_users', 'label': 'Rules on item modification (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_item_modification_mail_template', 'label': 'Rules on item modification (mail template)', 'mode': 'w', 'type': 'lines'}, {'id': 'item_deletion_notification_enabled', 'label': 'Enable item deletion notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_item_deletion_users', 'label': 'Rules on item deletion (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_item_deletion_mail_template', 'label': 'Rules on item deletion (mail template)', 'mode': 'w', 'type': 'lines'}, {'id': 'wf_transition_notification_enabled', 'label': 'Enable workflow transition notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_wf_transition_users', 'label': 'Rules on workflow transition (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_wf_transition_mail_template', 'label': 'Rules on workflow transition (mail template)', 'mode': 'w', 'type': 'lines'}, {'id': 'member_registration_notification_enabled', 'label': 'Enable member registration notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_member_registration_users', 'label': 'Rules on member registration (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_member_registration_mail_template', 'label': 'Rules on member registration (mail template)', 'mode': 'w', 'type': 'lines'}, {'id': 'member_modification_notification_enabled', 'label': 'Enable member modification notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_member_modification_users', 'label': 'Rules on member modification (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_member_modification_mail_template', 'label': 'Rules on member modification (mail template)', 'mode': 'w', 'type': 'lines'}, {'id': 'discussion_item_creation_notification_enabled', 'label': 'Enable discussion item creation notification', 'mode': 'w', 'type': 'boolean'}, {'id': 'on_discussion_item_creation_users', 'label': 'Rules on discussion item creation (users)', 'mode': 'w', 'type': 'lines'}, {'id': 'on_discussion_item_creation_mail_template', 'label': 'Rules on discussion item creation (mail template)', 'mode': 'w', 'type': 'lines'}, ) security = ClassSecurityInfo() decPrivate = security.declarePrivate decProtected = security.declareProtected decPublic = security.declarePublic def __init__(self, *args, **kwargs): self._uid_to_path = PersistentMapping() self._subscriptions = PersistentMapping() ################################################################# ## Notification handlers ######################## decPrivate('onItemCreation') def onItemCreation(self, obj): """Handler called when an item is created. It returns the number of mails which have been sent. **Warning:** this handler is not called when a discussion item is added. In this case, ``onDiscussionItemCreation()`` is called instead. """ if not self.getProperty('item_creation_notification_enabled'): return 0 if self.ignoreNotification(obj): return 0 extra_bindings = getBasicBindings(obj) extra_bindings.update({'current': obj, 'previous': None}) return self._handlerHelper(obj, 'item_creation', extra_bindings, extra_bindings, extra_bindings) decPrivate('onItemModification') def onItemModification(self, obj): """Handler called when an item is modified. It returns the number of mails which have been sent. """ if not self.getProperty('item_modification_notification_enabled'): return 0 if self.ignoreNotification(obj): return 0 extra_bindings = getBasicBindings(obj) extra_bindings.update({'current': obj, 'previous': self.getPreviousVersion(obj)}) return self._handlerHelper(obj, 'item_modification', extra_bindings, extra_bindings, extra_bindings) decPrivate('onItemDeletion') def onItemDeletion(self, obj, container): """Handler called when an item is deleted. It returns the number of mails which have been sent. """ if not self.getProperty('item_deletion_notification_enabled'): return 0 if self.ignoreNotification(obj): return 0 extra_bindings = getBasicBindings(obj) extra_bindings.update({'current': None, 'previous': obj, 'container': container}) return self._handlerHelper(obj, 'item_deletion', extra_bindings, extra_bindings, extra_bindings) decPrivate('onWorkflowTransition') def onWorkflowTransition(self, workflow, obj, action, result): """Handler called when a workflow transition is triggered. It returns the number of mails which have been sent. """ if not self.getProperty('wf_transition_notification_enabled'): return 0 if self.ignoreNotification(obj): return 0 wtool = getToolByName(self, 'portal_workflow') current_state = wtool.getInfoFor(obj, 'review_state') comments = wtool.getInfoFor(obj, 'comments') extra_bindings = getBasicBindings(obj) extra_bindings.update({'transition': action, 'comments': comments}) return self._handlerHelper(obj, 'wf_transition', extra_bindings, extra_bindings, extra_bindings) decPrivate('onMemberRegistration') def onMemberRegistration(self, member, properties): """Handler called when a new portal member has been registered. It returns the number of mails which have been sent. """ if not self.getProperty('member_registration_notification_enabled'): return 0 if self.ignoreNotification(member): return 0 if properties is None: properties = {} ## FIXME: How could it be? (Damien) current_user = getSecurityManager().getUser() extra_bindings = {'current_user': current_user, 'member': member, 'properties': properties, 'event': 'registration'} return self._handlerHelper(member, 'member_registration', extra_bindings, extra_bindings, extra_bindings) decPrivate('onMemberModification') def onMemberModification(self, member): """Handler called when a member changes his/her properties. It returns the number of mails which have been sent. """ ## This method can also be called when the member is ## registered. We have to check that. stack = inspect.stack() ## 1st item is ourself ## 2nd item is 'CMFCore.MemberDataTool.notifyMemberModified()' ## 3rd item is 'CMFCore.MemberDataTool.setMemberProperties()' ## 4th item is what we want to check: it is either 'addMember' ## or 'setProperties()' caller = stack[3][3] if caller != 'setProperties': return 0 if not self.getProperty('member_modification_notification_enabled'): return 0 if self.ignoreNotification(member): return 0 ## FIXME: what is the purpose of the following lines? (Damien) memberdata = getToolByName(self, 'portal_memberdata') properties = {} for key, value in memberdata.propertyItems(): properties[key] = value current_user = getSecurityManager().getUser() extra_bindings = {'current_user': current_user, 'member': member, 'properties': properties, 'event': 'modification'} return self._handlerHelper(member, 'member_modification', extra_bindings, extra_bindings, extra_bindings) decPrivate('onDiscussionItemCreation') def onDiscussionItemCreation(self, discussion_item): """Handler called when a discussion item is created. It returns the number of mails which have been sent. """ if not self.getProperty('discussion_item_creation_notification_enabled'): return 0 if self.ignoreNotification(discussion_item): return 0 ## We add two bindings to disambiguate the meaning of 'here' ## in the mail template and the rules: 'discussion_item' and ## 'discussed_item'. discussed_item = discussion_item while discussed_item.meta_type == discussion_item.meta_type: discussed_item = discussed_item.aq_inner.aq_parent.aq_parent extra_bindings = getBasicBindings(discussion_item) extra_bindings = {'discussion_item': discussion_item, 'discussed_item': discussed_item} return self._handlerHelper(discussion_item, 'discussion_item_creation', extra_bindings, extra_bindings, extra_bindings) def _handlerHelper(self, obj, what, get_users_extra_bindings, mail_template_extra_bindings, mail_template_options): """An helper method for ``on*()`` handlers. It returns the number of mails which have been sent. """ self._updateSubscriptionMapping(obj) users = self.getUsersToNotify(obj, what, get_users_extra_bindings) if self.isExtraSubscriptionsEnabled(): users.extend(self.getExtraSubscribersOf(self._getPath(obj))) users = self.removeUnAuthorizedSubscribers(users, obj) addresses = self.getEmailAddresses(users) if not addresses: LOG.warning("No addresses for '%s' notification of %s", what, obj.absolute_url(1)) return 0 template = self.getMailTemplate(obj, what, mail_template_extra_bindings) if template is None: LOG.warning("No mail template for '%s' notification of %s", what, obj.absolute_url(1)) return 0 try: message = template(**mail_template_options) except ConflictError: raise except: LOG.error("Cannot evaluate mail template '%s' on '%s' "\ "for '%s'", template.absolute_url(1), obj.absolute_url(1), what, exc_info=True) return 0 return self.sendNotification(addresses, message) ################################################################# ################################################################# ## Utility methods ############################### decPrivate('ignoreNotification') def ignoreNotification(self, obj): """Return ``True`` iff notification have been set to be ignored for ``obj``. """ ec = getExpressionContext(obj) users = [] for match_expr in self.getProperty('ignore_rules', ()): try: if self._match(match_expr, ec): return True except ConflictError: raise except: LOG.error("Error in 'ignore_rules' rule "\ "('%s') for '%s'", match_expr, obj.absolute_url(1), exc_info=True) return False decPrivate('getUsersToNotify') def getUsersToNotify(self, obj, what, ec_bindings=None): """Return a list of users to notify for the ``what`` of an object ``obj``, ``what`` being one of the implemented notification ("*item_modification*", "*wf_transition*", etc.). ``ec_bindings`` is a mapping which is injected into the expression context of the expression of the rules. """ rules = self.getProperty('on_%s_users' % what, None) if rules is None: raise NotImplementedError, \ 'Notification on "%s" is not implemented.' % what ec = getExpressionContext(obj, ec_bindings) users = [] for rule in rules: try: match_expr, users_expr = rule.split(RULE_DELIMITER) match_expr, users_expr = match_expr.strip(), users_expr.strip() except ValueError: LOG.error("'%s' is not a valid rule "\ "('on_%s_users' on '%s')", rule, what, obj.absolute_url(1)) continue match_expr = match_expr.strip() users_expr = users_expr.strip() try: if not self._match(match_expr, ec): continue except ConflictError: raise except: LOG.error("Error in 'on_%s_users' rule "\ "('%s') for '%s'", what, match_expr, obj.absolute_url(1), exc_info=True) continue if users_expr == '*': users.extend(self.getAllUsers()) break else: try: users.extend(Expression(users_expr)(ec)) except ConflictError: raise except: LOG.error("Error in 'on_%s_users' rule "\ "('%s') for '%s'", what, users_expr, obj.absolute_url(1), exc_info=True) return users decPrivate('getMailTemplate') def getMailTemplate(self, obj, what, ec_bindings=None): """Return the template to notify for the ``what`` of an object ``obj``, ``what`` being one of the implemented notification ("*item_modification*", "*wf_transition*", etc.), or ``None`` if none could be found. ``ec_bindings`` is a mapping which is injected into the expression context of the expression of the rules. """ rules = self.getProperty('on_%s_mail_template' % what, None) if rules is None: raise NotImplementedError, \ 'Notification on "%s" is not implemented.' ec = getExpressionContext(obj, ec_bindings) template = None for rule in rules: try: match_expr, template_expr = rule.split(RULE_DELIMITER) match_expr, template_expr = match_expr.strip(), template_expr.strip() except ValueError: LOG.error("'%s' is not a valid rule "\ "('on_%s_mail_template' on '%s')", rule, what, obj.absolute_url(1)) continue match_expr = match_expr.strip() template_expr = template_expr.strip() try: if not self._match(match_expr, ec): continue except ConflictError: raise except: LOG.error("Error in 'on_%s_mail_template' rule "\ "('%s') for '%s'", what, match_expr, obj.absolute_url(1), exc_info=True) continue try: template = Expression(template_expr)(ec) except ConflictError: raise except: LOG.error("Error in 'on_%s_mail_template' rule "\ "('%s') for '%s'", what, template_expr, obj.absolute_url(1), exc_info=True) continue if type(template) == StringType: template = getattr(obj, template, None) if template is not None: break return template decPrivate('getAllUsers') def getAllUsers(self): """Return a list of all user ids of the portal. **Warning:** this method may be costly if you rely on an external (non ZODB) user source. Use it at your own risk. """ mtool = getToolByName(self, 'portal_membership') return mtool.listMemberIds() decPrivate('removeUnAuthorizedSubscribers') def removeUnAuthorizedSubscribers(self, subscribers, obj): """Return users from ``subscribers`` who are authorized to view ``obj``. """ portal = getToolByName(self, 'portal_url').getPortalObject() mtool = getToolByName(self, 'portal_membership') filtered_subscribers = [] for subscriber in subscribers: ## We use '_huntUser()' because 'mtool.getMemberById()' ## would have provided a wrapped user object, with a ## specific context where the user is not allowed to view ## 'obj'. (Damien) member = mtool._huntUser(str(subscriber), portal) if member is not None: if member.has_permission('View', obj): filtered_subscribers.append(subscriber) elif self._anonymousShouldBeNotified(obj): filtered_subscribers.append(subscriber) return filtered_subscribers decPrivate('getPreviousVersion') def getPreviousVersion(self, obj): """Return the previous version of the object, or ``None`` if none could be found. FIXME: various implementations have been tried, without luck. See previous revisions of this file in the SVN repository. (Damien) Update (06/03/2006): Interesting related informations might be found at https://dev.plone.org/archetypes/ticket/648 """ return None decPrivate('getEmailAddresses') def getEmailAddresses(self, users): """Return email addresses of ``users``. For each value in ``users``: - if the value is not an e-mail, suppose it is an user id and try to get the ``email`` property of this user; - remove duplicates; - remove bogus e-mail addresses. """ mtool = getToolByName(self, 'portal_membership') addresses = {} for user in users: member = mtool.getMemberById(str(user)) if member is not None: user = member.getProperty('email', '') if EMAIL_REGEXP.match(user): addresses[user] = 1 return addresses.keys() decPrivate('sendNotification') def sendNotification(self, addresses, message): """Send ``message`` to all ``addresses``.""" mailhosts = self.superValues(MAIL_HOST_META_TYPES) if not mailhosts: raise MailHostNotFound mailhost = mailhosts[0] ptool = getToolByName(self, 'portal_properties') encoding = ptool.site_properties.getProperty('default_charset') message = encodeMailHeaders(message, encoding) if self.getProperty('debug_log_addresses'): LOG.info('About to send notifications to %s', addresses) n_messages_sent = 0 ## FIXME: This may not be very efficient. Or maybe it ## is... Would be good to take a look at other products which ## provide a similar feature. for address in addresses: this_message = ('To: %s\n' % address) + message try: ## FIXME: SecureMailHost, which is shipped with Plone, ## has declared 'send' deprecated and says ## 'secureSend' should be used instead. It seems to me ## that using '_send()' would be easier. (Damien) mailhost.send(this_message) n_messages_sent += 1 except ConflictError: raise except: LOG.error('Error while sending '\ 'notification: \n%s' % this_message, exc_info=True) pass return n_messages_sent def _match(self, expr, ec): """Return ``True`` if ``expr`` returns something which can be evaluated to ``True`` in the expression context (``ec``) or if ``expr`` is "*". """ if expr == '*': return True expr = Expression(expr) return bool(expr(ec)) def _getPath(self, obj): """Return path of ``obj``. A slash ('/') is appended to the path if the object is folderish. The returned path is relative to the portal object. """ utool = getToolByName(self, 'portal_url') path = utool.getRelativeContentURL(obj) path = '/' + path if not getattr(obj.aq_base, 'isPrincipiaFolderish', False): return path if path[-1] != '/': path += '/' return path def _getParents(self, path): """Get the parents of the item corresponding to ``path`` and return their respective path. Parents are returned from ``path`` to the portal root object. """ if path == '/': return [] if path[-1] == '/': path = path[:-1] parent = path[:path.rfind('/') + 1] parents = [parent] parents.extend(self._getParents(parent)) return tuple(parents) def _getUID(self, obj): """Return UID of the object.""" portal_uidhandler = getToolByName(self, 'portal_uidhandler') uid = portal_uidhandler.queryUid(obj, None) return uid def _anonymousShouldBeNotified(self, obj): """Return ``True`` iff anonymous users should be notified. It returns ``True`` iff anonymous users have the ``View`` permission on ``obj``. """ return 'Anonymous' in rolesForPermissionOn('View', obj) ################################################################# ################################################################# ## Extra subscriptions settings ############################### decProtected('View', 'isExtraSubscriptionsEnabled') def isExtraSubscriptionsEnabled(self): """Return ``True`` iff extra subscriptions are enabled.""" return self.getProperty('extra_subscriptions_enabled') decProtected('View', 'isExtraSubscriptionsRecursive') def isExtraSubscriptionsRecursive(self): """Return ``True`` iff extra subscriptions are recursive. Note that this method does not check whether extra subscriptions are enabled or not. """ return self.getProperty('extra_subscriptions_recursive') decProtected('View', 'isExtraSubscriptionsForAuthenticatedOnly') def isExtraSubscriptionsForAuthenticatedOnly(self): """Return ``True iff extra subscriptions are restricted to authenticated users. Note that this method does not check whether extra subscriptions are enabled or not. """ return self.getProperty('extra_subscriptions_for_authenticated_only') ################################################################# ################################################################# ## Extra subscriptions logic ############################ def _updateSubscriptionMapping(self, obj): """Update subscription mapping.""" uid = self._getUID(obj) if uid is not None: path = self._getPath(obj) known_path = self._uid_to_path.get(uid) if known_path != path: self._uid_to_path[uid] = path if known_path is not None: ## We have old informations for this object for key, value in self._subscriptions.items(): if key.startswith(known_path): new_key = path + key[len(known_path) : ] self._subscriptions[new_key] = value del self._subscriptions[key] decPublic('currentUserHasSubscribePermission') def currentUserHasSubscribePermissionOn(self, obj): """Return ``True`` iff the current user has ``SUBSCRIBE_PERMISSION`` on ``obj``. """ mtool = getToolByName(self, 'portal_membership') return mtool.checkPermission(SUBSCRIBE_PERMISSION, obj) decPublic('subscriptionToParentIsAllowed') def isSubscriptionToParentAllowed(self, obj): """Return ``True`` iff subscription to the parent of ``obj`` (i.e. the first folderish item above ``obj``) is allowed. This method uses Plone specific scripts. """ if self.isExtraSubscriptionsRecursive() \ and not obj.is_folderish() \ and obj.isDefaultPageInFolder(): try: parent = obj.aq_parent except ConflictError: raise except: return False return self.currentUserHasSubscribePermissionOn(parent) return False decPublic('showSubscriptionPortlet') def showSubscriptionPortlet(self, obj): """Return ``True`` iff the subscription portlet should be shown while viewing ``obj``. """ mtool = getToolByName(self, 'portal_membership') anonymous = mtool.isAnonymousUser() return self.isExtraSubscriptionsEnabled() \ and not (self.isExtraSubscriptionsForAuthenticatedOnly() \ and anonymous) \ and self.currentUserHasSubscribePermissionOn(obj) decPublic('subscribeTo') def subscribeTo(self, obj, email=None): """Subscribe ``email`` (or the current user if ``email`` is None) to ``obj``. """ if not self.isExtraSubscriptionsEnabled(): raise DisabledFeature if email is not None: if not self.isExtraSubscriptionsForAuthenticatedOnly(): raise DisabledFeature if not EMAIL_REGEXP.match(email): raise InvalidEmailAddress allowed_roles = rolesForPermissionOn(SUBSCRIBE_PERMISSION, obj) if 'Anonymous' not in allowed_roles: raise Unauthorized ## FIXME: We would like to send an email to ask the user ## to confirm its subscription. Since this has not yet ## been implemented, we raise an error. (Damien) raise NotImplementedError elif not self.currentUserHasSubscribePermissionOn(obj): raise Unauthorized else: user = getSecurityManager().getUser().getId() self._updateSubscriptionMapping(obj) path = self._getPath(obj) subscribers = self._subscriptions.get(path, {}) subscribers[user] = 1 self._subscriptions[path] = subscribers decPublic('unSubscribeFrom') def unSubscribeFrom(self, obj, email=None): """Unsubscribe ``email`` (or the current user if ``email`` is ``None``) from ``obj``. """ if not self.isExtraSubscriptionsEnabled(): raise DisabledFeature if email is not None: if not self.isExtraSubscriptionsForAuthenticatedOnly(): raise DisabledFeature if not EMAIL_REGEXP.match(email): raise InvalidEmailAddress ## FIXME: an anonymous user would like to unsubscribe ## his/her address. This has not yet been implemented, so ## we raise an exception. (Damien) raise NotImplementedError else: user = getSecurityManager().getUser().getId() self._updateSubscriptionMapping(obj) path = self._getPath(obj) subscribers = self._subscriptions.get(path, {}) try: del subscribers[user] self._subscriptions[path] = subscribers except KeyError: pass ## User was not subscribed. decPublic('unSubscribeFromObjectAbove') def unSubscribeFromObjectAbove(self, obj, email=None): """Find folderish items above ``obj`` and unsubscribe ``email`` (or the current user if ``email`` is ``None``) from the first one (s)he is subscribed to. If ``user`` is subscribed to ``obj``, this method is equivalent to ``unSubscribeFrom(obj, user)``. """ if not self.isExtraSubscriptionsEnabled(): raise DisabledFeature if email is not None: if not self.isExtraSubscriptionsForAuthenticatedOnly(): raise DisabledFeature if not EMAIL_REGEXP.match(email): raise InvalidEmailAddress ## FIXME: an anonymous user would like to unsubscribe ## his/her address. This has not yet been implemented, so ## we raise an exception. (Damien) raise NotImplementedError else: self._updateSubscriptionMapping(obj) utool = getToolByName(obj, 'portal_url') portal = utool.getPortalObject() portal_container = portal.aq_inner.aq_parent while obj != portal_container: if self.isSubscribedTo(obj, as_if_not_recursive=True): self.unSubscribeFrom(obj) break obj = obj.aq_parent decPublic('isSubscribedTo') def isSubscribedTo(self, obj, email=None, as_if_not_recursive=False): """Return ``True`` iff ``email`` (or the current user if ``email`` is ``None``) is subscribed to ``obj``. If ``as_if_not_recursive`` is ``True``, this method acts as if the recursive mode was off. """ if not self.isExtraSubscriptionsEnabled(): raise DisabledFeature if email is None: ## Yes, 'email' is actually the id of the current user. email = getSecurityManager().getUser().getId() self._updateSubscriptionMapping(obj) path = self._getPath(obj) subscribers = self.getExtraSubscribersOf(path, as_if_not_recursive) return subscribers.has_key(email) decPrivate('getExtraSubscribersOf') def getExtraSubscribersOf(self, path, as_if_not_recursive=False): """Return users or email addresses which are subscribed to the given path. This method returns a mapping whose keys are the users or email addresses. If ``as_if_not_recursive`` is ``True``, this method acts as if the recursive mode was off. """ subscribers = self._subscriptions.get(path, {}).copy() if self.isExtraSubscriptionsRecursive() and \ not as_if_not_recursive: if path[-1] == '/': path = path[:-1] i = path.rfind('/') if i != -1: parent = path[:i + 1] subscribers.update(self.getExtraSubscribersOf(parent)) return subscribers ################################################################# decProtected(SUBSCRIBE_PERMISSION, '__useless') def __useless(self): pass
class Box(Persistent): implements(IDepositBox) def __init__(self, max_age=config.MAX_AGE, purge_days=config.PURGE_DAYS): self.data = PersistentMapping() self._last_purge = int(time.time()) self.max_age = max_age self.purge_days = purge_days def _generate_new_id(self): """Generate new id. """ new_id = id_generator() while new_id in self.data.keys(): new_id = id_generator() return new_id def put(self, value, token=None): """Put value in box, with optional token, and return generated id. Calling this method also does a purge once a day (well, when the last purge was at least 24 hours ago). The frequency can be controlled with the purge_days attribute. """ cutoff = int(time.time()) - (self.purge_days * 86400) if self._last_purge < cutoff: self.purge() if value is None: raise ValueError id = self._generate_new_id() self.data[id] = BoxItem(token, value, confirmed=False) return id def edit(self, secret, value, token=None): """Edit value in the box, when secret and optional token match. """ if value is None: raise ValueError stored = self.get(secret, token=token) if value == stored: # No change return self.data[secret] = BoxItem(token, value, confirmed=True) def get(self, secret, token=None): stored = self.data.get(secret) if stored is None: return None if stored.token != token: # raise Exception return None if not stored.confirmed: # Purge this item when it is expired: cutoff = int(time.time()) - self.max_age * 86400 if stored.timestamp < cutoff: del self.data[secret] return None if token: # When there is a token, the item must be confirmed # before we return the value. Main use case: email # confirmation. return None return stored.value def confirm(self, secret, token=None): """Confirm the item/token and return whether this succeeded or not. """ stored = self.data.get(secret) if stored is None: return None if stored.token != token: # raise Exception? return None if not stored.confirmed: # First check if the confirmation comes too late. cutoff = int(time.time()) - self.max_age * 86400 if stored.timestamp < cutoff: del self.data[secret] # Report back that we have failed, in case anyone # wants to know. return False stored.confirmed = True return True def pop(self, secret, token=None): stored = self.get(secret, token=token) if stored is None: return None self.data.pop(secret) return stored def get_all_confirmed(self): for key, stored in self.data.items(): if stored.confirmed: yield stored.value def purge(self): """Purge items that have expired. Confirmed items are not purged. """ cutoff = int(time.time()) - self.max_age * 86400 logger.info("Started purging data.") for key, stored in self.data.items(): if not stored.confirmed and stored.timestamp < cutoff: logger.info("Purged data with secret %r", key) del self.data[key] self._last_purge = int(time.time()) logger.info("Finished purging data.")