Added branches to storyboard
Added three new migrations: autocreate_branches column added to projects; added branches table; branch_id column added to tasks. To db api added branches. Added branches controller. Added tests for patch. Change-Id: I50008dc8a2dcb07bea4015d27b702994fccff0a8
This commit is contained in:
parent
def7fa9f75
commit
eaa59426d5
156
storyboard/api/v1/branches.py
Normal file
156
storyboard/api/v1/branches.py
Normal file
@ -0,0 +1,156 @@
|
||||
# Copyright (c) 2015 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
|
||||
import pytz
|
||||
|
||||
from oslo.config import cfg
|
||||
from pecan import abort
|
||||
from pecan import response
|
||||
from pecan import rest
|
||||
from pecan.secure import secure
|
||||
import six
|
||||
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.search import search_engine
|
||||
from storyboard.api.v1 import validations
|
||||
from storyboard.api.v1 import wmodels
|
||||
from storyboard.common import decorators
|
||||
from storyboard.db.api import branches as branches_api
|
||||
from storyboard.openstack.common.gettextutils import _ # noqa
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
SEARCH_ENGINE = search_engine.get_engine()
|
||||
|
||||
|
||||
class BranchesController(rest.RestController):
|
||||
"""REST controller for branches.
|
||||
"""
|
||||
|
||||
_custom_actions = {"search": ["GET"]}
|
||||
|
||||
validation_post_schema = validations.BRANCHES_POST_SCHEMA
|
||||
validation_put_schema = validations.BRANCHES_PUT_SCHEMA
|
||||
|
||||
@decorators.db_exceptions
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose(wmodels.Branch, int)
|
||||
def get_one(self, branch_id):
|
||||
"""Retrieve information about the given branch.
|
||||
|
||||
:param branch_id: branch ID.
|
||||
"""
|
||||
|
||||
branch = branches_api.branch_get(branch_id)
|
||||
|
||||
if branch:
|
||||
return wmodels.Branch.from_db_model(branch)
|
||||
else:
|
||||
abort(404, _("Branch %s not found") % branch_id)
|
||||
|
||||
@decorators.db_exceptions
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([wmodels.Branch], int, int, wtypes.text, int, int,
|
||||
wtypes.text, wtypes.text)
|
||||
def get_all(self, marker=None, limit=None, name=None, project_id=None,
|
||||
project_group_id=None, sort_field='id', sort_dir='asc'):
|
||||
"""Retrieve a list of branches.
|
||||
|
||||
:param marker: The resource id where the page should begin.
|
||||
:param limit: The number of branches to retrieve.
|
||||
:param name: Filter branches based on name.
|
||||
:param project_id: Filter branches based on project.
|
||||
:param project_group_id: Filter branches based on project group.
|
||||
: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_branch = branches_api.branch_get(marker)
|
||||
|
||||
branches = \
|
||||
branches_api.branch_get_all(marker=marker_branch,
|
||||
limit=limit,
|
||||
name=name,
|
||||
project_id=project_id,
|
||||
project_group_id=project_group_id,
|
||||
sort_field=sort_field,
|
||||
sort_dir=sort_dir)
|
||||
branches_count = \
|
||||
branches_api.branch_get_count(name=name,
|
||||
project_id=project_id,
|
||||
project_group_id=project_group_id)
|
||||
|
||||
# Apply the query response headers.
|
||||
response.headers['X-Limit'] = str(limit)
|
||||
response.headers['X-Total'] = str(branches_count)
|
||||
if marker_branch:
|
||||
response.headers['X-Marker'] = str(marker_branch.id)
|
||||
|
||||
return [wmodels.Branch.from_db_model(b) for b in branches]
|
||||
|
||||
@decorators.db_exceptions
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(wmodels.Branch, body=wmodels.Branch)
|
||||
def post(self, branch):
|
||||
"""Create a new branch.
|
||||
|
||||
:param branch: a branch within the request body.
|
||||
"""
|
||||
|
||||
branch_dict = branch.as_dict()
|
||||
|
||||
# we can't create expired branch
|
||||
if branch.expiration_date or branch.expired:
|
||||
abort(400, _("Can't create expired branch."))
|
||||
|
||||
result = branches_api.branch_create(branch_dict)
|
||||
return wmodels.Branch.from_db_model(result)
|
||||
|
||||
@decorators.db_exceptions
|
||||
@secure(checks.superuser)
|
||||
@wsme_pecan.wsexpose(wmodels.Branch, int, body=wmodels.Branch)
|
||||
def put(self, branch_id, branch):
|
||||
"""Modify this branch.
|
||||
|
||||
:param branch_id: An ID of the branch.
|
||||
:param branch: a branch within the request body.
|
||||
"""
|
||||
|
||||
branch_dict = branch.as_dict(omit_unset=True)
|
||||
|
||||
if "expiration_date" in six.iterkeys(branch_dict):
|
||||
abort(400, _("Can't change expiration date."))
|
||||
|
||||
if "expired" in six.iterkeys(branch_dict):
|
||||
if branch_dict["expired"]:
|
||||
branch_dict["expiration_date"] = datetime.now(tz=pytz.utc)
|
||||
else:
|
||||
branch_dict["expiration_date"] = None
|
||||
|
||||
result = branches_api.branch_update(branch_id, branch_dict)
|
||||
|
||||
if result:
|
||||
return wmodels.Branch.from_db_model(result)
|
||||
else:
|
||||
abort(404, _("Branch %s not found") % branch_id)
|
@ -62,13 +62,13 @@ class TasksController(rest.RestController):
|
||||
|
||||
@decorators.db_exceptions
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([wmodels.Task], wtypes.text, int, int, int, int,
|
||||
@wsme_pecan.wsexpose([wmodels.Task], wtypes.text, int, int, int, int, int,
|
||||
[wtypes.text], [wtypes.text], int, int, wtypes.text,
|
||||
wtypes.text)
|
||||
def get_all(self, title=None, story_id=None, assignee_id=None,
|
||||
project_id=None, project_group_id=None, status=None,
|
||||
priority=None, marker=None, limit=None, sort_field='id',
|
||||
sort_dir='asc'):
|
||||
project_id=None, project_group_id=None, branch_id=None,
|
||||
status=None, priority=None, marker=None, limit=None,
|
||||
sort_field='id', sort_dir='asc'):
|
||||
"""Retrieve definitions of all of the tasks.
|
||||
|
||||
:param title: search by task title.
|
||||
@ -76,6 +76,7 @@ class TasksController(rest.RestController):
|
||||
: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 status: filter tasks by status.
|
||||
:param priority: filter tasks by priority.
|
||||
:param marker: The resource id where the page should begin.
|
||||
@ -98,6 +99,7 @@ class TasksController(rest.RestController):
|
||||
assignee_id=assignee_id,
|
||||
project_id=project_id,
|
||||
project_group_id=project_group_id,
|
||||
branch_id=branch_id,
|
||||
status=status,
|
||||
priority=priority,
|
||||
sort_field=sort_field,
|
||||
@ -110,6 +112,7 @@ class TasksController(rest.RestController):
|
||||
assignee_id=assignee_id,
|
||||
project_id=project_id,
|
||||
project_group_id=project_group_id,
|
||||
branch_id=branch_id,
|
||||
status=status,
|
||||
priority=priority)
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
from storyboard.api.v1.auth import AuthController
|
||||
from storyboard.api.v1.branches import BranchesController
|
||||
from storyboard.api.v1.project_groups import ProjectGroupsController
|
||||
from storyboard.api.v1.projects import ProjectsController
|
||||
from storyboard.api.v1.stories import StoriesController
|
||||
@ -33,6 +34,7 @@ class V1Controller(object):
|
||||
projects = ProjectsController()
|
||||
users = UsersController()
|
||||
teams = TeamsController()
|
||||
branches = BranchesController()
|
||||
stories = StoriesController()
|
||||
tags = TagsController()
|
||||
tasks = TasksController()
|
||||
|
@ -166,6 +166,21 @@ TASKS_PUT_SCHEMA = {
|
||||
TASKS_POST_SCHEMA = copy.deepcopy(TASKS_PUT_SCHEMA)
|
||||
TASKS_POST_SCHEMA["required"] = ["title"]
|
||||
|
||||
BRANCHES_PUT_SCHEMA = {
|
||||
"name": "branch_schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": CommonLength.lower_middle_length,
|
||||
"maxLength": CommonLength.top_middle_length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BRANCHES_POST_SCHEMA = copy.deepcopy(BRANCHES_PUT_SCHEMA)
|
||||
BRANCHES_POST_SCHEMA["required"] = ["name"]
|
||||
|
||||
STORY_TAGS_PUT_SCHEMA = {
|
||||
"name": "storyTag_schema",
|
||||
"type": "object",
|
||||
|
@ -74,6 +74,11 @@ class Project(base.APIBase):
|
||||
repo_url = wtypes.text
|
||||
"""This is a repo link for this project"""
|
||||
|
||||
autocreate_branches = bool
|
||||
"""This flag means that storyboard will try to create task branches
|
||||
automatically from the branches declared in the code repository.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
@ -195,6 +200,43 @@ class Task(base.APIBase):
|
||||
priority = wtypes.text
|
||||
"""The priority for this task, one of 'low', 'medium', 'high'"""
|
||||
|
||||
branch_id = int
|
||||
"""The ID of corresponding Branch"""
|
||||
|
||||
|
||||
class Branch(base.APIBase):
|
||||
"""Represents a branch."""
|
||||
|
||||
name = wtypes.text
|
||||
"""The branch unique name. This name will be displayed in the URL.
|
||||
At least 3 alphanumeric symbols.
|
||||
"""
|
||||
|
||||
project_id = int
|
||||
"""The ID of the corresponding Project."""
|
||||
|
||||
expired = bool
|
||||
"""A binary flag that marks branches that should no longer be
|
||||
selectable in tasks."""
|
||||
|
||||
expiration_date = datetime
|
||||
"""Last date the expired flag was switched to True."""
|
||||
|
||||
autocreated = bool
|
||||
"""A flag that marks autocreated entries, so that they can
|
||||
be auto-expired when the corresponding branch is deleted in the git repo.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
name="Storyboard-branch",
|
||||
project_id=1,
|
||||
expired=True,
|
||||
expiration_date=datetime(2015, 1, 1, 1, 1),
|
||||
autocreated=False
|
||||
)
|
||||
|
||||
|
||||
class Team(base.APIBase):
|
||||
"""The Team is a group od Users with a fixed set of permissions.
|
||||
|
36
storyboard/common/master_branch_helper.py
Normal file
36
storyboard/common/master_branch_helper.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Copyright (c) 2015 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.
|
||||
|
||||
|
||||
class MasterBranchHelper:
|
||||
name = "master"
|
||||
project_id = None
|
||||
expired = False
|
||||
expiration_date = None
|
||||
autocreated = False
|
||||
|
||||
def __init__(self, project_id):
|
||||
self.project_id = project_id
|
||||
|
||||
def as_dict(self):
|
||||
master_branch_dict = {
|
||||
"name": self.name,
|
||||
"project_id": self.project_id,
|
||||
"expired": self.expired,
|
||||
"expiration_date": self.expiration_date,
|
||||
"autocreated": self.autocreated
|
||||
}
|
||||
|
||||
return master_branch_dict
|
69
storyboard/db/api/branches.py
Normal file
69
storyboard/db/api/branches.py
Normal file
@ -0,0 +1,69 @@
|
||||
# Copyright (c) 2015 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 storyboard.db.api import base as api_base
|
||||
from storyboard.db import models
|
||||
|
||||
|
||||
def branch_get(branch_id):
|
||||
return api_base.entity_get(models.Branch, branch_id)
|
||||
|
||||
|
||||
def branch_get_all(marker=None, limit=None, sort_field=None, sort_dir=None,
|
||||
project_group_id=None, **kwargs):
|
||||
if not sort_field:
|
||||
sort_field = 'id'
|
||||
if not sort_dir:
|
||||
sort_dir = 'asc'
|
||||
|
||||
query = branch_build_query(project_group_id=project_group_id,
|
||||
**kwargs)
|
||||
|
||||
query = api_base.paginate_query(query=query,
|
||||
model=models.Branch,
|
||||
limit=limit,
|
||||
sort_key=sort_field,
|
||||
marker=marker,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
return query.all()
|
||||
|
||||
|
||||
def branch_get_count(project_group_id=None, **kwargs):
|
||||
query = branch_build_query(project_group_id=project_group_id,
|
||||
**kwargs)
|
||||
|
||||
return query.count()
|
||||
|
||||
|
||||
def branch_create(values):
|
||||
return api_base.entity_create(models.Branch, values)
|
||||
|
||||
|
||||
def branch_update(branch_id, values):
|
||||
return api_base.entity_update(models.Branch, branch_id, values)
|
||||
|
||||
|
||||
def branch_build_query(project_group_id, **kwargs):
|
||||
query = api_base.model_query(models.Branch)
|
||||
|
||||
if project_group_id:
|
||||
query = query.join(models.Project.project_groups) \
|
||||
.filter(models.ProjectGroup.id == project_group_id)
|
||||
|
||||
query = api_base.apply_query_filters(query=query, model=models.Branch,
|
||||
**kwargs)
|
||||
|
||||
return query
|
@ -13,7 +13,9 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from storyboard.common.master_branch_helper import MasterBranchHelper
|
||||
from storyboard.db.api import base as api_base
|
||||
from storyboard.db.api import branches as branches_api
|
||||
from storyboard.db import models
|
||||
|
||||
|
||||
@ -58,7 +60,11 @@ def project_get_count(project_group_id=None, **kwargs):
|
||||
|
||||
|
||||
def project_create(values):
|
||||
return api_base.entity_create(models.Project, values)
|
||||
# Create project and 'master' branch for him
|
||||
project = api_base.entity_create(models.Project, values)
|
||||
master_branch = MasterBranchHelper(project["id"])
|
||||
branches_api.branch_create(master_branch.as_dict())
|
||||
return project
|
||||
|
||||
|
||||
def project_update(project_id, values):
|
||||
|
@ -0,0 +1,51 @@
|
||||
# Copyright (c) 2015 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.
|
||||
#
|
||||
|
||||
"""This migration adds new column for autocreate branches and makes it 'false'
|
||||
in all projects in database.
|
||||
|
||||
Revision ID: 035
|
||||
Revises: 034
|
||||
Create Date: 2015-01-26 13:00:02.622503
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
||||
revision = '035'
|
||||
down_revision = '034'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql.expression import table
|
||||
|
||||
|
||||
def upgrade(active_plugins=None, options=None):
|
||||
op.add_column('projects', sa.Column('autocreate_branches',
|
||||
sa.Boolean(),
|
||||
default=False))
|
||||
|
||||
projects_table = table(
|
||||
'projects',
|
||||
sa.Column('autocreate_branches', sa.Boolean(), nullable=True)
|
||||
)
|
||||
|
||||
bind = op.get_bind()
|
||||
bind.execute(projects_table.update().
|
||||
values(autocreate_branches=False))
|
||||
|
||||
|
||||
def downgrade(active_plugins=None, options=None):
|
||||
op.drop_column('projects', 'autocreate_branches')
|
@ -0,0 +1,83 @@
|
||||
# Copyright (c) 2015 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.
|
||||
#
|
||||
|
||||
"""This migration adds new table for branches and for all projects in database
|
||||
adds branch with name 'master'.
|
||||
|
||||
Revision ID: 036
|
||||
Revises: 035
|
||||
Create Date: 2015-01-26 13:03:34.622503
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
||||
revision = '036'
|
||||
down_revision = '035'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql.expression import table
|
||||
|
||||
MYSQL_ENGINE = 'InnoDB'
|
||||
MYSQL_CHARSET = 'utf8'
|
||||
|
||||
|
||||
def upgrade(active_plugins=None, options=None):
|
||||
op.create_table(
|
||||
'branches',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(100), nullable=True),
|
||||
sa.Column('project_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('expired', sa.Boolean(), default=False, nullable=True),
|
||||
sa.Column('expiration_date', sa.DateTime(), default=None,
|
||||
nullable=True),
|
||||
sa.Column('autocreated', sa.Boolean(), default=False, nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name', 'project_id', name="branch_un_constr"),
|
||||
mysql_engine=MYSQL_ENGINE,
|
||||
mysql_charset=MYSQL_CHARSET
|
||||
)
|
||||
|
||||
bind = op.get_bind()
|
||||
|
||||
projects = list(bind.execute(
|
||||
sa.select(columns=['id', 'created_at', 'updated_at'],
|
||||
from_obj=sa.Table('projects', sa.MetaData()))))
|
||||
|
||||
branches_table = table(
|
||||
'branches',
|
||||
sa.Column('name', sa.String(100), nullable=True),
|
||||
sa.Column('project_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('expired', sa.Boolean(), default=False),
|
||||
sa.Column('expiration_date', sa.DateTime(), default=None),
|
||||
sa.Column('autocreated', sa.Boolean(), default=False),
|
||||
)
|
||||
|
||||
for project in projects:
|
||||
bind.execute(branches_table.insert().values(
|
||||
name="master",
|
||||
project_id=project['id'],
|
||||
created_at=project['created_at'],
|
||||
updated_at=project['updated_at']
|
||||
))
|
||||
|
||||
|
||||
def downgrade(active_plugins=None, options=None):
|
||||
op.drop_table('branches')
|
@ -0,0 +1,73 @@
|
||||
# Copyright (c) 2015 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.
|
||||
#
|
||||
|
||||
"""This migration adds new column for branch id and merge all tasks to branch
|
||||
'master' in corresponding project.
|
||||
|
||||
Revision ID: 037
|
||||
Revises: 036
|
||||
Create Date: 2015-01-27 13:17:34.622503
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
||||
revision = '037'
|
||||
down_revision = '036'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql.expression import table
|
||||
|
||||
MYSQL_ENGINE = 'InnoDB'
|
||||
MYSQL_CHARSET = 'utf8'
|
||||
|
||||
|
||||
def upgrade(active_plugins=None, options=None):
|
||||
op.add_column(
|
||||
'tasks',
|
||||
sa.Column('branch_id', sa.Integer(), nullable=True)
|
||||
)
|
||||
|
||||
bind = op.get_bind()
|
||||
|
||||
branches = list(bind.execute(
|
||||
sa.select(columns=['id', 'name', 'project_id'],
|
||||
from_obj=sa.Table('branches', sa.MetaData()))))
|
||||
|
||||
projects = list(bind.execute(
|
||||
sa.select(columns=['id'], from_obj=sa.Table('projects',
|
||||
sa.MetaData()))))
|
||||
branch_dict = {}
|
||||
|
||||
for branch in branches:
|
||||
branch_dict[(branch['project_id'], branch['name'])] = branch['id']
|
||||
|
||||
tasks_table = table(
|
||||
'tasks',
|
||||
sa.Column('project_id', sa.Integer(), nullable=True),
|
||||
sa.Column('branch_id', sa.Integer(), nullable=True)
|
||||
)
|
||||
|
||||
for project in projects:
|
||||
bind.execute(
|
||||
tasks_table.update().
|
||||
where(tasks_table.c.project_id == project['id']).
|
||||
values(branch_id=branch_dict[(project['id'], "master")])
|
||||
)
|
||||
|
||||
|
||||
def downgrade(active_plugins=None, options=None):
|
||||
op.drop_column('tasks', 'branch_id')
|
@ -242,8 +242,10 @@ class Project(FullText, ModelBuilder, Base):
|
||||
is_active = Column(Boolean, default=True)
|
||||
project_groups = relationship("ProjectGroup",
|
||||
secondary="project_group_mapping")
|
||||
autocreate_branches = Column(Boolean, default=False)
|
||||
|
||||
_public_fields = ["id", "name", "description", "tasks", "repo_url"]
|
||||
_public_fields = ["id", "name", "description", "tasks", "repo_url",
|
||||
"autocreate_branches"]
|
||||
|
||||
|
||||
class ProjectGroup(ModelBuilder, Base):
|
||||
@ -301,10 +303,30 @@ class Task(FullText, ModelBuilder, Base):
|
||||
story_id = Column(Integer, ForeignKey('stories.id'))
|
||||
project_id = Column(Integer, ForeignKey('projects.id'))
|
||||
assignee_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||
branch_id = Column(Integer, ForeignKey('branches.id'), nullable=True)
|
||||
priority = Column(Enum(*_TASK_PRIORITIES), default='medium')
|
||||
|
||||
_public_fields = ["id", "creator_id", "title", "status", "story_id",
|
||||
"project_id", "assignee_id", "priority"]
|
||||
"project_id", "assignee_id", "priority", "branch_id"]
|
||||
|
||||
|
||||
class Branch(FullText, ModelBuilder, Base):
|
||||
__tablename__ = 'branches'
|
||||
|
||||
__table_args__ = (
|
||||
schema.UniqueConstraint('name', 'project_id', name='branch_un_constr'),
|
||||
)
|
||||
|
||||
__fulltext_columns__ = ['name']
|
||||
|
||||
name = Column(String(CommonLength.top_middle_length))
|
||||
project_id = Column(Integer, ForeignKey('projects.id'))
|
||||
expired = Column(Boolean, default=False)
|
||||
expiration_date = Column(UTCDateTime, default=None)
|
||||
autocreated = Column(Boolean, default=False)
|
||||
|
||||
_public_fields = ["id", "name", "project_id", "expired",
|
||||
"expiration_date", "autocreated"]
|
||||
|
||||
|
||||
class StoryTag(ModelBuilder, Base):
|
||||
|
@ -32,6 +32,7 @@ class_mappings = {'task': [models.Task, wmodels.Task],
|
||||
'user': [models.User, wmodels.User],
|
||||
'team': [models.Team, wmodels.Team],
|
||||
'story': [models.Story, wmodels.Story],
|
||||
'branch': [models.Branch, wmodels.Branch],
|
||||
'tag': [models.StoryTag, wmodels.Tag]}
|
||||
|
||||
|
||||
@ -140,6 +141,7 @@ class NotificationHook(hooks.PecanHook):
|
||||
'projects': 'project',
|
||||
'project_groups': 'project_group',
|
||||
'tasks': 'task',
|
||||
'branches': 'branch',
|
||||
'timeline_events': 'timeline_event',
|
||||
'users': 'user',
|
||||
'teams': 'team',
|
||||
|
144
storyboard/tests/api/test_branches.py
Normal file
144
storyboard/tests/api/test_branches.py
Normal file
@ -0,0 +1,144 @@
|
||||
# Copyright (c) 2015 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 storyboard.tests import base
|
||||
|
||||
|
||||
class TestBranches(base.FunctionalTest):
|
||||
def setUp(self):
|
||||
super(TestBranches, self).setUp()
|
||||
|
||||
self.resource = '/branches'
|
||||
|
||||
self.branch_01 = {
|
||||
'name': 'test_branch_01',
|
||||
'project_id': 1
|
||||
}
|
||||
|
||||
self.branch_02 = {
|
||||
'name': 'test_branch_02',
|
||||
'project_id': 100
|
||||
}
|
||||
|
||||
self.branch_03 = {
|
||||
'name': 'test_branch_03',
|
||||
'project_id': 1,
|
||||
'expiration_date': '2014-01-01T00:00:00+00:00'
|
||||
}
|
||||
|
||||
self.put_branch_01 = {
|
||||
'project_id': 2
|
||||
}
|
||||
|
||||
self.put_branch_02 = {
|
||||
'expired': True
|
||||
}
|
||||
|
||||
self.put_branch_03 = {
|
||||
'expired': False
|
||||
}
|
||||
|
||||
self.put_branch_04 = {
|
||||
'expired': False,
|
||||
'expiration_date': '2014-01-01T00:00:00+00:00'
|
||||
}
|
||||
|
||||
self.project_01 = {
|
||||
'name': 'project-for-put',
|
||||
'description': 'test_description'
|
||||
}
|
||||
|
||||
self.default_headers['Authorization'] = 'Bearer valid_superuser_token'
|
||||
|
||||
def test_create(self):
|
||||
response = self.post_json(self.resource, self.branch_01)
|
||||
branch = response.json
|
||||
self.assertIn("id", branch)
|
||||
self.assertEqual(branch['name'], self.branch_01['name'])
|
||||
self.assertEqual(branch['project_id'], self.branch_01['project_id'])
|
||||
self.assertEqual(branch['expired'], False)
|
||||
self.assertIsNone(branch['expiration_date'])
|
||||
self.assertEqual(branch['autocreated'], False)
|
||||
|
||||
def test_create_invalid(self):
|
||||
response = self.post_json(self.resource, self.branch_03,
|
||||
expect_errors=True)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_update(self):
|
||||
response = self.post_json(self.resource, self.branch_01)
|
||||
branch = response.json
|
||||
self.assertEqual(branch['name'], self.branch_01['name'])
|
||||
self.assertEqual(branch['project_id'], self.branch_01['project_id'])
|
||||
self.assertIn("id", branch)
|
||||
resource = "".join([self.resource, ("/%d" % branch['id'])])
|
||||
|
||||
response_project = self.post_json('/projects', self.project_01)
|
||||
project = response_project.json
|
||||
|
||||
self.assertEqual(self.project_01['name'], project['name'])
|
||||
self.assertEqual(self.project_01['description'],
|
||||
project['description'])
|
||||
self.assertIn("id", project)
|
||||
self.put_branch_01["id"] = project["id"]
|
||||
|
||||
response = self.put_json(resource, self.put_branch_01)
|
||||
branch = response.json
|
||||
self.assertEqual(branch['name'], self.branch_01['name'])
|
||||
self.assertEqual(branch['project_id'],
|
||||
self.put_branch_01['project_id'])
|
||||
|
||||
response = self.put_json(resource, self.put_branch_02)
|
||||
branch = response.json
|
||||
self.assertEqual(branch['expired'], True)
|
||||
self.assertIsNotNone(branch['expiration_date'])
|
||||
|
||||
response = self.put_json(resource, self.put_branch_03)
|
||||
branch = response.json
|
||||
self.assertEqual(branch['expired'], False)
|
||||
self.assertIsNone(branch['expiration_date'])
|
||||
|
||||
def test_update_expiration_date(self):
|
||||
response = self.post_json(self.resource, self.branch_01)
|
||||
branch = response.json
|
||||
self.assertEqual(branch['name'], self.branch_01['name'])
|
||||
self.assertEqual(branch['project_id'], self.branch_01['project_id'])
|
||||
self.assertIn("id", branch)
|
||||
resource = "".join([self.resource, ("/%d" % branch['id'])])
|
||||
|
||||
response = self.put_json(resource, self.put_branch_02)
|
||||
branch = response.json
|
||||
self.assertEqual(branch['expired'], True)
|
||||
self.assertIsNotNone(branch['expiration_date'])
|
||||
|
||||
response = self.put_json(resource, self.put_branch_04,
|
||||
expect_errors=True)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_get_one(self):
|
||||
response = self.post_json(self.resource, self.branch_01)
|
||||
branch = response.json
|
||||
resource = "".join([self.resource, ("/%d" % branch['id'])])
|
||||
|
||||
branch = self.get_json(path=resource)
|
||||
self.assertEqual(branch['name'], self.branch_01['name'])
|
||||
self.assertEqual(branch['project_id'], self.branch_01['project_id'])
|
||||
self.assertEqual(branch['expired'], False)
|
||||
self.assertIsNone(branch['expiration_date'])
|
||||
self.assertEqual(branch['autocreated'], False)
|
||||
|
||||
def test_get_invalid(self):
|
||||
resource = "".join([self.resource, "/1000"])
|
||||
response = self.get_json(path=resource, expect_errors=True)
|
||||
self.assertEqual(404, response.status_code)
|
46
storyboard/tests/db/api/test_branches.py
Normal file
46
storyboard/tests/db/api/test_branches.py
Normal file
@ -0,0 +1,46 @@
|
||||
# Copyright (c) 2015 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 storyboard.db.api import branches
|
||||
from storyboard.db.api import projects
|
||||
from storyboard.tests.db import base
|
||||
|
||||
|
||||
class BranchesTest(base.BaseDbTestCase):
|
||||
def setUp(self):
|
||||
super(BranchesTest, self).setUp()
|
||||
|
||||
self.branch_01 = {
|
||||
'name': u'test_branch',
|
||||
'project_id': 1
|
||||
}
|
||||
|
||||
self.project_01 = {
|
||||
'name': u'TestProject',
|
||||
'description': u'TestDescription'
|
||||
}
|
||||
|
||||
projects.project_create(self.project_01)
|
||||
|
||||
def test_create_branch(self):
|
||||
self._test_create(self.branch_01, branches.branch_create)
|
||||
|
||||
def test_update_branch(self):
|
||||
delta = {
|
||||
'expired': True
|
||||
}
|
||||
|
||||
self._test_update(self.branch_01, delta, branches.branch_create,
|
||||
branches.branch_update)
|
Loading…
x
Reference in New Issue
Block a user