Esempio n. 1
0
    def handle_timeline_events(self, session, resource, author, subscribers):

        for user_id in subscribers:
            user = db_api.entity_get(models.User, user_id, session=session)
            send_notification = get_preference(
                'receive_notifications_worklists', user)
            if (send_notification != 'true'
                    and resource.get('worklist_id') is not None):
                continue

            if resource['event_type'] == 'user_comment':
                event_info = json.dumps(
                    self.resolve_comments(session=session, event=resource))

            else:
                event_info = resource['event_info']

            # Don't send a notification if the user isn't allowed to see the
            # thing this event is about.
            event = events_api.event_get(resource['id'],
                                         current_user=user_id,
                                         session=session)
            if not events_api.is_visible(event, user_id, session=session):
                continue

            db_api.entity_create(models.SubscriptionEvents, {
                "author_id": author.id,
                "subscriber_id": user_id,
                "event_type": resource['event_type'],
                "event_info": event_info
            },
                                 session=session)
Esempio n. 2
0
    def handle_timeline_events(self, session, resource, author, subscribers):

        for user_id in subscribers:
            user = db_api.entity_get(models.User, user_id, session=session)
            send_notification = get_preference(
                'receive_notifications_worklists', user)
            if (send_notification != 'true' and
                    resource.get('worklist_id') is not None):
                continue

            if resource['event_type'] == 'user_comment':
                event_info = json.dumps(
                    self.resolve_comments(session=session, event=resource)
                )

            else:
                event_info = resource['event_info']

            # Don't send a notification if the user isn't allowed to see the
            # thing this event is about.
            event = events_api.event_get(
                resource['id'], current_user=user_id, session=session)
            if not events_api.is_visible(event, user_id, session=session):
                continue

            db_api.entity_create(models.SubscriptionEvents, {
                "author_id": author.id,
                "subscriber_id": user_id,
                "event_type": resource['event_type'],
                "event_info": event_info
            }, session=session)
Esempio n. 3
0
    def get_all(self,
                story_id=None,
                event_type=None,
                marker=None,
                offset=None,
                limit=None,
                sort_field=None,
                sort_dir=None):
        """Retrieve all events that have happened under specified story.

        :param story_id: Filter events by story ID.
        :param event_type: A selection of event types to get.
        :param marker: The resource id where the page should begin.
        :param offset: The offset to start the page at.
        :param limit: The number of events to retrieve.
        :param sort_field: The name of the field to sort on.
        :param sort_dir: Sort direction for results (asc, desc).
        """

        # Boundary check on limit.
        if limit is not None:
            limit = max(0, limit)

        # Sanity check on event types.
        if event_type:
            for r_type in event_type:
                if r_type not in event_types.ALL:
                    msg = _('Invalid event_type requested. Event type must be '
                            'one of the following: %s')
                    msg = msg % (', '.join(event_types.ALL), )
                    abort(400, msg)

        # Resolve the marker record.
        marker_event = None
        if marker is not None:
            marker_event = events_api.event_get(marker)

        event_count = events_api.events_get_count(story_id=story_id,
                                                  event_type=event_type)
        events = events_api.events_get_all(story_id=story_id,
                                           event_type=event_type,
                                           marker=marker_event,
                                           offset=offset,
                                           limit=limit,
                                           sort_field=sort_field,
                                           sort_dir=sort_dir)

        # Apply the query response headers.
        if limit:
            response.headers['X-Limit'] = str(limit)
        response.headers['X-Total'] = str(event_count)
        if marker_event:
            response.headers['X-Marker'] = str(marker_event.id)
        if offset is not None:
            response.headers['X-Offset'] = str(offset)

        return [
            wmodels.TimeLineEvent.resolve_event_values(
                wmodels.TimeLineEvent.from_db_model(event)) for event in events
        ]
