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)
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)
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 ]
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)
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]
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)
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)
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)
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, ))
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