Merge "Refactor Tasks to work as a Subcontroller"

This commit is contained in:
Jenkins 2015-03-30 10:02:40 +00:00 committed by Gerrit Code Review
commit 0392d47b96
5 changed files with 370 additions and 66 deletions

View File

@ -24,6 +24,9 @@ Stories
.. rest-controller:: storyboard.api.v1.stories:StoriesController
:webprefix: /v1/stories
.. rest-controller:: storyboard.api.v1.stories:TasksNestedController
:webprefix: /v1/stories/<story_id>/tasks
Comments and Timeline events
============================
.. rest-controller:: storyboard.api.v1.timeline:CommentsController
@ -34,7 +37,7 @@ Comments and Timeline events
Tasks
=====
.. rest-controller:: storyboard.api.v1.tasks:TasksController
.. rest-controller:: storyboard.api.v1.tasks:TasksPrimaryController
:webprefix: /v1/tasks
Branches

View File

@ -26,6 +26,7 @@ import wsmeext.pecan as wsme_pecan
from storyboard.api.auth import authorization_checks as checks
from storyboard.api.v1.search import search_engine
from storyboard.api.v1.tags import TagsController
from storyboard.api.v1.tasks import TasksNestedController
from storyboard.api.v1.timeline import CommentsController
from storyboard.api.v1.timeline import TimeLineEventsController
from storyboard.api.v1 import validations
@ -194,6 +195,7 @@ class StoriesController(rest.RestController):
comments = CommentsController()
events = TimeLineEventsController()
tasks = TasksNestedController()
tags = TagsController()
@decorators.db_exceptions

View File