Esempio n. 4
0
    def test_delete_story(self):
        # This test uses mock_data
        story_id = 1
        # Verify that we can look up a story with tasks and events
        story = stories_api.story_get_simple(story_id)
        self.assertIsNotNone(story)
        tasks = tasks_api.task_get_all(story_id=story_id)
        self.assertEqual(len(tasks), 3)
        task_ids = [t.id for t in tasks]
        events = events_api.events_get_all(story_id=story_id)
        self.assertEqual(len(events), 3)
        event_ids = [e.id for e in events]

        # Delete the story
        stories_api.story_delete(story_id)
        story = stories_api.story_get_simple(story_id)
        self.assertIsNone(story)
        # Verify that the story's tasks were deleted
        tasks = tasks_api.task_get_all(story_id=story_id)
        self.assertEqual(len(tasks), 0)
        for tid in task_ids:
            task = tasks_api.task_get(task_id=tid)
            self.assertIsNone(task)
        # And the events
        events = events_api.events_get_all(story_id=story_id)
        self.assertEqual(len(events), 0)
        for eid in event_ids:
            event = events_api.event_get(event_id=eid)
            self.assertIsNone(event)
Esempio n. 5
0
    def get_all(self, story_id=None, event_type=None, marker=None,
                offset=None, limit=None, sort_field=None, sort_dir=None):
        """Retrieve all events that have happened under specified story.

        Example::

          curl https://my.example.org/api/v1/stories/11/events

        :param story_id: Filter events by story ID.
        :param event_type: A selection of event types to get.
        :param marker: The resource id where the page should begin.
        :param offset: The offset to start the page at.
        :param limit: The number of events to retrieve.
        :param sort_field: The name of the field to sort on.
        :param sort_dir: Sort direction for results (asc, desc).
        """

        current_user = request.current_user_id

        # Boundary check on limit.
        if limit is not None:
            limit = max(0, limit)

        # Sanity check on event types.
        if event_type:
            for r_type in event_type:
                if r_type not in event_types.ALL:
                    msg = _('Invalid event_type requested. Event type must be '
                            'one of the following: %s')
                    msg = msg % (', '.join(event_types.ALL),)
                    abort(400, msg)

        # Resolve the marker record.
        marker_event = None
        if marker is not None:
            marker_event = events_api.event_get(marker)

        event_count = events_api.events_get_count(story_id=story_id,
                                                  event_type=event_type,
                                                  current_user=current_user)
        events = events_api.events_get_all(story_id=story_id,
                                           event_type=event_type,
                                           marker=marker_event,
                                           offset=offset,
                                           limit=limit,
                                           sort_field=sort_field,
                                           sort_dir=sort_dir,
                                           current_user=current_user)

        # Apply the query response headers.
        if limit:
            response.headers['X-Limit'] = str(limit)
        response.headers['X-Total'] = str(event_count)
        if marker_event:
            response.headers['X-Marker'] = str(marker_event.id)
        if offset is not None:
            response.headers['X-Offset'] = str(offset)

        return [wmodels.TimeLineEvent.resolve_event_values(
            wmodels.TimeLineEvent.from_db_model(event)) for event in events]
Esempio n. 6
0
    def test_delete_story(self):
        # This test uses mock_data
        story_id = 1
        # Verify that we can look up a story with tasks and events
        story = stories_api.story_get_simple(story_id)
        self.assertIsNotNone(story)
        tasks = tasks_api.task_get_all(story_id=story_id)
        self.assertEqual(len(tasks), 3)
        task_ids = [t.id for t in tasks]
        events = events_api.events_get_all(story_id=story_id)
        self.assertEqual(len(events), 3)
        event_ids = [e.id for e in events]

        # Delete the story
        stories_api.story_delete(story_id)
        story = stories_api.story_get_simple(story_id)
        self.assertIsNone(story)
        # Verify that the story's tasks were deleted
        tasks = tasks_api.task_get_all(story_id=story_id)
        self.assertEqual(len(tasks), 0)
        for tid in task_ids:
            task = tasks_api.task_get(task_id=tid)
            self.assertIsNone(task)
        # And the events
        events = events_api.events_get_all(story_id=story_id)
        self.assertEqual(len(events), 0)
        for eid in event_ids:
            event = events_api.event_get(event_id=eid)
            self.assertIsNone(event)
