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:
Michael Krotscheck 2014-03-03 17:46:47 -08:00
parent 93b851b719
commit 195cfad727
7 changed files with 167 additions and 16 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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