@ -38,7 +38,63 @@ CONF = cfg.CONF
SEARCH_ENGINE = search_engine.get_engine()
class TasksController(rest.RestController):
def milestone_is_valid(milestone_id):
milestone = milestones_api.milestone_get(milestone_id)
if not milestone:
raise exc.NotFound(_("Milestone %d not found.") %
milestone_id)
if milestone['expired']:
abort(400, _("Can't associate task to expired milestone."))
def post_timeline_events(original_task, updated_task):
# If both the assignee_id and the status were changed there will be
# two separate comments in the activity log.
author_id = request.current_user_id
specific_change = False
if original_task.status != updated_task.status:
events_api.task_status_changed_event(
story_id=original_task.story_id,
task_id=original_task.id,
task_title=original_task.title,
author_id=author_id,
old_status=original_task.status,
new_status=updated_task.status)
specific_change = True
if original_task.priority != updated_task.priority:
events_api.task_priority_changed_event(
story_id=original_task.story_id,
task_id=original_task.id,
task_title=original_task.title,
author_id=author_id,
old_priority=original_task.priority,
new_priority=updated_task.priority)
specific_change = True
if original_task.assignee_id != updated_task.assignee_id:
events_api.task_assignee_changed_event(
story_id=original_task.story_id,
task_id=original_task.id,
task_title=original_task.title,
author_id=author_id,
old_assignee_id=original_task.assignee_id,
new_assignee_id=updated_task.assignee_id)
specific_change = True
if not specific_change:
events_api.task_details_changed_event(
story_id=original_task.story_id,
task_id=original_task.id,
task_title=original_task.title,
author_id=author_id)
class TasksPrimaryController(rest.RestController):
"""Manages tasks."""
_custom_actions = {"search": ["GET"]}
@ -128,16 +184,6 @@ class TasksController(rest.RestController):
return [wmodels.Task.from_db_model(s) for s in tasks]
def _milestone_is_valid(self, milestone_id):
milestone = milestones_api.milestone_get(milestone_id)
if not milestone:
raise exc.NotFound(_("Milestone %d not found.") %
milestone_id)
if milestone['expired']:
abort(400, _("Can't associate task to expired milestone."))
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wmodels.Task, body=wmodels.Task)
@ -155,7 +201,7 @@ class TasksController(rest.RestController):
abort(400,
_("Milestones can only be associated with merged tasks"))
self._milestone_is_valid(task.milestone_id)
milestone_is_valid(task.milestone_id)
creator_id = request.current_user_id
task.creator_id = creator_id
@ -184,9 +230,6 @@ class TasksController(rest.RestController):
if task.creator_id and task.creator_id != original_task.creator_id:
abort(400, _("You can't change author of task."))
updated_task = tasks_api.task_update(task_id,
task.as_dict(omit_unset=True))
if task.milestone_id:
if original_task['status'] != 'merged' and task.status != 'merged':
abort(400,
@ -197,7 +240,7 @@ class TasksController(rest.RestController):
abort(400,
_("Milestones can only be associated with merged tasks"))
self._milestone_is_valid(task.milestone_id)
milestone_is_valid(task.milestone_id)
task_dict = task.as_dict(omit_unset=True)
@ -207,55 +250,11 @@ class TasksController(rest.RestController):
updated_task = tasks_api.task_update(task_id, task_dict)
if updated_task:
self._post_timeline_events(original_task, updated_task)
post_timeline_events(original_task, updated_task)
return wmodels.Task.from_db_model(updated_task)
else:
raise exc.NotFound(_("Task %s not found") % task_id)
def _post_timeline_events(self, original_task, updated_task):
# If both the assignee_id and the status were changed there will be
# two separate comments in the activity log.
author_id = request.current_user_id
specific_change = False
if original_task.status != updated_task.status:
events_api.task_status_changed_event(
story_id=original_task.story_id,
task_id=original_task.id,
task_title=original_task.title,
author_id=author_id,
old_status=original_task.status,
new_status=updated_task.status)
specific_change = True
if original_task.priority != updated_task.priority:
events_api.task_priority_changed_event(
story_id=original_task.story_id,
task_id=original_task.id,
task_title=original_task.title,
author_id=author_id,
old_priority=original_task.priority,
new_priority=updated_task.priority)
specific_change = True
if original_task.assignee_id != updated_task.assignee_id:
events_api.task_assignee_changed_event(
story_id=original_task.story_id,
task_id=original_task.id,
task_title=original_task.title,
author_id=author_id,
old_assignee_id=original_task.assignee_id,
new_assignee_id=updated_task.assignee_id)
specific_change = True
if not specific_change:
events_api.task_details_changed_event(
story_id=original_task.story_id,
task_id=original_task.id,
task_title=original_task.title,
author_id=author_id)
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wmodels.Task, int, status_code=204)
@ -266,6 +265,9 @@ class TasksController(rest.RestController):
"""
original_task = tasks_api.task_get(task_id)
if not original_task:
raise exc.NotFound(_("Task %s not found.") % task_id)
events_api.task_deleted_event(
story_id=original_task.story_id,
task_id=original_task.id,
@ -289,3 +291,202 @@ class TasksController(rest.RestController):
limit=limit)
return [wmodels.Task.from_db_model(task) for task in tasks]
class TasksNestedController(rest.RestController):
"""Manages tasks through the /stories/<story_id>/tasks endpoint."""
validation_post_schema = validations.TASKS_POST_SCHEMA
validation_put_schema = validations.TASKS_PUT_SCHEMA
@decorators.db_exceptions
@secure(checks.guest)
@wsme_pecan.wsexpose(wmodels.Task, int, int)
def get_one(self, story_id, task_id):
"""Retrieve details about one task.
:param story_id: An ID of the story.
:param task_id: An ID of the task.
"""
task = tasks_api.task_get(task_id)
if task:
if task.story_id != story_id:
abort(400, _("URL story_id and task.story_id do not match"))
return wmodels.Task.from_db_model(task)
else:
raise exc.NotFound(_("Task %s not found") % task_id)
@decorators.db_exceptions
@secure(checks.guest)
@wsme_pecan.wsexpose([wmodels.Task], int, wtypes.text, int, int, int, int,
int, [wtypes.text], [wtypes.text], int, int,
wtypes.text, wtypes.text)
def get_all(self, story_id, title=None, assignee_id=None, project_id=None,
project_group_id=None, branch_id=None, milestone_id=None,
status=None, priority=None, marker=None, limit=None,
sort_field='id', sort_dir='asc'):
"""Retrieve definitions of all of the tasks.
:param story_id: filter tasks by story ID.
:param title: search by task title.
:param assignee_id: filter tasks by who they are assigned to.
:param project_id: filter the tasks based on project.
:param project_group_id: filter tasks based on project group.
:param branch_id: filter tasks based on branch_id.
:param milestone_id: filter tasks based on milestone.
:param status: filter tasks by status.
:param priority: filter tasks by priority.
:param marker: The resource id where the page should begin.
:param limit: The number of tasks 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 None:
limit = CONF.page_size_default
limit = min(CONF.page_size_maximum, max(1, limit))
# Resolve the marker record.
marker_task = tasks_api.task_get(marker)
tasks = tasks_api \
.task_get_all(title=title,
story_id=story_id,
assignee_id=assignee_id,
project_id=project_id,
project_group_id=project_group_id,
branch_id=branch_id,
milestone_id=milestone_id,
status=status,
priority=priority,
sort_field=sort_field,
sort_dir=sort_dir,
marker=marker_task,
limit=limit)
task_count = tasks_api \
.task_get_count(title=title,
story_id=story_id,
assignee_id=assignee_id,
project_id=project_id,
project_group_id=project_group_id,
branch_id=branch_id,
milestone_id=milestone_id,
status=status,
priority=priority)
# Apply the query response headers.
response.headers['X-Limit'] = str(limit)
response.headers['X-Total'] = str(task_count)
if marker_task:
response.headers['X-Marker'] = str(marker_task.id)
return [wmodels.Task.from_db_model(s) for s in tasks]
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wmodels.Task, int, body=wmodels.Task)
def post(self, story_id, task):
"""Create a new task.
:param story_id: An ID of the story.
:param task: a task within the request body.
"""
if task.creator_id and task.creator_id != request.current_user_id:
abort(400, _("You can't select author of task."))
creator_id = request.current_user_id
task.creator_id = creator_id
if not task.story_id:
task.story_id = story_id
if task.story_id != story_id:
abort(400, _("URL story_id and task.story_id do not match"))
if task.milestone_id:
if task.status != 'merged':
abort(400,
_("Milestones can only be associated with merged tasks"))
milestone_is_valid(task.milestone_id)
created_task = tasks_api.task_create(task.as_dict())
events_api.task_created_event(story_id=task.story_id,
task_id=created_task.id,
task_title=created_task.title,
author_id=creator_id)
return wmodels.Task.from_db_model(created_task)
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wmodels.Task, int, int, body=wmodels.Task)
def put(self, story_id, task_id, task):
"""Modify this task.
:param story_id: An ID of the story.
:param task_id: An ID of the task.
:param task: a task within the request body.
"""
original_task = tasks_api.task_get(task_id)
if original_task.story_id != story_id:
abort(400, _("URL story_id and task.story_id do not match"))
if task.creator_id and task.creator_id != original_task.creator_id:
abort(400, _("You can't change author of task."))
if task.milestone_id:
if original_task['status'] != 'merged' and task.status != 'merged':
abort(400,
_("Milestones can only be associated with merged tasks"))
if (original_task['status'] == 'merged' and
task.status and task.status != 'merged'):
abort(400,
_("Milestones can only be associated with merged tasks"))
milestone_is_valid(task.milestone_id)
task_dict = task.as_dict(omit_unset=True)
if task.status and task.status != 'merged':
task_dict['milestone_id'] = None
updated_task = tasks_api.task_update(task_id, task_dict)
if updated_task:
post_timeline_events(original_task, updated_task)
return wmodels.Task.from_db_model(updated_task)
else:
raise exc.NotFound(_("Task %s not found") % task_id)
@decorators.db_exceptions
@secure(checks.authenticated)
@wsme_pecan.wsexpose(wmodels.Task, int, int, status_code=204)
def delete(self, story_id, task_id):
"""Delete this task.
:param story_id: An ID of the story.
:param task_id: An ID of the task.
"""
original_task = tasks_api.task_get(task_id)
if not original_task:
raise exc.NotFound(_("Task %s not found.") % task_id)
if original_task.story_id != story_id:
abort(400, _("URL story_id and task.story_id do not match"))
events_api.task_deleted_event(
story_id=original_task.story_id,
task_id=original_task.id,
task_title=original_task.title,
author_id=request.current_user_id)
tasks_api.task_delete(task_id)