Esempio n. 7
0
    def get_one(self, story_id, event_id):
        """Retrieve details about one event.

        :param story_id: An ID of the story. It stays in params as a
                         placeholder so that pecan knows where to match an
                         incoming value. It will stay unused, as far as events
                         have their own unique ids.
        :param event_id: An ID of the event.
        """

        event = events_api.event_get(event_id)

        if event:
            wsme_event = wmodels.TimeLineEvent.from_db_model(event)
            wsme_event = wmodels.TimeLineEvent.resolve_event_values(wsme_event)
            return wsme_event
        else:
            raise exc.NotFound(_("Event %s not found") % event_id)
Esempio n. 8
0
    def get_one(self, story_id, event_id):
        """Retrieve details about one event.

        :param story_id: An ID of the story. It stays in params as a
                         placeholder so that pecan knows where to match an
                         incoming value. It will stay unused, as far as events
                         have their own unique ids.
        :param event_id: An ID of the event.
        """

        event = events_api.event_get(event_id)

        if event:
            wsme_event = wmodels.TimeLineEvent.from_db_model(event)
            wsme_event = wmodels.TimeLineEvent.resolve_event_values(wsme_event)
            return wsme_event
        else:
            raise exc.NotFound(_("Event %s not found") % event_id)
Esempio n. 9
0
    def get_one(self, event_id):
        """Retrieve details about one event.

        Example::

          curl https://my.example.org/api/v1/events/15994

        :param event_id: An ID of the event.
        """

        event = events_api.event_get(event_id,
                                     current_user=request.current_user_id)

        if events_api.is_visible(event, request.current_user_id):
            wsme_event = wmodels.TimeLineEvent.from_db_model(event)
            wsme_event = wmodels.TimeLineEvent.resolve_event_values(wsme_event)
            return wsme_event
        else:
            raise exc.NotFound(_("Event %s not found") % event_id)
Esempio n. 10
0
    def get_one(self, event_id):
        """Retrieve details about one event.

        Example::

          curl https://my.example.org/api/v1/events/15994

        :param event_id: An ID of the event.
        """

        event = events_api.event_get(event_id,
                                     current_user=request.current_user_id)

        if events_api.is_visible(event, request.current_user_id):
            wsme_event = wmodels.TimeLineEvent.from_db_model(event)
            wsme_event = wmodels.TimeLineEvent.resolve_event_values(wsme_event)
            return wsme_event
        else:
            raise exc.NotFound(_("Event %s not found") % event_id)
Esempio n. 11
0
    def handle(self, author_id, method, path, status, resource, resource_id,
               sub_resource=None, sub_resource_id=None,
               resource_before=None, resource_after=None):
        """This worker handles API events and attempts to determine whether
        they correspond to user subscriptions.

        :param author_id: ID of the author's user record.
        :param method: The HTTP Method.
        :param path: The full HTTP Path requested.
        :param status: The returned HTTP Status of the response.
        :param resource: The resource type.
        :param resource_id: The ID of the resource.
        :param sub_resource: The subresource type.
        :param sub_resource_id: The ID of the subresource.
        :param resource_before: The resource state before this event occurred.
        :param resource_after: The resource state after this event occurred.
        """

        if resource == 'timeline_event':
            event = timeline_events.event_get(resource_id)
            subscribers = subscriptions.subscription_get_all_subscriber_ids(
                'story', event.story_id
            )
            handle_timeline_events(event, author_id, subscribers)

        elif resource == 'project_group':
            subscribers = subscriptions.subscription_get_all_subscriber_ids(
                resource, resource_id
            )
            handle_resources(method=method,
                             resource_id=resource_id,
                             sub_resource_id=sub_resource_id,
                             author_id=author_id,
                             subscribers=subscribers)

        if method == 'DELETE' and not sub_resource_id:
            handle_deletions(resource, resource_id)
