Add an API endpoint for Worklists

Add an API endpoint for managing Worklists and their contents.
Basic CRUD functionality is exposed on `/v1/worklists`, with
CRUD functionality for the contents of a given Worklist
exposed on `/v1/worklists/:id/items`.

Change-Id: I13fc79c3231cb314d1741f0c0903d5a7ac29095e
This commit is contained in:
Adam Coldrick
2015-09-10 16:07:07 +00:00
parent a049ab65ca
commit 44027e117a
5 changed files with 467 additions and 1 deletions

View File

@@ -27,6 +27,7 @@ from storyboard.api.v1.task_statuses import TaskStatusesController
from storyboard.api.v1.tasks import TasksPrimaryController
from storyboard.api.v1.teams import TeamsController
from storyboard.api.v1.users import UsersController
from storyboard.api.v1.worklists import WorklistsController
class V1Controller(object):
@@ -44,5 +45,6 @@ class V1Controller(object):
subscriptions = SubscriptionsController()
subscription_events = SubscriptionEventsController()
systeminfo = SystemInfoController()
worklists = WorklistsController()
openid = AuthController()

View File

@@ -445,3 +445,66 @@ class AccessToken(base.APIBase):
class TaskStatus(base.APIBase):
key = wtypes.text
name = wtypes.text
class Worklist(base.APIBase):
"""Represents a worklist."""
title = wtypes.text
"""The title of the worklist."""
creator_id = int
"""The ID of the User who created this worklist."""
project_id = int
"""The ID of the Project this worklist is associated with."""
permission_id = int
"""The ID of the Permission which defines who can edit this worklist."""
private = bool
"""A flag to identify if this is a private or public worklist."""
archived = bool
"""A flag to identify whether or not the worklist has been archived."""
automatic = bool
"""A flag to identify whether the contents are obtained by a filter or are
stored in the database."""
# NOTE(SotK): Criteria/Criterion is used as the existing code in the webclient
# refers to such filters as Criteria.
class WorklistCriterion(base.APIBase):
"""Represents a filter used to construct an automatic worklist."""
title = wtypes.text
"""The title of the filter, as displayed in the UI."""
list_id = int
"""The ID of the Worklist this filter is for."""
value = wtypes.text
"""The value to use as a filter."""
field = wtypes.text
"""The field to filter by."""
class WorklistItem(base.APIBase):
"""Represents an item in a worklist.
The item could be either a story or a task.
"""
list_id = int
"""The ID of the Worklist this item belongs to."""
item_id = int
"""The ID of the Task or Story for this item."""
item_type = wtypes.text
"""The type of this item, either "story" or "task"."""
list_position = int
"""The position of this item in the Worklist."""

View File

