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:
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
243
storyboard/api/v1/worklists.py
Normal file
243
storyboard/api/v1/worklists.py
Normal 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()
|
||||
156
storyboard/db/api/worklists.py
Normal file
156
storyboard/db/api/worklists.py
Normal 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
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user