Archive cards instead of deleting them

This patch adds an "archived" field to WorklistItems, which
signifies whether or not the card is archived. It also makes
the delete endpoint for cards set this field to True rather
than deleting the card entirely.

This patch also makes the function for adding new cards to
boards/worklists check if there is a card for the same object
archived in the board/worklist, and if there is restores that
card rather than creating a new one.

Change-Id: Iacb2733a2c647f179c29a7d6449497f65b868860
This commit is contained in:
Adam Coldrick 2016-02-26 15:56:41 +00:00 committed by Adam Coldrick
parent 2a8ba8c18c
commit 598f965899
6 changed files with 126 additions and 61 deletions

View File

@ -573,6 +573,9 @@ class WorklistItem(base.APIBase):
list_position = int
"""The position of this item in the Worklist."""
archived = bool
"""Whether or not this item is archived."""
display_due_date = int
"""The ID of the due date displayed on this item."""
@ -629,6 +632,8 @@ class Worklist(base.APIBase):
self.items = []
user_id = request.current_user_id
for item in worklist.items:
if item.archived:
continue
item_model = WorklistItem.from_db_model(item)
if item.item_type == 'story':
story = stories_api.story_get(item.item_id)

View File

@ -157,8 +157,14 @@ class ItemsSubcontroller(rest.RestController):
raise exc.NotFound(_("Item %s seems to have been deleted, "
"try refreshing your page.") % item_id)
worklists_api.move_item(id, item_id, list_position, list_id)
if display_due_date is not None:
worklists_api.update_item(item_id, display_due_date)
if display_due_date == -1:
display_due_date = None
update_dict = {
'display_due_date': display_due_date
}
worklists_api.update_item(item_id, update_dict)
updated = worklists_api.get_item_by_id(item_id)
result = wmodels.WorklistItem.from_db_model(updated)
@ -179,10 +185,11 @@ class ItemsSubcontroller(rest.RestController):
if not worklists_api.editable_contents(worklists_api.get(id),
user_id):
raise exc.NotFound(_("Worklist %s not found") % id)
if worklists_api.get_item_by_id(item_id) is None:
item = worklists_api.get_item_by_id(item_id)
if item is None:
raise exc.NotFound(_("Item %s seems to have already been deleted,"
" try refreshing your page.") % item_id)
worklists_api.remove_item(id, item_id)
worklists_api.update_item(item_id, {'archived': True})
class WorklistsController(rest.RestController):

View File

@ -1,4 +1,4 @@
# Copyright (c) 2015 Codethink Limited
# Copyright (c) 2015-2016 Codethink Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -97,6 +97,17 @@ def get_from_lane(worklist):
return lane.board
# FIXME: This assumes that boards only contain a task or story once, which
# is not actually enforced when creating a card.
def get_card(board, item_type, item_id, archived=False):
for lane in board.lanes:
for card in lane.worklist.items:
if (card.item_type == item_type and
card.item_id == item_id and
card.archived == archived):
return card
def get_owners(board):
for permission in board.permissions:
if permission.codename == 'edit_board':

View File