@@ -0,0 +1,243 @@
# Copyright (c) 2015 Codethink Limited
#
# 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.
from oslo_config import cfg
from pecan import abort
from pecan import request
from pecan import response
from pecan import rest
from pecan.secure import secure
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from storyboard.api.auth import authorization_checks as checks
from storyboard.api.v1 import wmodels
from storyboard.common import decorators
from storyboard.common import exception as exc
from storyboard.db.api import worklists as worklists_api
from storyboard.openstack.common.gettextutils import _ # noqa
CONF = cfg.CONF
def visible(worklist, user=None):
if not worklist:
return False
if user and worklist.private:
# TODO(SotK): Permissions
return user == worklist.creator_id
return not worklist.private
def editable(worklist, user=None):
if not worklist:
return False
if not user:
return False
# TODO(SotK): Permissions
return user == worklist.creator_id
class ItemsSubcontroller(rest.RestController):
"""Manages operations on the items in worklists."""
@decorators.db_exceptions
@secure(checks.guest)
@wsme_pecan.wsexpose([wmodels.WorklistItem], int)
def get(self, worklist_id):
"""Get items inside a worklist.
:param worklist_id: The ID of the worklist.
"""
worklist = worklists_api.get(worklist_id)
user_id = request.current_user_id
if not worklist or not visible(worklist, user_id):
raise exc.NotFound(_("Worklist %s not found") % worklist_id)
if worklist.items is None:
return []
worklist.items.sort(key=lambda i: i.list_position)
return [wmodels.WorklistItem.from_db_model(item)
for item in worklist.items]
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wmodels.WorklistItem, int, int, wtypes.text, int)
def post(self, id, item_id, item_type, list_position):
"""Add an item to a worklist.
:param id: The ID of the worklist.
:param item_id: The ID of the item.
:param item_type: The type of the item (i.e. "story" or "task").
:param list_position: The position in the list to add the item.
"""
user_id = request.current_user_id
if not editable(worklists_api.get(id), user_id):
raise exc.NotFound(_("Worklist %s not found") % id)
worklists_api.add_item(id, item_id, item_type, list_position)
return wmodels.WorklistItem.from_db_model(
worklists_api.get_item_at_position(id, list_position))
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wmodels.WorklistItem, int, int, int, int)
def put(self, id, item_id, list_position, list_id=None):
"""Update a WorklistItem.
:param id: The ID of the worklist.
:param item_id: The ID of the worklist_item to be moved.
"""
user_id = request.current_user_id
if not editable(worklists_api.get(id), user_id):
raise exc.NotFound(_("Worklist %s not found") % id)
worklists_api.update_item(id, item_id, list_position, list_id)
return wmodels.WorklistItem.from_db_model(
worklists_api.get_item_by_id(item_id))
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(None, int, int, status_code=204)
def delete(self, id, item_id):
"""Remove an item from a worklist.
:param id: The ID of the worklist.
:param item_id: The ID of the worklist item to be removed.
"""
user_id = request.current_user_id
if not editable(worklists_api.get(id), user_id):
raise exc.NotFound(_("Worklist %s not found") % id)
worklists_api.remove_item(id, item_id)
class WorklistsController(rest.RestController):
"""Manages operations on worklists."""
@decorators.db_exceptions
@secure(checks.guest)
@wsme_pecan.wsexpose(wmodels.Worklist, int)
def get_one(self, worklist_id):
"""Retrieve details about one worklist.
:param worklist_id: The ID of the worklist.
"""
worklist = worklists_api.get(worklist_id)
user_id = request.current_user_id
if worklist and visible(worklist, user_id):
return wmodels.Worklist.from_db_model(worklist)
else:
raise exc.NotFound(_("Worklist %s not found") % worklist_id)
@decorators.db_exceptions
@secure(checks.guest)
@wsme_pecan.wsexpose([wmodels.Worklist], wtypes.text, int, int,
bool, wtypes.text, wtypes.text)
def get_all(self, title=None, creator_id=None, project_id=None,
archived=False, sort_field='id', sort_dir='asc'):
"""Retrieve definitions of all of the worklists.
:param title: A string to filter the title by.
:param creator_id: Filter worklists by their creator.
:param project_id: Filter worklists by project ID.
:param archived: Filter worklists by whether they are archived or not.
:param sort_field: The name of the field to sort on.
:param sort_dir: Sort direction for results (asc, desc).
"""
worklists = worklists_api.get_all(title=title,
creator_id=creator_id,
project_id=project_id,
archived=archived,
sort_field=sort_field,
sort_dir=sort_dir)
user_id = request.current_user_id
visible_worklists = [wmodels.Worklist.from_db_model(w)
for w in worklists
if w.archived == archived
and visible(w, user_id)]
# Apply the query response headers
response.headers['X-Total'] = str(len(visible_worklists))
return visible_worklists
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wmodels.Worklist, body=wmodels.Worklist)
def post(self, worklist):
"""Create a new worklist.
:param worklist: A worklist within the request body.
"""
worklist_dict = worklist.as_dict()
user_id = request.current_user_id
if worklist.creator_id and worklist.creator_id != user_id:
abort(400, _("You can't select the creator of a worklist."))
worklist_dict.update({"creator_id": user_id})
created_worklist = worklists_api.create(worklist_dict)
return wmodels.Worklist.from_db_model(created_worklist)
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wmodels.Worklist, int, body=wmodels.Worklist)
def put(self, id, worklist):
"""Modify this worklist.
:param id: The ID of the worklist.
:param worklist: A worklist within the request body.
"""
user_id = request.current_user_id
if not editable(worklists_api.get(id), user_id):
raise exc.NotFound(_("Worklist %s not found") % id)
updated_worklist = worklists_api.update(id, worklist.as_dict())
return wmodels.Worklist.from_db_model(updated_worklist)
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(None, int, status_code=204)
def delete(self, worklist_id):
"""Archive this worklist.
:param worklist_id: The ID of the worklist to be archived.
"""
worklist = worklists_api.get(worklist_id)
user_id = request.current_user_id
if not editable(worklist, user_id):
raise exc.NotFound(_("Worklist %s not found") % worklist_id)
worklist_dict = wmodels.Worklist.from_db_model(worklist).as_dict()
worklist_dict.update({"archived": True})
worklists_api.update(worklist_id, worklist_dict)
items = ItemsSubcontroller()