Esempio n. 12
0
    def handle_email(self,
                     session,
                     author,
                     subscribers,
                     method,
                     url,
                     path,
                     query_string,
                     status,
                     resource,
                     resource_id,
                     sub_resource=None,
                     sub_resource_id=None,
                     resource_before=None,
                     resource_after=None):
        """Send an email for a specific event.

        We assume that filtering logic has already occurred when this method
        is invoked.

        :param session: An event-specific SQLAlchemy session.
        :param author: The author's user record.
        :param subscribers: A list of subscribers that should receive an email.
        :param method: The HTTP Method.
        :param url: The Referer header from the request.
        :param path: The full HTTP Path requested.
        :param query_string: The query string from the request.
        :param status: The returned HTTP Status of the response.
        :param resource: The resource type.
        :param resource_id: The ID of the resource.
        :param sub_resource: The subresource type.
        :param sub_resource_id: The ID of the subresource.
        :param resource_before: The resource state before this event occurred.
        :param resource_after: The resource state after this event occurred.
        """

        email_config = CONF.plugin_email

        # Retrieve the template names.
        (subject_template, text_template, html_template) = \
            self.get_templates(method=method,
                               resource_name=resource,
                               sub_resource_name=sub_resource)

        # Build our factory. If an HTML template exists, add it. If it can't
        # find the template, skip.
        try:
            factory = EmailFactory(sender=email_config.sender,
                                   subject=subject_template,
                                   text_template=text_template)
        except TemplateNotFound:
            LOG.error("Templates not found [%s, %s]" %
                      (subject_template, text_template))
            return

        # Try to add an HTML template
        try:
            factory.add_text_template(html_template, 'html')
        except TemplateNotFound:
            LOG.debug('Template %s not found' % (html_template, ))

        # If there's a reply-to in our config, add that.
        if email_config.reply_to:
            factory.add_header('Reply-To', email_config.reply_to)

        # If there is a fallback URL configured, use it if needed
        if email_config.default_url and url is None:
            url = email_config.default_url

        # Resolve the resource instance
        resource_instance = self.resolve_resource_by_name(
            session, resource, resource_id)
        sub_resource_instance = self.resolve_resource_by_name(
            session, sub_resource, sub_resource_id)

        # Set In-Reply-To message id for 'task', 'story', and
        # 'worklist' resources
        story_id = None
        worklist_id = None
        if resource == 'task' and method == 'DELETE':
            # FIXME(pedroalvarez): Workaround the fact that the task won't be
            # in the database anymore if it has been deleted.
            # We should archive instead of delete to solve this.
            story_id = resource_before['story_id']
            created_at = self.resolve_resource_by_name(session, 'story',
                                                       story_id).created_at
        elif resource == 'task':
            story_id = resource_instance.story.id
            created_at = resource_instance.story.created_at
        elif resource == 'story':
            story_id = resource_instance.id
            created_at = resource_instance.created_at
        elif resource == 'worklist':
            worklist_id = resource_instance.id
            created_at = resource_instance.created_at

        if story_id and created_at:
            thread_id = "<storyboard.story.%s.%s@%s>" % (
                created_at.strftime("%Y%m%d%H%M"), story_id, getfqdn())
        elif worklist_id and created_at:
            thread_id = "<storyboard.worklist.%s.%s@%s>" % (
                created_at.strftime("%Y%m%d%H%M"), story_id, getfqdn())
        else:
            thread_id = make_msgid()

        factory.add_header("In-Reply-To", thread_id)
        factory.add_header("X-StoryBoard-Subscription-Type", resource)

        # Figure out the diff between old and new.
        before, after = self.get_changed_properties(resource_before,
                                                    resource_after)

        # For each subscriber, create the email and send it.
        with smtp.get_smtp_client() as smtp_client:
            for subscriber in subscribers:

                # Make sure this subscriber's preferences indicate they want
                # email and they're not receiving digests.
                if not self.get_preference('plugin_email_enable', subscriber) \
                        or self.get_preference('plugin_email_digest',
                                               subscriber):
                    continue

                send_notification = self.get_preference(
                    'receive_notifications_worklists', subscriber)
                if send_notification != 'true' and story_id is None:
                    continue

                # Don't send a notification if the user isn't allowed to see
                # the thing this event is about.
                if 'event_type' in resource:
                    event = events_api.event_get(resource['id'],
                                                 current_user=subscriber.id,
                                                 session=session)
                    if not events_api.is_visible(
                            event, subscriber.id, session=session):
                        continue

                try:
                    # Build an email.
                    message = factory.build(recipient=subscriber.email,
                                            author=author,
                                            resource=resource_instance,
                                            sub_resource=sub_resource_instance,
                                            url=url,
                                            query_string=query_string,
                                            before=before,
                                            after=after)
                    # Send the email.
                    from_addr = message.get('From')
                    to_addrs = message.get('To')

                    try:
                        smtp_client.sendmail(from_addr=from_addr,
                                             to_addrs=to_addrs,
                                             msg=message.as_string())
                    except smtplib.SMTPException as e:
                        LOG.error('Cannot send email, discarding: %s' % (e, ))
                except Exception as e:
                    # Skip, keep going.
                    LOG.error("Cannot schedule email: %s" % (e.message, ))
