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:
Nikita Konovalov
2014-08-14 16:52:24 +04:00
parent 01a4af2805
commit e60f499445
8 changed files with 395 additions and 384 deletions

View File

@@ -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.

View File

@@ -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):

View File

@@ -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):

View File

@@ -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]

View File

@@ -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)

View File

@@ -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]

View File

@@ -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):

View 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))