View File

@@ -0,0 +1,156 @@
# Copyright (c) 2015 Codethink Limited
#
# 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.
from sqlalchemy.orm import subqueryload
from wsme.exc import ClientSideError
from storyboard.common import exception as exc
from storyboard.db.api import base as api_base
from storyboard.db.api import stories
from storyboard.db.api import tasks
from storyboard.db import models
from storyboard.openstack.common.gettextutils import _ # noqa
def _worklist_get(id, session=None):
if not session:
session = api_base.get_session()
query = session.query(models.Worklist).options(
subqueryload(models.Worklist.items)).filter_by(id=id)
return query.first()
def get(worklist_id):
return _worklist_get(worklist_id)
def get_all(title=None, creator_id=None, project_id=None,
sort_field=None, sort_dir=None, **kwargs):
return api_base.entity_get_all(models.Worklist,
title=title,
creator_id=creator_id,
project_id=project_id,
sort_field=sort_field,
sort_dir=sort_dir,
**kwargs)
def get_count(**kwargs):
return api_base.entity_get_count(models.Worklist, **kwargs)
def create(values):
return api_base.entity_create(models.Worklist, values)
def update(worklist_id, values):
return api_base.entity_update(models.Worklist, worklist_id, values)
def add_item(worklist_id, item_id, item_type, list_position):
session = api_base.get_session()
with session.begin(subtransactions=True):
worklist = _worklist_get(worklist_id, session)
if worklist is None:
raise exc.NotFound(_("Worklist %s not found") % worklist_id)
if item_type == 'story':
item = stories.story_get(item_id)
elif item_type == 'task':
item = tasks.task_get(item_id)
else:
raise ClientSideError(_("An item in a worklist must be either a "
"story or a task"))
if item is None:
raise exc.NotFound(_("%(type)s %(id)s not found") %
{'type': item_type, 'id': item_id})
item_dict = {
'list_id': worklist_id,
'item_id': item_id,
'item_type': item_type,
'list_position': list_position
}
worklist_item = api_base.entity_create(models.WorklistItem, item_dict)
if worklist.items is None:
worklist.items = [worklist_item]
else:
worklist.items.append(worklist_item)
session.add(worklist_item)
session.add(worklist)
return worklist
def get_item_by_id(item_id):
session = api_base.get_session()
query = session.query(models.WorklistItem).filter_by(id=str(item_id))
return query.first()
def get_item_at_position(worklist_id, list_position):
session = api_base.get_session()
query = session.query(models.WorklistItem).filter_by(
list_id=worklist_id, list_position=list_position)
return query.first()
def update_item_list_id(item, new_list_id):
session = api_base.get_session()
with session.begin(subtransactions=True):
old_list = _worklist_get(item.list_id)
new_list = _worklist_get(new_list_id)
if new_list is None:
raise exc.NotFound(_("Worklist %s not found") % new_list_id)
old_list.items.remove(item)
new_list.items.append(item)
def update_item(worklist_id, item_id, list_position, list_id=None):
item = get_item_by_id(item_id)
update_dict = {'list_position': list_position}
if list_id is not None:
update_item_list_id(item, list_id)
api_base.entity_update(models.WorklistItem, item_id, update_dict)
def remove_item(worklist_id, item_id):
session = api_base.get_session()
with session.begin(subtransactions=True):
worklist = _worklist_get(worklist_id, session)
if worklist is None:
raise exc.NotFound(_("Worklist %s not found") % worklist_id)
item = get_item_by_id(item_id)
if item is None:
raise exc.NotFound(_("WorklistItem %s not found") % item_id)
item_entry = [i for i in worklist.items if i.id == item_id][0]
worklist.items.remove(item_entry)
session.add(worklist)
session.delete(item)
return worklist

View File

@@ -34,7 +34,8 @@ class_mappings = {'task': [models.Task, wmodels.Task],
'story': [models.Story, wmodels.Story],
'branch': [models.Branch, wmodels.Branch],
'milestone': [models.Milestone, wmodels.Milestone],
'tag': [models.StoryTag, wmodels.Tag]}
'tag': [models.StoryTag, wmodels.Tag],
'worklist': [models.Worklist, wmodels.Worklist]}
class NotificationHook(hooks.PecanHook):
@@ -153,6 +154,7 @@ class NotificationHook(hooks.PecanHook):
'subscription_events': 'subscription_event',
'systeminfo': 'systeminfo',
'openid': 'openid',
'worklists': 'worklist',
# Second level resources
'comments': 'comment'