View File

@ -24,7 +24,7 @@ from storyboard.api.v1.subscriptions import SubscriptionsController
from storyboard.api.v1.system_info import SystemInfoController
from storyboard.api.v1.tags import TagsController
from storyboard.api.v1.task_statuses import TaskStatusesController
from storyboard.api.v1.tasks import TasksController
from storyboard.api.v1.tasks import TasksPrimaryController
from storyboard.api.v1.teams import TeamsController
from storyboard.api.v1.users import UsersController
@ -39,7 +39,7 @@ class V1Controller(object):
milestones = MilestonesController()
stories = StoriesController()
tags = TagsController()
tasks = TasksController()
tasks = TasksPrimaryController()
task_statuses = TaskStatusesController()
subscriptions = SubscriptionsController()
subscription_events = SubscriptionEventsController()

View File

@ -17,9 +17,9 @@ import six.moves.urllib.parse as urlparse
from storyboard.tests import base
class TestTasks(base.FunctionalTest):
class TestTasksPrimary(base.FunctionalTest):
def setUp(self):
super(TestTasks, self).setUp()
super(TestTasksPrimary, self).setUp()
self.resource = '/tasks'
self.task_01 = {
@ -49,6 +49,104 @@ class TestTasks(base.FunctionalTest):
self.assertEqual(1, created_task['story_id'])
class TestTasksNestedController(base.FunctionalTest):
def setUp(self):
super(TestTasksNestedController, self).setUp()
self.resource = '/stories/1/tasks'
self.task_01 = {
'title': 'StoryBoard',
'status': 'todo',
'story_id': 1
}
self.default_headers['Authorization'] = 'Bearer valid_superuser_token'
def test_tasks_endpoint(self):
response = self.get_json(self.resource)
self.assertEqual(3, len(response))
def test_get(self):
response = self.get_json(self.resource + "/1")
# Get an existing task under a given story
self.assertIsNotNone(response)
def test_get_for_wrong_story(self):
response = self.get_json(self.resource + "/4", expect_errors=True)
# Get an existing task under a given story
self.assertEqual(400, response.status_code)
def test_create(self):
result = self.post_json(self.resource, {
'title': 'StoryBoard',
'status': 'todo',
'story_id': 1
})
# Retrieve the created task
created_task = self \
.get_json('%s/%d' % ("/tasks", result.json['id']))
self.assertEqual(result.json['id'], created_task['id'])
self.assertEqual('StoryBoard', created_task['title'])
self.assertEqual('todo', created_task['status'])
self.assertEqual(1, created_task['story_id'])
def test_create_id_in_url(self):
result = self.post_json(self.resource, {
'title': 'StoryBoard',
'status': 'todo',
})
# story_id is not set in the body. URL should handle that
# Retrieve the created task
created_task = self \
.get_json('%s/%d' % ("/tasks", result.json['id']))
self.assertEqual(result.json['id'], created_task['id'])
self.assertEqual('StoryBoard', created_task['title'])
self.assertEqual('todo', created_task['status'])
self.assertEqual(1, created_task['story_id'])
def test_create_error(self):
result = self.post_json(self.resource, {
'title': 'StoryBoard',
'status': 'todo',
'story_id': 100500
}, expect_errors=True)
# task.story_id does not match the URL
self.assertEqual(400, result.status_code)
def test_update(self):
original_task = self.get_json(self.resource)[0]
original_id = original_task["id"]
result = self.put_json(self.resource + "/%s" % original_id, {
'title': 'task_updated',
})
self.assertEqual(200, result.status_code)
def test_update_error(self):
original_task = self.get_json(self.resource)[0]
original_id = original_task["id"]
result = self.put_json(self.resource + "/%s" % original_id, {
'title': 'task_updated',
'story_id': 100500
}, expect_errors=True)
self.assertEqual(400, result.status_code)
def test_delete(self):
result = self.delete(self.resource + "/1")
self.assertEqual(204, result.status_code)
def test_delete_for_wrong_story(self):
result = self.delete(self.resource + "/4", expect_errors=True)
self.assertEqual(400, result.status_code)
class TestTaskSearch(base.FunctionalTest):
def setUp(self):
super(TestTaskSearch, self).setUp()