Added DELETE method for projects, stories, and tasks.
While true deletion of records is a bad idea, we still need an ability to remove individual items from showing up in searches. This commit aims to lay the ground work for this, though we'll want to put better permissions around it. I could use some help on this - for some reason running the tests dies a horrible death because it gets confused about the migrations. Change-Id: Ib0eb2f06553644878fb7bf6ad49a820efcdf22e0
This commit is contained in:
parent
93b851b719
commit
195cfad727
|
@ -13,9 +13,13 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
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
|
||||
|
@ -41,11 +45,15 @@ class Project(base.APIBase):
|
|||
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.")
|
||||
description="This is an awesome project.",
|
||||
is_active=True)
|
||||
|
||||
|
||||
class ProjectsController(rest.RestController):
|
||||
|
@ -63,7 +71,12 @@ class ProjectsController(rest.RestController):
|
|||
"""
|
||||
|
||||
project = dbapi.project_get(project_id)
|
||||
return project
|
||||
|
||||
if project:
|
||||
return Project.from_db_model(project)
|
||||
else:
|
||||
raise ClientSideError("Project %s not found" % id,
|
||||
status_code=404)
|
||||
|
||||
@secure(checks.guest)
|
||||
@wsme_pecan.wsexpose([Project])
|
||||
|
@ -93,4 +106,19 @@ class ProjectsController(rest.RestController):
|
|||
"""
|
||||
result = dbapi.project_update(project_id,
|
||||
project.as_dict(omit_unset=True))
|
||||
return Project.from_db_model(result)
|
||||
|
||||
if result:
|
||||
return Project.from_db_model(result)
|
||||
else:
|
||||
raise ClientSideError("Project %s not found" % id,
|
||||
status_code=404)
|
||||
|
||||
@wsme_pecan.wsexpose(Project, int)
|
||||
def delete(self, project_id):
|
||||
"""Delete this project.
|
||||
|
||||
:param project_id: An ID of the project.
|
||||
"""
|
||||
dbapi.project_delete(project_id)
|
||||
|
||||
response.status_code = 204
|
||||
|
|
|
@ -13,8 +13,11 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from pecan import response
|
||||
from pecan import rest
|
||||
from wsme.exc import ClientSideError
|
||||
from wsme import types as wtypes
|
||||
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from storyboard.api.v1 import base
|
||||
|
@ -37,6 +40,9 @@ class Story(base.APIBase):
|
|||
is_bug = bool
|
||||
"""Is this a bug or a feature :)"""
|
||||
|
||||
is_active = bool
|
||||
"""Is this an active story, or has it been deleted?"""
|
||||
|
||||
#todo(nkonovalov): replace with a enum
|
||||
priority = wtypes.text
|
||||
"""Priority.
|
||||
|
@ -52,6 +58,7 @@ class Story(base.APIBase):
|
|||
title="Use Storyboard to manage Storyboard",
|
||||
description="We should use Storyboard to manage Storyboard.",
|
||||
is_bug=False,
|
||||
is_active=True,
|
||||
priority='Critical')
|
||||
|
||||
|
||||
|
@ -65,7 +72,12 @@ class StoriesController(rest.RestController):
|
|||
:param story_id: An ID of the story.
|
||||
"""
|
||||
story = dbapi.story_get(story_id)
|
||||
return Story.from_db_model(story)
|
||||
|
||||
if story:
|
||||
return Story.from_db_model(story)
|
||||
else:
|
||||
raise ClientSideError("Story %s not found" % id,
|
||||
status_code=404)
|
||||
|
||||
@wsme_pecan.wsexpose([Story], int)
|
||||
def get_all(self, project_id=None):
|
||||
|
@ -99,4 +111,19 @@ class StoriesController(rest.RestController):
|
|||
"""
|
||||
updated_story = dbapi.story_update(story_id,
|
||||
story.as_dict(omit_unset=True))
|
||||
return Story.from_db_model(updated_story)
|
||||
|
||||
if updated_story:
|
||||
return Story.from_db_model(updated_story)
|
||||
else:
|
||||
raise ClientSideError("Story %s not found" % id,
|
||||
status_code=404)
|
||||
|
||||
@wsme_pecan.wsexpose(Story, int)
|
||||
def delete(self, story_id):
|
||||
"""Delete this story.
|
||||
|
||||
:param story_id: An ID of the story.
|
||||
"""
|
||||
dbapi.story_delete(story_id)
|
||||
|
||||
response.status_code = 204
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from pecan import response
|
||||
from pecan import rest
|
||||
from wsme.exc import ClientSideError
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
@ -37,6 +39,9 @@ class Task(base.APIBase):
|
|||
Allowed values: ['Todo', 'In review', 'Landed'].
|
||||
"""
|
||||
|
||||
is_active = bool
|
||||
"""Is this an active task, or has it been deleted?"""
|
||||
|
||||
story_id = int
|
||||
"""The ID of the corresponding Story."""
|
||||
|
||||
|
@ -54,7 +59,12 @@ class TasksController(rest.RestController):
|
|||
:param task_id: An ID of the task.
|
||||
"""
|
||||
task = dbapi.task_get(task_id)
|
||||
return Task.from_db_model(task)
|
||||
|
||||
if task:
|
||||
return Task.from_db_model(task)
|
||||
else:
|
||||
raise ClientSideError("Task %s not found" % id,
|
||||
status_code=404)
|
||||
|
||||
@wsme_pecan.wsexpose([Task], int)
|
||||
def get_all(self, story_id=None):
|
||||
|
@ -83,4 +93,19 @@ class TasksController(rest.RestController):
|
|||
"""
|
||||
updated_task = dbapi.task_update(task_id,
|
||||
task.as_dict(omit_unset=True))
|
||||
return Task.from_db_model(updated_task)
|
||||
|
||||
if updated_task:
|
||||
return Task.from_db_model(updated_task)
|
||||
else:
|
||||
raise ClientSideError("Task %s not found" % id,
|
||||
status_code=404)
|
||||
|
||||
@wsme_pecan.wsexpose(Task, int)
|
||||
def delete(self, task_id):
|
||||
"""Delete this task.
|
||||
|
||||
:param task_id: An ID of the task.
|
||||
"""
|
||||
dbapi.task_delete(task_id)
|
||||
|
||||
response.status_code = 204
|
||||
|
|
|
@ -78,7 +78,7 @@ def model_query(model, session=None):
|
|||
|
||||
def __entity_get(kls, entity_id, session):
|
||||
query = model_query(kls, session)
|
||||
return query.filter_by(id=entity_id).first()
|
||||
return query.filter_by(id=entity_id, is_active=True).first()
|
||||
|
||||
|
||||
def _entity_get(kls, entity_id):
|
||||
|
@ -100,9 +100,9 @@ def _entity_create(kls, values):
|
|||
with session.begin():
|
||||
try:
|
||||
session.add(entity)
|
||||
except db_exc.DBDuplicateEntry as e:
|
||||
raise exc.DuplicateEntry("Duplicate etnry for : %s"
|
||||
% (kls.__name__, e.colums))
|
||||
except db_exc.DBDuplicateEntry:
|
||||
raise exc.DuplicateEntry("Duplicate entry for : %s"
|
||||
% (kls.__name__))
|
||||
|
||||
return entity
|
||||
|
||||
|
@ -171,7 +171,7 @@ def project_get(project_id):
|
|||
|
||||
|
||||
def project_get_all(**kwargs):
|
||||
return _entity_get_all(models.Project, **kwargs)
|
||||
return _entity_get_all(models.Project, is_active=True)
|
||||
|
||||
|
||||
def project_create(values):
|
||||
|
@ -182,6 +182,14 @@ def project_update(project_id, values):
|
|||
return _entity_update(models.Project, project_id, values)
|
||||
|
||||
|
||||
def project_delete(project_id):
|
||||
project = project_get(project_id)
|
||||
|
||||
if project:
|
||||
project.is_active = False
|
||||
_entity_update(models.Project, project_id, project.as_dict())
|
||||
|
||||
|
||||
# BEGIN Stories
|
||||
|
||||
def story_get(story_id):
|
||||
|
@ -192,14 +200,15 @@ def story_get_all(project_id=None):
|
|||
if project_id:
|
||||
return story_get_all_in_project(project_id)
|
||||
else:
|
||||
return _entity_get_all(models.Story)
|
||||
return _entity_get_all(models.Story, is_active=True)
|
||||
|
||||
|
||||
def story_get_all_in_project(project_id):
|
||||
session = get_session()
|
||||
|
||||
query = model_query(models.Story, session).join(models.Task)
|
||||
return query.filter_by(project_id=project_id)
|
||||
return query.filter(models.Task.project_id == project_id,
|
||||
models.Story.is_active)
|
||||
|
||||
|
||||
def story_create(values):
|
||||
|
@ -210,6 +219,14 @@ def story_update(story_id, values):
|
|||
return _entity_update(models.Story, story_id, values)
|
||||
|
||||
|
||||
def story_delete(story_id):
|
||||
story = story_get(story_id)
|
||||
|
||||
if story:
|
||||
story.is_active = False
|
||||
_entity_update(models.Story, story_id, story.as_dict())
|
||||
|
||||
|
||||
# BEGIN Tasks
|
||||
|
||||
def task_get(task_id):
|
||||
|
@ -217,7 +234,7 @@ def task_get(task_id):
|
|||
|
||||
|
||||
def task_get_all(story_id=None):
|
||||
return _entity_get_all(models.Task, story_id=story_id)
|
||||
return _entity_get_all(models.Task, story_id=story_id, is_active=True)
|
||||
|
||||
|
||||
def task_create(values):
|
||||
|
@ -226,3 +243,11 @@ def task_create(values):
|
|||
|
||||
def task_update(task_id, values):
|
||||
return _entity_update(models.Task, task_id, values)
|
||||
|
||||
|
||||
def task_delete(task_id):
|
||||
task = task_get(task_id)
|
||||
|
||||
if task:
|
||||
task.is_active = False
|
||||
_entity_update(models.Task, task_id, task.as_dict())
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# 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.
|
||||
#
|
||||
|
||||
"""deletion states
|
||||
|
||||
Revision ID: 21645ef1040f
|
||||
Revises: 399f57edc6b6
|
||||
Create Date: 2014-03-03 16:08:12.584302
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '21645ef1040f'
|
||||
down_revision = '399f57edc6b6'
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import Boolean
|
||||
from sqlalchemy import Column
|
||||
|
||||
|
||||
def upgrade(active_plugins=None, options=None):
|
||||
op.add_column('projects',
|
||||
Column('is_active', Boolean(), default=True))
|
||||
op.add_column('stories',
|
||||
Column('is_active', Boolean(), default=True))
|
||||
op.add_column('tasks',
|
||||
Column('is_active', Boolean(), default=True))
|
||||
|
||||
|
||||
def downgrade(active_plugins=None, options=None):
|
||||
op.drop_column('projects', 'is_active')
|
||||
op.drop_column('stories', 'is_active')
|
||||
op.drop_column('tasks', 'is_active')
|
|
@ -145,6 +145,7 @@ class Project(Base):
|
|||
team_id = Column(Integer, ForeignKey('teams.id'))
|
||||
team = relationship(Team, primaryjoin=team_id == Team.id)
|
||||
tasks = relationship('Task', backref='project')
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
|
||||
class ProjectGroup(Base):
|
||||
|
@ -198,6 +199,7 @@ class Story(Base):
|
|||
tasks = relationship('Task', backref='story')
|
||||
comments = relationship('Comment', backref='story')
|
||||
tags = relationship('StoryTag', backref='story')
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
|
||||
class Task(Base):
|
||||
|
@ -209,6 +211,7 @@ class Task(Base):
|
|||
project_id = Column(Integer, ForeignKey('projects.id'))
|
||||
assignee_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||
milestone_id = Column(Integer, ForeignKey('milestones.id'), nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
|
||||
class Comment(Base):
|
||||
|
|
|
@ -9,7 +9,7 @@ oslo.sphinx
|
|||
testrepository>=0.0.18
|
||||
testscenarios>=0.4,<0.5
|
||||
testtools>=0.9.34
|
||||
posix_ipc
|
||||
posix_ipc>=0.9.8
|
||||
|
||||
|
||||
# Some of the tests use real MySQL and Postgres databases
|
||||
|
|
Loading…
Reference in New Issue