@ -1,4 +1,4 @@
# Copyright (c) 2015 Codethink Limited
# Copyright (c) 2015-2016 Codethink Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -76,39 +76,60 @@ def update(worklist_id, values):
def add_item(worklist_id, item_id, item_type, list_position):
session = api_base.get_session()
worklist = _worklist_get(worklist_id)
if worklist is None:
raise exc.NotFound(_("Worklist %s not found") % worklist_id)
with session.begin(subtransactions=True):
worklist = _worklist_get(worklist_id, session)
if worklist is None:
raise exc.NotFound(_("Worklist %s not found") % worklist_id)
if item_type == 'story':
item = stories.story_get(item_id)
elif item_type == 'task':
item = tasks.task_get(item_id)
else:
raise ClientSideError(_("An item in a worklist must be either a "
"story or a task"))
if item is None:
raise exc.NotFound(_("%(type)s %(id)s not found") %
{'type': item_type, 'id': item_id})
item_dict = {
'list_id': worklist_id,
'item_id': item_id,
'item_type': item_type,
# Check if this item has an archived card in this worklist to restore
archived = get_item_by_item_id(
worklist, item_type, item_id, archived=True)
if archived:
update = {
'archived': False,
'list_position': list_position
}
worklist_item = api_base.entity_create(models.WorklistItem, item_dict)
api_base.entity_update(models.WorklistItem, archived.id, update)
return worklist
if worklist.items is None:
worklist.items = [worklist_item]
else:
worklist.items.append(worklist_item)
session.add(worklist_item)
session.add(worklist)
# If this worklist is a lane, check if the item has an archived card
# somewhere in the board to restore
if is_lane(worklist):
board = boards.get_from_lane(worklist)
archived = boards.get_card(board, item_type, item_id, archived=True)
if archived:
update = {
'archived': False,
'list_id': worklist_id,
'list_position': list_position
}
api_base.entity_update(models.WorklistItem, archived.id, update)
return worklist
# Create a new card
if item_type == 'story':
item = stories.story_get(item_id)
elif item_type == 'task':
item = tasks.task_get(item_id)
else:
raise ClientSideError(_("An item in a worklist must be either a "
"story or a task"))
if item is None:
raise exc.NotFound(_("%(type)s %(id)s not found") %
{'type': item_type, 'id': item_id})
item_dict = {
'list_id': worklist_id,
'item_id': item_id,
'item_type': item_type,
'list_position': list_position
}
worklist_item = api_base.entity_create(models.WorklistItem, item_dict)
if worklist.items is None:
worklist.items = [worklist_item]
else:
worklist.items.append(worklist_item)
return worklist
@ -129,6 +150,15 @@ def get_item_at_position(worklist_id, list_position):
return query.first()
def get_item_by_item_id(worklist, item_type, item_id, archived):
session = api_base.get_session()
query = session.query(models.WorklistItem).filter_by(
list_id=worklist.id, item_type=item_type,
item_id=item_id, archived=archived)
return query.first()
def move_item(worklist_id, item_id, list_position, list_id=None):
session = api_base.get_session()
@ -176,36 +206,10 @@ def move_item(worklist_id, item_id, list_position, list_id=None):
list_item.list_position += 1
def update_item(item_id, display_due_date):
if display_due_date == -1:
display_due_date = None
updated = {
'display_due_date': display_due_date
}
def update_item(item_id, updated):
return api_base.entity_update(models.WorklistItem, item_id, updated)
def remove_item(worklist_id, item_id):
session = api_base.get_session()
with session.begin(subtransactions=True):
worklist = _worklist_get(worklist_id, session)
if worklist is None:
raise exc.NotFound(_("Worklist %s not found") % worklist_id)
item = get_item_by_id(item_id)
if item is None:
raise exc.NotFound(_("WorklistItem %s not found") % item_id)
item_entry = [i for i in worklist.items if i.id == item_id][0]
worklist.items.remove(item_entry)
session.add(worklist)
session.delete(item)
return worklist
def is_lane(worklist):
lanes = api_base.entity_get_all(models.BoardWorklist,
list_id=worklist.id)

View File

@ -0,0 +1,37 @@
# 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.
#
"""Add archived field to cards
Revision ID: 4301fc1c4158
Revises: 054
Create Date: 2016-02-25 19:15:29.464877
"""
# revision identifiers, used by Alembic.
revision = '054'
down_revision = '053'
from alembic import op
import sqlalchemy as sa
def upgrade(active_plugins=None, options=None):
op.add_column(
'worklist_items', sa.Column('archived', sa.Boolean(), nullable=True))
def downgrade(active_plugins=None, options=None):
op.drop_column('worklist_items', 'archived')

View File

@ -532,6 +532,7 @@ class WorklistItem(ModelBuilder, Base):
display_due_date = Column(Integer,
ForeignKey('due_dates.id'),
nullable=True)
archived = Column(Boolean, default=False)
_public_fields = ["id", "list_id", "list_position", "item_type",
"item_id"]