187 lines
7.0 KiB
Python
187 lines
7.0 KiB
Python
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import json
|
|
import re
|
|
|
|
from pecan import hooks
|
|
from wsme.rest.json import tojson
|
|
|
|
from storyboard.api.v1 import wmodels
|
|
import storyboard.common.hook_priorities as priority
|
|
from storyboard.db.api import base as api_base
|
|
from storyboard.db import models
|
|
from storyboard.notifications.publisher import publish
|
|
|
|
|
|
class_mappings = {'task': [models.Task, wmodels.Task],
|
|
'project_group': [models.ProjectGroup, wmodels.ProjectGroup],
|
|
'project': [models.ProjectGroup, wmodels.Project],
|
|
'user': [models.User, wmodels.User],
|
|
'team': [models.Team, wmodels.Team],
|
|
'story': [models.Story, wmodels.Story],
|
|
'branch': [models.Branch, wmodels.Branch],
|
|
'milestone': [models.Milestone, wmodels.Milestone],
|
|
'tag': [models.StoryTag, wmodels.Tag],
|
|
'worklist': [models.Worklist, wmodels.Worklist],
|
|
'board': [models.Board, wmodels.Board],
|
|
'comment': [models.Comment, wmodels.Comment],
|
|
'due_date': [models.DueDate, wmodels.DueDate],
|
|
'event': [models.TimeLineEvent, wmodels.TimeLineEvent]}
|
|
|
|
|
|
class NotificationHook(hooks.PecanHook):
|
|
priority = priority.DEFAULT
|
|
|
|
def __init__(self):
|
|
super(NotificationHook, self).__init__()
|
|
|
|
def before(self, state):
|
|
# Ignore get methods, we only care about changes.
|
|
if state.request.method not in ['POST', 'PUT', 'DELETE']:
|
|
return
|
|
|
|
request = state.request
|
|
|
|
# Attempt to determine the type of the payload. This checks for
|
|
# nested paths.
|
|
(resource, resource_id, subresource, subresource_id) \
|
|
= self.parse(request.path)
|
|
|
|
state.old_entity_values = self.get_original_resource(resource,
|
|
resource_id)
|
|
|
|
def after(self, state):
|
|
# Ignore get methods, we only care about changes.
|
|
if state.request.method not in ['POST', 'PUT', 'DELETE']:
|
|
return
|
|
|
|
# Ignore requests that failed
|
|
if state.response.status_code >= 400:
|
|
return
|
|
|
|
request = state.request
|
|
response = state.response
|
|
|
|
# Attempt to determine the type of the payload. This checks for
|
|
# nested paths.
|
|
(resource, resource_id, subresource, subresource_id) \
|
|
= self.parse(request.path)
|
|
|
|
# FIXME: Ignore worklists and boards for now, since they cause a
|
|
# 500 error when moving cards that seems to be pika breaking.
|
|
if resource in ['board', 'worklist']:
|
|
return
|
|
|
|
# On a POST method, the server has assigned an ID to the resource,
|
|
# so we should be getting it from the resource rather than the URL.
|
|
if state.request.method == 'POST':
|
|
if response.body:
|
|
response_body = json.loads(response.body)
|
|
if not subresource:
|
|
resource_id = response_body.get('id')
|
|
elif subresource == 'comment':
|
|
subresource_id = response_body.get('comment').get('id')
|
|
else:
|
|
resource_id = None
|
|
|
|
# Get a copy of the resource post-modification. Will return None in
|
|
# the case of a DELETE.
|
|
new_resource = self.get_original_resource(resource, resource_id)
|
|
|
|
# Extract the old resource when possible.
|
|
if hasattr(state, 'old_entity_values'):
|
|
old_resource = state.old_entity_values
|
|
else:
|
|
old_resource = None
|
|
|
|
# Build the payload. Use of None is included to ensure that we don't
|
|
# accidentally blow up the API call, but we don't anticipate it
|
|
# happening.
|
|
publish(author_id=request.current_user_id,
|
|
method=request.method,
|
|
url=request.headers.get('Referer'),
|
|
path=request.path,
|
|
query_string=request.query_string,
|
|
status=response.status_code,
|
|
resource=resource,
|
|
resource_id=resource_id,
|
|
sub_resource=subresource,
|
|
sub_resource_id=subresource_id,
|
|
resource_before=old_resource,
|
|
resource_after=new_resource)
|
|
|
|
def get_original_resource(self, resource, resource_id):
|
|
"""Given a resource name and ID, will load that resource and map it
|
|
to a JSON object.
|
|
"""
|
|
if not resource or not resource_id or resource not in \
|
|
class_mappings.keys():
|
|
return None
|
|
|
|
model_class, wmodel_class = class_mappings[resource]
|
|
entity = api_base.entity_get(model_class, resource_id)
|
|
if entity:
|
|
return tojson(wmodel_class, wmodel_class.from_db_model(entity))
|
|
else:
|
|
# In the case of a DELETE, the entity will be returned as None
|
|
return None
|
|
|
|
def parse(self, s):
|
|
url_pattern = re.match(r"^(/api)?/v1/([a-z_]+)/?([0-9]+)?"
|
|
r"/?([a-z]+)?/?([0-9]+)?$", s)
|
|
if not url_pattern or url_pattern.groups()[1] == "openid":
|
|
return None, None, None, None
|
|
|
|
groups = url_pattern.groups()
|
|
resource = self.singularize_resource(groups[1])
|
|
sub_resource = self.singularize_resource(groups[3])
|
|
|
|
return resource, groups[2], sub_resource, groups[4]
|
|
|
|
def singularize_resource(self, resource_name):
|
|
"""Convert a resource name into its singular version."""
|
|
|
|
resource_naming_dict = {
|
|
|
|
# Top level resources
|
|
'stories': 'story',
|
|
'projects': 'project',
|
|
'project_groups': 'project_group',
|
|
'tasks': 'task',
|
|
'branches': 'branch',
|
|
'milestones': 'milestone',
|
|
'timeline_events': 'timeline_event',
|
|
'users': 'user',
|
|
'teams': 'team',
|
|
'tags': 'tag',
|
|
'task_statuses': 'task_status',
|
|
'subscriptions': 'subscription',
|
|
'subscription_events': 'subscription_event',
|
|
'systeminfo': 'systeminfo',
|
|
'openid': 'openid',
|
|
'worklists': 'worklist',
|
|
'boards': 'board',
|
|
'due_dates': 'due_date',
|
|
'events': 'event',
|
|
|
|
# Second level resources
|
|
'comments': 'comment'
|
|
}
|
|
|
|
if not resource_name or resource_name not in resource_naming_dict:
|
|
return None
|
|
return resource_naming_dict.get(resource_name)
|