Refactor WSME models
All WSME models moved to a separate file. This is necessary for further commits to avoid cyclic imports of some controllers and their models. Change-Id: I15a23eca728a02c80119cdf008fe67dc200c3935
This commit is contained in:
@@ -18,13 +18,10 @@ from pecan import response
|
||||
from pecan import rest
|
||||
from pecan.secure import secure
|
||||
from wsme.exc import ClientSideError
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
import storyboard.api.auth.authorization_checks as checks
|
||||
from storyboard.api.v1 import base
|
||||
from storyboard.api.v1.projects import Project
|
||||
from storyboard.common.custom_types import NameType
|
||||
from storyboard.api.v1 import wmodels
|
||||
from storyboard.db.api import project_groups
|
||||
from storyboard.db.api import projects
|
||||
|
||||
@@ -32,34 +29,13 @@ from storyboard.db.api import projects
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class ProjectGroup(base.APIBase):
|
||||
"""Represents a group of projects."""
|
||||
|
||||
name = NameType()
|
||||
"""The Project Group unique name. This name will be displayed in the URL.
|
||||
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
|
||||
separators.
|
||||
"""
|
||||
|
||||
title = wtypes.text
|
||||
"""The full name of the project group, which can contain spaces, special
|
||||
characters, etc.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
name="Infra",
|
||||
title="Awesome projects")
|
||||
|
||||
|
||||
class ProjectsSubcontroller(rest.RestController):
|
||||
"""This controller should be used to list, add or remove projects from a
|
||||
Project Group.
|
||||
"""
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([Project], int)
|
||||
@wsme_pecan.wsexpose([wmodels.Project], int)
|
||||
def get(self, project_group_id):
|
||||
"""Get projects inside a project group.
|
||||
|
||||
@@ -71,18 +47,18 @@ class ProjectsSubcontroller(rest.RestController):
|
||||
if not project_group:
|
||||
raise ClientSideError("The requested project group does not exist")
|
||||
|
||||
return [Project.from_db_model(project)
|
||||
return [wmodels.Project.from_db_model(project)
|
||||
for project in project_group.projects]
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(Project, int, int)
|
||||
@wsme_pecan.wsexpose(wmodels.Project, int, int)
|
||||
def put(self, project_group_id, project_id):
|
||||
"""Add a project to a project_group
|
||||
"""
|
||||
|
||||
project_groups.project_group_add_project(project_group_id, project_id)
|
||||
|
||||
return Project.from_db_model(projects.project_get(project_id))
|
||||
return wmodels.Project.from_db_model(projects.project_get(project_id))
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(None, int, int)
|
||||
@@ -104,7 +80,7 @@ class ProjectGroupsController(rest.RestController):
|
||||
"""
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose(ProjectGroup, int)
|
||||
@wsme_pecan.wsexpose(wmodels.ProjectGroup, int)
|
||||
def get_one(self, project_group_id):
|
||||
"""Retrieve information about the given project group.
|
||||
|
||||
@@ -117,11 +93,11 @@ class ProjectGroupsController(rest.RestController):
|
||||
project_group_id,
|
||||
status_code=404)
|
||||
|
||||
return ProjectGroup.from_db_model(group)
|
||||
return wmodels.ProjectGroup.from_db_model(group)
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([ProjectGroup], int, int, unicode, unicode, unicode,
|
||||
unicode)
|
||||
@wsme_pecan.wsexpose([wmodels.ProjectGroup], int, int, unicode, unicode,
|
||||
unicode, unicode)
|
||||
def get(self, marker=None, limit=None, name=None, title=None,
|
||||
sort_field='id', sort_dir='asc'):
|
||||
"""Retrieve a list of projects groups."""
|
||||
@@ -149,10 +125,10 @@ class ProjectGroupsController(rest.RestController):
|
||||
if marker_group:
|
||||
response.headers['X-Marker'] = str(marker_group.id)
|
||||
|
||||
return [ProjectGroup.from_db_model(group) for group in groups]
|
||||
return [wmodels.ProjectGroup.from_db_model(group) for group in groups]
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(ProjectGroup, body=ProjectGroup)
|
||||
@wsme_pecan.wsexpose(wmodels.ProjectGroup, body=wmodels.ProjectGroup)
|
||||
def post(self, project_group):
|
||||
"""Create a new project group.
|
||||
|
||||
@@ -165,10 +141,10 @@ class ProjectGroupsController(rest.RestController):
|
||||
if not created_group:
|
||||
raise ClientSideError("Could not create ProjectGroup")
|
||||
|
||||
return ProjectGroup.from_db_model(created_group)
|
||||
return wmodels.ProjectGroup.from_db_model(created_group)
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(ProjectGroup, int, body=ProjectGroup)
|
||||
@wsme_pecan.wsexpose(wmodels.ProjectGroup, int, body=wmodels.ProjectGroup)
|
||||
def put(self, project_group_id, project_group):
|
||||
"""Modify this project group.
|
||||
|
||||
@@ -184,10 +160,10 @@ class ProjectGroupsController(rest.RestController):
|
||||
raise ClientSideError("Could not update group %s" %
|
||||
project_group_id)
|
||||
|
||||
return ProjectGroup.from_db_model(updated_group)
|
||||
return wmodels.ProjectGroup.from_db_model(updated_group)
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(ProjectGroup, int)
|
||||
@wsme_pecan.wsexpose(wmodels.ProjectGroup, int)
|
||||
def delete(self, project_group_id):
|
||||
"""Delete this project group.
|
||||
|
||||
|
||||
@@ -19,13 +19,11 @@ from pecan import response
|
||||
from pecan import rest
|
||||
from pecan.secure import secure
|
||||
from wsme.exc import ClientSideError
|
||||
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 base
|
||||
from storyboard.api.v1.search import search_engine
|
||||
from storyboard.common.custom_types import NameType
|
||||
from storyboard.api.v1 import wmodels
|
||||
from storyboard.db.api import projects as projects_api
|
||||
|
||||
CONF = cfg.CONF
|
||||
@@ -33,36 +31,6 @@ CONF = cfg.CONF
|
||||
SEARCH_ENGINE = search_engine.get_engine()
|
||||
|
||||
|
||||
class Project(base.APIBase):
|
||||
"""The Storyboard Registry describes the open source world as ProjectGroups
|
||||
and Projects. Each ProjectGroup may be responsible for several Projects.
|
||||
For example, the OpenStack Infrastructure ProjectGroup has Zuul, Nodepool,
|
||||
Storyboard as Projects, among others.
|
||||
"""
|
||||
|
||||
name = NameType()
|
||||
"""The Project unique name. This name will be displayed in the URL.
|
||||
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
|
||||
separators.
|
||||
"""
|
||||
|
||||
description = wtypes.text
|
||||
"""Details about the project's work, highlights, goals, and how to
|
||||
contribute. Use plain text, paragraphs are preserved and URLs are
|
||||
linked in pages.
|
||||
"""
|
||||
|
||||
is_active = bool
|
||||
"""Is this an active project, or has it been deleted?"""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
name="StoryBoard",
|
||||
description="This is an awesome project.",
|
||||
is_active=True)
|
||||
|
||||
|
||||
class ProjectsController(rest.RestController):
|
||||
"""REST controller for Projects.
|
||||
|
||||
@@ -72,7 +40,7 @@ class ProjectsController(rest.RestController):
|
||||
_custom_actions = {"search": ["GET"]}
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose(Project, int)
|
||||
@wsme_pecan.wsexpose(wmodels.Project, int)
|
||||
def get_one_by_id(self, project_id):
|
||||
"""Retrieve information about the given project.
|
||||
|
||||
@@ -82,13 +50,13 @@ class ProjectsController(rest.RestController):
|
||||
project = projects_api.project_get(project_id)
|
||||
|
||||
if project:
|
||||
return Project.from_db_model(project)
|
||||
return wmodels.Project.from_db_model(project)
|
||||
else:
|
||||
raise ClientSideError("Project %s not found" % project_id,
|
||||
status_code=404)
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose(Project, unicode)
|
||||
@wsme_pecan.wsexpose(wmodels.Project, unicode)
|
||||
def get_one_by_name(self, project_name):
|
||||
"""Retrieve information about the given project.
|
||||
|
||||
@@ -98,14 +66,14 @@ class ProjectsController(rest.RestController):
|
||||
project = projects_api.project_get_by_name(project_name)
|
||||
|
||||
if project:
|
||||
return Project.from_db_model(project)
|
||||
return wmodels.Project.from_db_model(project)
|
||||
else:
|
||||
raise ClientSideError("Project %s not found" % project_name,
|
||||
status_code=404)
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([Project], int, int, unicode, unicode, unicode,
|
||||
unicode)
|
||||
@wsme_pecan.wsexpose([wmodels.Project], int, int, unicode, unicode,
|
||||
unicode, unicode)
|
||||
def get(self, marker=None, limit=None, name=None, description=None,
|
||||
sort_field='id', sort_dir='asc'):
|
||||
"""Retrieve a list of projects.
|
||||
@@ -140,20 +108,20 @@ class ProjectsController(rest.RestController):
|
||||
if marker_project:
|
||||
response.headers['X-Marker'] = str(marker_project.id)
|
||||
|
||||
return [Project.from_db_model(p) for p in projects]
|
||||
return [wmodels.Project.from_db_model(p) for p in projects]
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(Project, body=Project)
|
||||
@wsme_pecan.wsexpose(wmodels.Project, body=wmodels.Project)
|
||||
def post(self, project):
|
||||
"""Create a new project.
|
||||
|
||||
:param project: a project within the request body.
|
||||
"""
|
||||
result = projects_api.project_create(project.as_dict())
|
||||
return Project.from_db_model(result)
|
||||
return wmodels.Project.from_db_model(result)
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(Project, int, body=Project)
|
||||
@wsme_pecan.wsexpose(wmodels.Project, int, body=wmodels.Project)
|
||||
def put(self, project_id, project):
|
||||
"""Modify this project.
|
||||
|
||||
@@ -164,7 +132,7 @@ class ProjectsController(rest.RestController):
|
||||
project.as_dict(omit_unset=True))
|
||||
|
||||
if result:
|
||||
return Project.from_db_model(result)
|
||||
return wmodels.Project.from_db_model(result)
|
||||
else:
|
||||
raise ClientSideError("Project %s not found" % project_id,
|
||||
status_code=404)
|
||||
@@ -177,7 +145,7 @@ class ProjectsController(rest.RestController):
|
||||
return False
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([Project], unicode, unicode, int, int)
|
||||
@wsme_pecan.wsexpose([wmodels.Project], unicode, unicode, int, int)
|
||||
def search(self, q="", marker=None, limit=None):
|
||||
"""The search endpoint for projects.
|
||||
|
||||
@@ -188,7 +156,7 @@ class ProjectsController(rest.RestController):
|
||||
projects = SEARCH_ENGINE.projects_query(q=q, marker=marker,
|
||||
limit=limit)
|
||||
|
||||
return [Project.from_db_model(project) for project in projects]
|
||||
return [wmodels.Project.from_db_model(project) for project in projects]
|
||||
|
||||
@expose()
|
||||
def _route(self, args, request):
|
||||
|
||||
@@ -20,14 +20,13 @@ from pecan import response
|
||||
from pecan import rest
|
||||
from pecan.secure import secure
|
||||
from wsme.exc import ClientSideError
|
||||
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 base
|
||||
from storyboard.api.v1.search import search_engine
|
||||
from storyboard.api.v1.timeline import CommentsController
|
||||
from storyboard.api.v1.timeline import TimeLineEventsController
|
||||
from storyboard.api.v1 import wmodels
|
||||
from storyboard.db.api import stories as stories_api
|
||||
from storyboard.db.api import timeline_events as events_api
|
||||
|
||||
@@ -36,65 +35,13 @@ CONF = cfg.CONF
|
||||
SEARCH_ENGINE = search_engine.get_engine()
|
||||
|
||||
|
||||
class Story(base.APIBase):
|
||||
"""The Story is the main element of StoryBoard. It represents a user story
|
||||
(generally a bugfix or a feature) that needs to be implemented. It will be
|
||||
broken down into a series of Tasks, which will each target a specific
|
||||
Project and branch.
|
||||
"""
|
||||
|
||||
title = wtypes.text
|
||||
"""A descriptive label for the story, to show in listings."""
|
||||
|
||||
description = wtypes.text
|
||||
"""A complete description of the goal this story wants to cover."""
|
||||
|
||||
is_bug = bool
|
||||
"""Is this a bug or a feature :)"""
|
||||
|
||||
creator_id = int
|
||||
"""User ID of the Story creator"""
|
||||
|
||||
todo = int
|
||||
"""The number of tasks remaining to be worked on."""
|
||||
|
||||
inprogress = int
|
||||
"""The number of in-progress tasks for this story."""
|
||||
|
||||
review = int
|
||||
"""The number of tasks in review for this story."""
|
||||
|
||||
merged = int
|
||||
"""The number of merged tasks for this story."""
|
||||
|
||||
invalid = int
|
||||
"""The number of invalid tasks for this story."""
|
||||
|
||||
status = unicode
|
||||
"""The derived status of the story, one of 'active', 'merged', 'invalid'"""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
title="Use Storyboard to manage Storyboard",
|
||||
description="We should use Storyboard to manage Storyboard.",
|
||||
is_bug=False,
|
||||
creator_id=1,
|
||||
todo=0,
|
||||
inprogress=1,
|
||||
review=1,
|
||||
merged=0,
|
||||
invalid=0,
|
||||
status="active")
|
||||
|
||||
|
||||
class StoriesController(rest.RestController):
|
||||
"""Manages operations on stories."""
|
||||
|
||||
_custom_actions = {"search": ["GET"]}
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose(Story, int)
|
||||
@wsme_pecan.wsexpose(wmodels.Story, int)
|
||||
def get_one(self, story_id):
|
||||
"""Retrieve details about one story.
|
||||
|
||||
@@ -103,13 +50,13 @@ class StoriesController(rest.RestController):
|
||||
story = stories_api.story_get(story_id)
|
||||
|
||||
if story:
|
||||
return Story.from_db_model(story)
|
||||
return wmodels.Story.from_db_model(story)
|
||||
else:
|
||||
raise ClientSideError("Story %s not found" % story_id,
|
||||
status_code=404)
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([Story], int, int, int, int, unicode, unicode,
|
||||
@wsme_pecan.wsexpose([wmodels.Story], int, int, int, int, unicode, unicode,
|
||||
unicode, unicode, unicode)
|
||||
def get_all(self, project_id=None, assignee_id=None, marker=None,
|
||||
limit=None, status=None, title=None, description=None,
|
||||
@@ -167,10 +114,10 @@ class StoriesController(rest.RestController):
|
||||
if marker_story:
|
||||
response.headers['X-Marker'] = str(marker_story.id)
|
||||
|
||||
return [Story.from_db_model(s) for s in stories]
|
||||
return [wmodels.Story.from_db_model(s) for s in stories]
|
||||
|
||||
@secure(checks.authenticated)
|
||||
@wsme_pecan.wsexpose(Story, body=Story)
|
||||
@wsme_pecan.wsexpose(wmodels.Story, body=wmodels.Story)
|
||||
def post(self, story):
|
||||
"""Create a new story.
|
||||
|
||||
@@ -184,10 +131,10 @@ class StoriesController(rest.RestController):
|
||||
|
||||
events_api.story_created_event(created_story.id, user_id)
|
||||
|
||||
return Story.from_db_model(created_story)
|
||||
return wmodels.Story.from_db_model(created_story)
|
||||
|
||||
@secure(checks.authenticated)
|
||||
@wsme_pecan.wsexpose(Story, int, body=Story)
|
||||
@wsme_pecan.wsexpose(wmodels.Story, int, body=wmodels.Story)
|
||||
def put(self, story_id, story):
|
||||
"""Modify this story.
|
||||
|
||||
@@ -202,13 +149,13 @@ class StoriesController(rest.RestController):
|
||||
user_id = request.current_user_id
|
||||
events_api.story_details_changed_event(story_id, user_id)
|
||||
|
||||
return Story.from_db_model(updated_story)
|
||||
return wmodels.Story.from_db_model(updated_story)
|
||||
else:
|
||||
raise ClientSideError("Story %s not found" % story_id,
|
||||
status_code=404)
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(Story, int)
|
||||
@wsme_pecan.wsexpose(wmodels.Story, int)
|
||||
def delete(self, story_id):
|
||||
"""Delete this story.
|
||||
|
||||
@@ -222,7 +169,7 @@ class StoriesController(rest.RestController):
|
||||
events = TimeLineEventsController()
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([Story], unicode, unicode, int, int)
|
||||
@wsme_pecan.wsexpose([wmodels.Story], unicode, unicode, int, int)
|
||||
def search(self, q="", marker=None, limit=None):
|
||||
"""The search endpoint for stories.
|
||||
|
||||
@@ -234,7 +181,7 @@ class StoriesController(rest.RestController):
|
||||
marker=marker,
|
||||
limit=limit)
|
||||
|
||||
return [Story.from_db_model(story) for story in stories]
|
||||
return [wmodels.Story.from_db_model(story) for story in stories]
|
||||
|
||||
@expose()
|
||||
def _route(self, args, request):
|
||||
|
||||
@@ -19,12 +19,11 @@ from pecan import response
|
||||
from pecan import rest
|
||||
from pecan.secure import secure
|
||||
from wsme.exc import ClientSideError
|
||||
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 base
|
||||
from storyboard.api.v1.search import search_engine
|
||||
from storyboard.api.v1 import wmodels
|
||||
from storyboard.db.api import tasks as tasks_api
|
||||
from storyboard.db.api import timeline_events as events_api
|
||||
|
||||
@@ -33,49 +32,13 @@ CONF = cfg.CONF
|
||||
SEARCH_ENGINE = search_engine.get_engine()
|
||||
|
||||
|
||||
class Task(base.APIBase):
|
||||
"""A Task represents an actionable work item, targeting a specific Project
|
||||
and a specific branch. It is part of a Story. There may be multiple tasks
|
||||
in a story, pointing to different projects or different branches. Each task
|
||||
is generally linked to a code change proposed in Gerrit.
|
||||
"""
|
||||
|
||||
title = wtypes.text
|
||||
"""An optional short label for the task, to show in listings."""
|
||||
|
||||
# TODO(ruhe): replace with enum
|
||||
status = wtypes.text
|
||||
"""Status.
|
||||
Allowed values: ['todo', 'inprogress', 'invalid', 'review', 'merged'].
|
||||
Human readable versions are left to the UI.
|
||||
"""
|
||||
|
||||
is_active = bool
|
||||
"""Is this an active task, or has it been deleted?"""
|
||||
|
||||
creator_id = int
|
||||
"""Id of the User who has created this Task"""
|
||||
|
||||
story_id = int
|
||||
"""The ID of the corresponding Story."""
|
||||
|
||||
project_id = int
|
||||
"""The ID of the corresponding Project."""
|
||||
|
||||
assignee_id = int
|
||||
"""The ID of the invidiual to whom this task is assigned."""
|
||||
|
||||
priority = wtypes.text
|
||||
"""The priority for this task, one of 'low', 'medium', 'high'"""
|
||||
|
||||
|
||||
class TasksController(rest.RestController):
|
||||
"""Manages tasks."""
|
||||
|
||||
_custom_actions = {"search": ["GET"]}
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose(Task, int)
|
||||
@wsme_pecan.wsexpose(wmodels.Task, int)
|
||||
def get_one(self, task_id):
|
||||
"""Retrieve details about one task.
|
||||
|
||||
@@ -84,13 +47,13 @@ class TasksController(rest.RestController):
|
||||
task = tasks_api.task_get(task_id)
|
||||
|
||||
if task:
|
||||
return Task.from_db_model(task)
|
||||
return wmodels.Task.from_db_model(task)
|
||||
else:
|
||||
raise ClientSideError("Task %s not found" % task_id,
|
||||
status_code=404)
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([Task], int, int, int, int, unicode, unicode)
|
||||
@wsme_pecan.wsexpose([wmodels.Task], int, int, int, int, unicode, unicode)
|
||||
def get_all(self, story_id=None, assignee_id=None, marker=None,
|
||||
limit=None, sort_field='id', sort_dir='asc'):
|
||||
"""Retrieve definitions of all of the tasks.
|
||||
@@ -129,10 +92,10 @@ class TasksController(rest.RestController):
|
||||
if marker_task:
|
||||
response.headers['X-Marker'] = str(marker_task.id)
|
||||
|
||||
return [Task.from_db_model(s) for s in tasks]
|
||||
return [wmodels.Task.from_db_model(s) for s in tasks]
|
||||
|
||||
@secure(checks.authenticated)
|
||||
@wsme_pecan.wsexpose(Task, body=Task)
|
||||
@wsme_pecan.wsexpose(wmodels.Task, body=wmodels.Task)
|
||||
def post(self, task):
|
||||
"""Create a new task.
|
||||
|
||||
@@ -149,10 +112,10 @@ class TasksController(rest.RestController):
|
||||
task_title=created_task.title,
|
||||
author_id=creator_id)
|
||||
|
||||
return Task.from_db_model(created_task)
|
||||
return wmodels.Task.from_db_model(created_task)
|
||||
|
||||
@secure(checks.authenticated)
|
||||
@wsme_pecan.wsexpose(Task, int, body=Task)
|
||||
@wsme_pecan.wsexpose(wmodels.Task, int, body=wmodels.Task)
|
||||
def put(self, task_id, task):
|
||||
"""Modify this task.
|
||||
|
||||
@@ -166,7 +129,7 @@ class TasksController(rest.RestController):
|
||||
|
||||
if updated_task:
|
||||
self._post_timeline_events(original_task, updated_task)
|
||||
return Task.from_db_model(updated_task)
|
||||
return wmodels.Task.from_db_model(updated_task)
|
||||
else:
|
||||
raise ClientSideError("Task %s not found" % task_id,
|
||||
status_code=404)
|
||||
@@ -216,7 +179,7 @@ class TasksController(rest.RestController):
|
||||
author_id=author_id)
|
||||
|
||||
@secure(checks.authenticated)
|
||||
@wsme_pecan.wsexpose(Task, int)
|
||||
@wsme_pecan.wsexpose(wmodels.Task, int)
|
||||
def delete(self, task_id):
|
||||
"""Delete this task.
|
||||
|
||||
@@ -235,7 +198,7 @@ class TasksController(rest.RestController):
|
||||
response.status_code = 204
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([Task], unicode, unicode, int, int)
|
||||
@wsme_pecan.wsexpose([wmodels.Task], unicode, unicode, int, int)
|
||||
def search(self, q="", marker=None, limit=None):
|
||||
"""The search endpoint for tasks.
|
||||
|
||||
@@ -247,4 +210,4 @@ class TasksController(rest.RestController):
|
||||
marker=marker,
|
||||
limit=limit)
|
||||
|
||||
return [Task.from_db_model(task) for task in tasks]
|
||||
return [wmodels.Task.from_db_model(task) for task in tasks]
|
||||
|
||||
@@ -19,46 +19,22 @@ from pecan import response
|
||||
from pecan import rest
|
||||
from pecan.secure import secure
|
||||
from wsme.exc import ClientSideError
|
||||
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 base
|
||||
from storyboard.api.v1.users import User
|
||||
from storyboard.common.custom_types import NameType
|
||||
from storyboard.api.v1 import wmodels
|
||||
from storyboard.db.api import teams as teams_api
|
||||
from storyboard.db.api import users as users_api
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class Team(base.APIBase):
|
||||
"""The Team is a group od Users with a fixed set of permissions.
|
||||
"""
|
||||
|
||||
name = NameType()
|
||||
"""The Team unique name. This name will be displayed in the URL.
|
||||
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
|
||||
separators.
|
||||
"""
|
||||
|
||||
description = wtypes.text
|
||||
"""Details about the team.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
name="StoryBoard-core",
|
||||
description="Core reviewers of StoryBoard team.")
|
||||
|
||||
|
||||
class UsersSubcontroller(rest.RestController):
|
||||
"""This controller should be used to list, add or remove users from a Team.
|
||||
"""
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([User], int)
|
||||
@wsme_pecan.wsexpose([wmodels.User], int)
|
||||
def get(self, team_id):
|
||||
"""Get users inside a team.
|
||||
|
||||
@@ -70,17 +46,17 @@ class UsersSubcontroller(rest.RestController):
|
||||
if not team:
|
||||
raise ClientSideError("The requested team does not exist")
|
||||
|
||||
return [User.from_db_model(user) for user in team.users]
|
||||
return [wmodels.User.from_db_model(user) for user in team.users]
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(User, int, int)
|
||||
@wsme_pecan.wsexpose(wmodels.User, int, int)
|
||||
def put(self, team_id, user_id):
|
||||
"""Add a user to a team."""
|
||||
|
||||
teams_api.team_add_user(team_id, user_id)
|
||||
user = users_api.user_get(user_id)
|
||||
|
||||
return User.from_db_model(user)
|
||||
return wmodels.User.from_db_model(user)
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(None, int, int)
|
||||
@@ -95,7 +71,7 @@ class TeamsController(rest.RestController):
|
||||
"""REST controller for Teams."""
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose(Team, int)
|
||||
@wsme_pecan.wsexpose(wmodels.Team, int)
|
||||
def get_one_by_id(self, team_id):
|
||||
"""Retrieve information about the given team.
|
||||
|
||||
@@ -105,29 +81,29 @@ class TeamsController(rest.RestController):
|
||||
team = teams_api.team_get(team_id)
|
||||
|
||||
if team:
|
||||
return Team.from_db_model(team)
|
||||
return wmodels.Team.from_db_model(team)
|
||||
else:
|
||||
raise ClientSideError("Team %s not found" % team_id,
|
||||
status_code=404)
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose(Team, unicode)
|
||||
@wsme_pecan.wsexpose(wmodels.Team, unicode)
|
||||
def get_one_by_name(self, team_name):
|
||||
"""Retrieve information about the given team.
|
||||
|
||||
:param name: team name.
|
||||
:param team_name: team name.
|
||||
"""
|
||||
|
||||
team = teams_api.team_get_by_name(team_name)
|
||||
|
||||
if team:
|
||||
return Team.from_db_model(team)
|
||||
return wmodels.Team.from_db_model(team)
|
||||
else:
|
||||
raise ClientSideError("Team %s not found" % team_name,
|
||||
status_code=404)
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([Team], int, int, unicode, unicode, unicode,
|
||||
@wsme_pecan.wsexpose([wmodels.Team], int, int, unicode, unicode, unicode,
|
||||
unicode)
|
||||
def get(self, marker=None, limit=None, name=None, description=None,
|
||||
sort_field='id', sort_dir='asc'):
|
||||
@@ -164,20 +140,20 @@ class TeamsController(rest.RestController):
|
||||
if marker_team:
|
||||
response.headers['X-Marker'] = str(marker_team.id)
|
||||
|
||||
return [Team.from_db_model(t) for t in teams]
|
||||
return [wmodels.Team.from_db_model(t) for t in teams]
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(Team, body=Team)
|
||||
@wsme_pecan.wsexpose(wmodels.Team, body=wmodels.Team)
|
||||
def post(self, team):
|
||||
"""Create a new team.
|
||||
|
||||
:param team: a team within the request body.
|
||||
"""
|
||||
result = teams_api.team_create(team.as_dict())
|
||||
return Team.from_db_model(result)
|
||||
return wmodels.Team.from_db_model(result)
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(Team, int, body=Team)
|
||||
@wsme_pecan.wsexpose(wmodels.Team, int, body=wmodels.Team)
|
||||
def put(self, team_id, team):
|
||||
"""Modify this team.
|
||||
|
||||
@@ -188,7 +164,7 @@ class TeamsController(rest.RestController):
|
||||
team.as_dict(omit_unset=True))
|
||||
|
||||
if result:
|
||||
return Team.from_db_model(result)
|
||||
return wmodels.Team.from_db_model(result)
|
||||
else:
|
||||
raise ClientSideError("Team %s not found" % team_id,
|
||||
status_code=404)
|
||||
|
||||
@@ -19,13 +19,11 @@ from pecan import response
|
||||
from pecan import rest
|
||||
from pecan.secure import secure
|
||||
from wsme.exc import ClientSideError
|
||||
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 base
|
||||
from storyboard.api.v1.search import search_engine
|
||||
from storyboard.common import event_resolvers
|
||||
from storyboard.api.v1 import wmodels
|
||||
from storyboard.common import event_types
|
||||
from storyboard.db.api import comments as comments_api
|
||||
from storyboard.db.api import timeline_events as events_api
|
||||
@@ -35,87 +33,11 @@ CONF = cfg.CONF
|
||||
SEARCH_ENGINE = search_engine.get_engine()
|
||||
|
||||
|
||||
class Comment(base.APIBase):
|
||||
"""Any user may leave comments for stories. Also comments api is used by
|
||||
gerrit to leave service comments.
|
||||
"""
|
||||
|
||||
content = wtypes.text
|
||||
"""The content of the comment."""
|
||||
|
||||
is_active = bool
|
||||
"""Is this an active comment, or has it been deleted?"""
|
||||
|
||||
|
||||
class TimeLineEvent(base.APIBase):
|
||||
"""An event object should be created each time a story or a task state
|
||||
changes.
|
||||
"""
|
||||
|
||||
event_type = wtypes.text
|
||||
"""This type should serve as a hint for the web-client when rendering
|
||||
a comment."""
|
||||
|
||||
event_info = wtypes.text
|
||||
"""A JSON encoded field with details about the event."""
|
||||
|
||||
story_id = int
|
||||
"""The ID of the corresponding Story."""
|
||||
|
||||
author_id = int
|
||||
"""The ID of User who has left the comment."""
|
||||
|
||||
comment_id = int
|
||||
"""The id of a comment linked to this event."""
|
||||
|
||||
comment = Comment
|
||||
"""The resolved comment."""
|
||||
|
||||
@staticmethod
|
||||
def resolve_event_values(event):
|
||||
if event.comment_id:
|
||||
comment = comments_api.comment_get(event.comment_id)
|
||||
event.comment = Comment.from_db_model(comment)
|
||||
|
||||
event = TimeLineEvent._resolve_info(event)
|
||||
|
||||
return event
|
||||
|
||||
@staticmethod
|
||||
def _resolve_info(event):
|
||||
if event.event_type == event_types.STORY_CREATED:
|
||||
return event_resolvers.story_created(event)
|
||||
|
||||
elif event.event_type == event_types.STORY_DETAILS_CHANGED:
|
||||
return event_resolvers.story_details_changed(event)
|
||||
|
||||
elif event.event_type == event_types.USER_COMMENT:
|
||||
return event_resolvers.user_comment(event)
|
||||
|
||||
elif event.event_type == event_types.TASK_CREATED:
|
||||
return event_resolvers.task_created(event)
|
||||
|
||||
elif event.event_type == event_types.TASK_STATUS_CHANGED:
|
||||
return event_resolvers.task_status_changed(event)
|
||||
|
||||
elif event.event_type == event_types.TASK_PRIORITY_CHANGED:
|
||||
return event_resolvers.task_priority_changed(event)
|
||||
|
||||
elif event.event_type == event_types.TASK_ASSIGNEE_CHANGED:
|
||||
return event_resolvers.task_assignee_changed(event)
|
||||
|
||||
elif event.event_type == event_types.TASK_DETAILS_CHANGED:
|
||||
return event_resolvers.task_details_changed(event)
|
||||
|
||||
elif event.event_type == event_types.TASK_DELETED:
|
||||
return event_resolvers.task_deleted(event)
|
||||
|
||||
|
||||
class TimeLineEventsController(rest.RestController):
|
||||
"""Manages comments."""
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose(TimeLineEvent, int, int)
|
||||
@wsme_pecan.wsexpose(wmodels.TimeLineEvent, int, int)
|
||||
def get_one(self, story_id, event_id):
|
||||
"""Retrieve details about one event.
|
||||
|
||||
@@ -127,15 +49,16 @@ class TimeLineEventsController(rest.RestController):
|
||||
event = events_api.event_get(event_id)
|
||||
|
||||
if event:
|
||||
wsme_event = TimeLineEvent.from_db_model(event)
|
||||
wsme_event = TimeLineEvent.resolve_event_values(wsme_event)
|
||||
wsme_event = wmodels.TimeLineEvent.from_db_model(event)
|
||||
wsme_event = wmodels.TimeLineEvent.resolve_event_values(wsme_event)
|
||||
return wsme_event
|
||||
else:
|
||||
raise ClientSideError("Comment %s not found" % event_id,
|
||||
status_code=404)
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([TimeLineEvent], int, int, int, unicode, unicode)
|
||||
@wsme_pecan.wsexpose([wmodels.TimeLineEvent], int, int, int, unicode,
|
||||
unicode)
|
||||
def get_all(self, story_id=None, marker=None, limit=None, sort_field=None,
|
||||
sort_dir=None):
|
||||
"""Retrieve all events that have happened under specified story.
|
||||
@@ -168,15 +91,15 @@ class TimeLineEventsController(rest.RestController):
|
||||
if marker_event:
|
||||
response.headers['X-Marker'] = str(marker_event.id)
|
||||
|
||||
return [TimeLineEvent.resolve_event_values(
|
||||
TimeLineEvent.from_db_model(event)) for event in events]
|
||||
return [wmodels.TimeLineEvent.resolve_event_values(
|
||||
wmodels.TimeLineEvent.from_db_model(event)) for event in events]
|
||||
|
||||
|
||||
class CommentsController(rest.RestController):
|
||||
"""Manages comments."""
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose(Comment, int, int)
|
||||
@wsme_pecan.wsexpose(wmodels.Comment, int, int)
|
||||
def get_one(self, story_id, comment_id):
|
||||
"""Retrieve details about one comment.
|
||||
|
||||
@@ -188,13 +111,13 @@ class CommentsController(rest.RestController):
|
||||
comment = comments_api.comment_get(comment_id)
|
||||
|
||||
if comment:
|
||||
return Comment.from_db_model(comment)
|
||||
return wmodels.Comment.from_db_model(comment)
|
||||
else:
|
||||
raise ClientSideError("Comment %s not found" % comment_id,
|
||||
status_code=404)
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([Comment], int, int, int, unicode, unicode)
|
||||
@wsme_pecan.wsexpose([wmodels.Comment], int, int, int, unicode, unicode)
|
||||
def get_all(self, story_id=None, marker=None, limit=None, sort_field='id',
|
||||
sort_dir='asc'):
|
||||
"""Retrieve all comments posted under specified story.
|
||||
@@ -238,10 +161,10 @@ class CommentsController(rest.RestController):
|
||||
if marker_event:
|
||||
response.headers['X-Marker'] = str(marker)
|
||||
|
||||
return [Comment.from_db_model(comment) for comment in comments]
|
||||
return [wmodels.Comment.from_db_model(comment) for comment in comments]
|
||||
|
||||
@secure(checks.authenticated)
|
||||
@wsme_pecan.wsexpose(TimeLineEvent, int, body=Comment)
|
||||
@wsme_pecan.wsexpose(wmodels.TimeLineEvent, int, body=wmodels.Comment)
|
||||
def post(self, story_id, comment):
|
||||
"""Create a new comment.
|
||||
|
||||
@@ -257,13 +180,13 @@ class CommentsController(rest.RestController):
|
||||
"event_type": event_types.USER_COMMENT,
|
||||
"comment_id": created_comment.id
|
||||
}
|
||||
event = TimeLineEvent.from_db_model(
|
||||
event = wmodels.TimeLineEvent.from_db_model(
|
||||
events_api.event_create(event_values))
|
||||
event = TimeLineEvent.resolve_event_values(event)
|
||||
event = wmodels.TimeLineEvent.resolve_event_values(event)
|
||||
return event
|
||||
|
||||
@secure(checks.authenticated)
|
||||
@wsme_pecan.wsexpose(Comment, int, int, body=Comment)
|
||||
@wsme_pecan.wsexpose(wmodels.Comment, int, int, body=wmodels.Comment)
|
||||
def put(self, story_id, comment_id, comment_body):
|
||||
"""Update an existing comment.
|
||||
|
||||
@@ -282,10 +205,10 @@ class CommentsController(rest.RestController):
|
||||
updated_comment = comments_api.comment_update(comment_id,
|
||||
comment_body.as_dict())
|
||||
|
||||
return Comment.from_db_model(updated_comment)
|
||||
return wmodels.Comment.from_db_model(updated_comment)
|
||||
|
||||
@secure(checks.authenticated)
|
||||
@wsme_pecan.wsexpose(Comment, int, int)
|
||||
@wsme_pecan.wsexpose(wmodels.Comment, int, int)
|
||||
def delete(self, story_id, comment_id):
|
||||
"""Update an existing comment.
|
||||
|
||||
@@ -305,7 +228,7 @@ class CommentsController(rest.RestController):
|
||||
return response
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([Comment], unicode, unicode, int, int)
|
||||
@wsme_pecan.wsexpose([wmodels.Comment], unicode, unicode, int, int)
|
||||
def search(self, q="", marker=None, limit=None):
|
||||
"""The search endpoint for comments.
|
||||
|
||||
@@ -317,4 +240,4 @@ class CommentsController(rest.RestController):
|
||||
marker=marker,
|
||||
limit=limit)
|
||||
|
||||
return [Comment.from_db_model(comment) for comment in comments]
|
||||
return [wmodels.Comment.from_db_model(comment) for comment in comments]
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from oslo.config import cfg
|
||||
from pecan import expose
|
||||
from pecan import request
|
||||
@@ -22,12 +20,11 @@ from pecan import response
|
||||
from pecan import rest
|
||||
from pecan.secure import secure
|
||||
from wsme.exc import ClientSideError
|
||||
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 base
|
||||
from storyboard.api.v1.search import search_engine
|
||||
from storyboard.api.v1 import wmodels
|
||||
from storyboard.db.api import users as users_api
|
||||
|
||||
CONF = cfg.CONF
|
||||
@@ -35,48 +32,14 @@ CONF = cfg.CONF
|
||||
SEARCH_ENGINE = search_engine.get_engine()
|
||||
|
||||
|
||||
class User(base.APIBase):
|
||||
"""Represents a user."""
|
||||
|
||||
username = wtypes.text
|
||||
"""A short unique name, beginning with a lower-case letter or number, and
|
||||
containing only letters, numbers, dots, hyphens, or plus signs"""
|
||||
|
||||
full_name = wtypes.text
|
||||
"""Full (Display) name."""
|
||||
|
||||
openid = wtypes.text
|
||||
"""The unique identifier, returned by an OpneId provider"""
|
||||
|
||||
email = wtypes.text
|
||||
"""Email Address."""
|
||||
|
||||
# Todo(nkonovalov): use teams to define superusers
|
||||
is_superuser = bool
|
||||
|
||||
last_login = datetime
|
||||
"""Date of the last login."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
username="elbarto",
|
||||
full_name="Bart Simpson",
|
||||
openid="https://login.launchpad.net/+id/Abacaba",
|
||||
email="skinnerstinks@springfield.net",
|
||||
is_staff=False,
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
last_login=datetime(2014, 1, 1, 16, 42))
|
||||
|
||||
|
||||
class UsersController(rest.RestController):
|
||||
"""Manages users."""
|
||||
|
||||
_custom_actions = {"search": ["GET"]}
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([User], int, int, unicode, unicode, unicode, unicode)
|
||||
@wsme_pecan.wsexpose([wmodels.User], int, int, unicode, unicode, unicode,
|
||||
unicode)
|
||||
def get(self, marker=None, limit=None, username=None, full_name=None,
|
||||
sort_field='id', sort_dir='asc'):
|
||||
"""Page and filter the users in storyboard.
|
||||
@@ -111,10 +74,10 @@ class UsersController(rest.RestController):
|
||||
if marker_user:
|
||||
response.headers['X-Marker'] = str(marker_user.id)
|
||||
|
||||
return [User.from_db_model(u) for u in users]
|
||||
return [wmodels.User.from_db_model(u) for u in users]
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose(User, int)
|
||||
@wsme_pecan.wsexpose(wmodels.User, int)
|
||||
def get_one(self, user_id):
|
||||
"""Retrieve details about one user.
|
||||
|
||||
@@ -132,7 +95,7 @@ class UsersController(rest.RestController):
|
||||
return user
|
||||
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(User, body=User)
|
||||
@wsme_pecan.wsexpose(wmodels.User, body=wmodels.User)
|
||||
def post(self, user):
|
||||
"""Create a new user.
|
||||
|
||||
@@ -140,10 +103,10 @@ class UsersController(rest.RestController):
|
||||
"""
|
||||
|
||||
created_user = users_api.user_create(user.as_dict())
|
||||
return User.from_db_model(created_user)
|
||||
return wmodels.User.from_db_model(created_user)
|
||||
|
||||
@secure(checks.authenticated)
|
||||
@wsme_pecan.wsexpose(User, int, body=User)
|
||||
@wsme_pecan.wsexpose(wmodels.User, int, body=wmodels.User)
|
||||
def put(self, user_id, user):
|
||||
"""Modify this user.
|
||||
|
||||
@@ -163,10 +126,10 @@ class UsersController(rest.RestController):
|
||||
return response
|
||||
|
||||
updated_user = users_api.user_update(user_id, user_dict)
|
||||
return User.from_db_model(updated_user)
|
||||
return wmodels.User.from_db_model(updated_user)
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([User], unicode, unicode, int, int)
|
||||
@wsme_pecan.wsexpose([wmodels.User], unicode, unicode, int, int)
|
||||
def search(self, q="", marker=None, limit=None):
|
||||
"""The search endpoint for users.
|
||||
|
||||
@@ -176,7 +139,7 @@ class UsersController(rest.RestController):
|
||||
|
||||
users = SEARCH_ENGINE.users_query(q=q, marker=marker, limit=limit)
|
||||
|
||||
return [User.from_db_model(u) for u in users]
|
||||
return [wmodels.User.from_db_model(u) for u in users]
|
||||
|
||||
@expose()
|
||||
def _route(self, args, request):
|
||||
|
||||
295
storyboard/api/v1/wmodels.py
Normal file
295
storyboard/api/v1/wmodels.py
Normal file
@@ -0,0 +1,295 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
#
|
||||
# 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 datetime import datetime
|
||||
|
||||
from wsme import types as wtypes
|
||||
|
||||
from storyboard.api.v1 import base
|
||||
from storyboard.common.custom_types import NameType
|
||||
from storyboard.common import event_resolvers
|
||||
from storyboard.common import event_types
|
||||
from storyboard.db.api import comments as comments_api
|
||||
|
||||
|
||||
class Comment(base.APIBase):
|
||||
"""Any user may leave comments for stories. Also comments api is used by
|
||||
gerrit to leave service comments.
|
||||
"""
|
||||
|
||||
content = wtypes.text
|
||||
"""The content of the comment."""
|
||||
|
||||
is_active = bool
|
||||
"""Is this an active comment, or has it been deleted?"""
|
||||
|
||||
|
||||
class Project(base.APIBase):
|
||||
"""The Storyboard Registry describes the open source world as ProjectGroups
|
||||
and Projects. Each ProjectGroup may be responsible for several Projects.
|
||||
For example, the OpenStack Infrastructure ProjectGroup has Zuul, Nodepool,
|
||||
Storyboard as Projects, among others.
|
||||
"""
|
||||
|
||||
name = NameType()
|
||||
"""The Project unique name. This name will be displayed in the URL.
|
||||
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
|
||||
separators.
|
||||
"""
|
||||
|
||||
description = wtypes.text
|
||||
"""Details about the project's work, highlights, goals, and how to
|
||||
contribute. Use plain text, paragraphs are preserved and URLs are
|
||||
linked in pages.
|
||||
"""
|
||||
|
||||
is_active = bool
|
||||
"""Is this an active project, or has it been deleted?"""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
name="StoryBoard",
|
||||
description="This is an awesome project.",
|
||||
is_active=True)
|
||||
|
||||
|
||||
class ProjectGroup(base.APIBase):
|
||||
"""Represents a group of projects."""
|
||||
|
||||
name = NameType()
|
||||
"""The Project Group unique name. This name will be displayed in the URL.
|
||||
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
|
||||
separators.
|
||||
"""
|
||||
|
||||
title = wtypes.text
|
||||
"""The full name of the project group, which can contain spaces, special
|
||||
characters, etc.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
name="Infra",
|
||||
title="Awesome projects")
|
||||
|
||||
|
||||
class Story(base.APIBase):
|
||||
"""The Story is the main element of StoryBoard. It represents a user story
|
||||
(generally a bugfix or a feature) that needs to be implemented. It will be
|
||||
broken down into a series of Tasks, which will each target a specific
|
||||
Project and branch.
|
||||
"""
|
||||
|
||||
title = wtypes.text
|
||||
"""A descriptive label for the story, to show in listings."""
|
||||
|
||||
description = wtypes.text
|
||||
"""A complete description of the goal this story wants to cover."""
|
||||
|
||||
is_bug = bool
|
||||
"""Is this a bug or a feature :)"""
|
||||
|
||||
creator_id = int
|
||||
"""User ID of the Story creator"""
|
||||
|
||||
todo = int
|
||||
"""The number of tasks remaining to be worked on."""
|
||||
|
||||
inprogress = int
|
||||
"""The number of in-progress tasks for this story."""
|
||||
|
||||
review = int
|
||||
"""The number of tasks in review for this story."""
|
||||
|
||||
merged = int
|
||||
"""The number of merged tasks for this story."""
|
||||
|
||||
invalid = int
|
||||
"""The number of invalid tasks for this story."""
|
||||
|
||||
status = unicode
|
||||
"""The derived status of the story, one of 'active', 'merged', 'invalid'"""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
title="Use Storyboard to manage Storyboard",
|
||||
description="We should use Storyboard to manage Storyboard.",
|
||||
is_bug=False,
|
||||
creator_id=1,
|
||||
todo=0,
|
||||
inprogress=1,
|
||||
review=1,
|
||||
merged=0,
|
||||
invalid=0,
|
||||
status="active")
|
||||
|
||||
|
||||
class Task(base.APIBase):
|
||||
"""A Task represents an actionable work item, targeting a specific Project
|
||||
and a specific branch. It is part of a Story. There may be multiple tasks
|
||||
in a story, pointing to different projects or different branches. Each task
|
||||
is generally linked to a code change proposed in Gerrit.
|
||||
"""
|
||||
|
||||
title = wtypes.text
|
||||
"""An optional short label for the task, to show in listings."""
|
||||
|
||||
# TODO(ruhe): replace with enum
|
||||
status = wtypes.text
|
||||
"""Status.
|
||||
Allowed values: ['todo', 'inprogress', 'invalid', 'review', 'merged'].
|
||||
Human readable versions are left to the UI.
|
||||
"""
|
||||
|
||||
is_active = bool
|
||||
"""Is this an active task, or has it been deleted?"""
|
||||
|
||||
creator_id = int
|
||||
"""Id of the User who has created this Task"""
|
||||
|
||||
story_id = int
|
||||
"""The ID of the corresponding Story."""
|
||||
|
||||
project_id = int
|
||||
"""The ID of the corresponding Project."""
|
||||
|
||||
assignee_id = int
|
||||
"""The ID of the invidiual to whom this task is assigned."""
|
||||
|
||||
priority = wtypes.text
|
||||
"""The priority for this task, one of 'low', 'medium', 'high'"""
|
||||
|
||||
|
||||
class Team(base.APIBase):
|
||||
"""The Team is a group od Users with a fixed set of permissions.
|
||||
"""
|
||||
|
||||
name = NameType()
|
||||
"""The Team unique name. This name will be displayed in the URL.
|
||||
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
|
||||
separators.
|
||||
"""
|
||||
|
||||
description = wtypes.text
|
||||
"""Details about the team.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
name="StoryBoard-core",
|
||||
description="Core reviewers of StoryBoard team.")
|
||||
|
||||
|
||||
class TimeLineEvent(base.APIBase):
|
||||
"""An event object should be created each time a story or a task state
|
||||
changes.
|
||||
"""
|
||||
|
||||
event_type = wtypes.text
|
||||
"""This type should serve as a hint for the web-client when rendering
|
||||
a comment."""
|
||||
|
||||
event_info = wtypes.text
|
||||
"""A JSON encoded field with details about the event."""
|
||||
|
||||
story_id = int
|
||||
"""The ID of the corresponding Story."""
|
||||
|
||||
author_id = int
|
||||
"""The ID of User who has left the comment."""
|
||||
|
||||
comment_id = int
|
||||
"""The id of a comment linked to this event."""
|
||||
|
||||
comment = Comment
|
||||
"""The resolved comment."""
|
||||
|
||||
@staticmethod
|
||||
def resolve_event_values(event):
|
||||
if event.comment_id:
|
||||
comment = comments_api.comment_get(event.comment_id)
|
||||
event.comment = Comment.from_db_model(comment)
|
||||
|
||||
event = TimeLineEvent._resolve_info(event)
|
||||
|
||||
return event
|
||||
|
||||
@staticmethod
|
||||
def _resolve_info(event):
|
||||
if event.event_type == event_types.STORY_CREATED:
|
||||
return event_resolvers.story_created(event)
|
||||
|
||||
elif event.event_type == event_types.STORY_DETAILS_CHANGED:
|
||||
return event_resolvers.story_details_changed(event)
|
||||
|
||||
elif event.event_type == event_types.USER_COMMENT:
|
||||
return event_resolvers.user_comment(event)
|
||||
|
||||
elif event.event_type == event_types.TASK_CREATED:
|
||||
return event_resolvers.task_created(event)
|
||||
|
||||
elif event.event_type == event_types.TASK_STATUS_CHANGED:
|
||||
return event_resolvers.task_status_changed(event)
|
||||
|
||||
elif event.event_type == event_types.TASK_PRIORITY_CHANGED:
|
||||
return event_resolvers.task_priority_changed(event)
|
||||
|
||||
elif event.event_type == event_types.TASK_ASSIGNEE_CHANGED:
|
||||
return event_resolvers.task_assignee_changed(event)
|
||||
|
||||
elif event.event_type == event_types.TASK_DETAILS_CHANGED:
|
||||
return event_resolvers.task_details_changed(event)
|
||||
|
||||
elif event.event_type == event_types.TASK_DELETED:
|
||||
return event_resolvers.task_deleted(event)
|
||||
|
||||
|
||||
class User(base.APIBase):
|
||||
"""Represents a user."""
|
||||
|
||||
username = wtypes.text
|
||||
"""A short unique name, beginning with a lower-case letter or number, and
|
||||
containing only letters, numbers, dots, hyphens, or plus signs"""
|
||||
|
||||
full_name = wtypes.text
|
||||
"""Full (Display) name."""
|
||||
|
||||
openid = wtypes.text
|
||||
"""The unique identifier, returned by an OpneId provider"""
|
||||
|
||||
email = wtypes.text
|
||||
"""Email Address."""
|
||||
|
||||
# Todo(nkonovalov): use teams to define superusers
|
||||
is_superuser = bool
|
||||
|
||||
last_login = datetime
|
||||
"""Date of the last login."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
username="elbarto",
|
||||
full_name="Bart Simpson",
|
||||
openid="https://login.launchpad.net/+id/Abacaba",
|
||||
email="skinnerstinks@springfield.net",
|
||||
is_staff=False,
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
last_login=datetime(2014, 1, 1, 16, 42))
|
||||
Reference in New Issue
Block a user