Esempio n. 13
0
def subscription_get_all_subscriber_ids(resource, resource_id):
    '''Test subscription discovery. The tested algorithm is as follows:

    If you're subscribed to a project_group, you will be notified about
    project_group, project, story, and task changes.

    If you are subscribed to a project, you will be notified about project,
    story, and task changes.

    If you are subscribed to a task, you will be notified about changes to
    that task.

    If you are subscribed to a story, you will be notified about changes to
    that story and its tasks.

    :param resource: The name of the resource.
    :param resource_id: The ID of the resource.
    :return: A list of user id's.
    '''
    affected = {
        'project_group': set(),
        'project': set(),
        'story': set(),
        'task': set()
    }

    # If we accidentally pass a timeline_event, we're actually going to treat
    # it as a story.
    if resource == 'timeline_event':
        event = timeline_api.event_get(resource_id)
        if event:
            resource = 'story'
            resource_id = event.story_id
        else:
            return set()

    # Sanity check exit.
    if resource not in affected.keys():
        return set()

    # Make sure the requested resource is going to be handled.
    affected[resource].add(resource_id)

    # Resolve either from story->task or from task->story, so the root
    # resource id remains pristine.
    if resource == 'story':
        # Get this story's tasks
        query = api_base.model_query(models.Task.id) \
            .filter(models.Task.story_id.in_(affected['story']))

        affected['task'] = affected['task'] \
            .union(r for (r, ) in query.all())
    elif resource == 'task':
        # Get this tasks's story
        query = api_base.model_query(models.Task.story_id) \
            .filter(models.Task.id == resource_id)

        affected['story'].add(query.first().story_id)

    # If there are tasks, there will also be projects.
    if affected['task']:
        # Get all the tasks's projects
        query = api_base.model_query(distinct(models.Task.project_id)) \
            .filter(models.Task.id.in_(affected['task']))

        affected['project'] = affected['project'] \
            .union(r for (r, ) in query.all())

    # If there are projects, there will also be project groups.
    if affected['project']:
        # Get all the projects' groups.
        query = api_base.model_query(
            distinct(models.project_group_mapping.c.project_group_id)) \
            .filter(models.project_group_mapping.c.project_id
                    .in_(affected['project']))

        affected['project_group'] = affected['project_group'] \
            .union(r for (r, ) in query.all())

    # Load all subscribers.
    subscribers = set()
    for affected_type in affected:
        query = api_base.model_query(distinct(
            models.Subscription.user_id)) \
            .filter(models.Subscription.target_type == affected_type) \
            .filter(models.Subscription.target_id.in_(affected[affected_type]))

        results = query.all()
        subscribers = subscribers.union(r for (r, ) in results)

    return subscribers