Exemplo n.º 1
0
        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
Exemplo n.º 2
0
        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)])
Exemplo n.º 4
0
    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)])
Exemplo n.º 5
0
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
Exemplo n.º 6
0
    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
Exemplo n.º 8
0
    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 == {}")
Exemplo n.º 9
0
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
Exemplo n.º 10
0
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()
Exemplo n.º 11
0
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)
Exemplo n.º 12
0
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()
Exemplo n.º 13
0
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
Exemplo n.º 14
0
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
Exemplo n.º 15
0
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.")