2118 lines
71 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from sqlmodel import Session, select, func
from typing import List, Optional
from app.database import get_session
from app.models import (
KanbanBoard, KanbanColumn, KanbanCard, Channel, User,
KanbanChecklist, KanbanChecklistItem, UserRole,
KanbanCardComment, KanbanCardAttachment, KanbanTimeEntry,
KanbanCustomField, KanbanCustomFieldValue, KanbanCardTemplate,
KanbanCardActivityLog
)
from app.schemas import (
KanbanBoardCreate, KanbanBoardUpdate, KanbanBoardResponse,
KanbanColumnCreate, KanbanColumnUpdate, KanbanColumnResponse,
KanbanCardCreate, KanbanCardUpdate, KanbanCardResponse,
KanbanBoardWithColumns, KanbanColumnWithCards,
KanbanChecklistCreate, KanbanChecklistUpdate, KanbanChecklistResponse,
KanbanChecklistItemCreate, KanbanChecklistItemUpdate, KanbanChecklistItemResponse,
KanbanChecklistWithItems, KanbanCardWithChecklists,
KanbanCardCommentCreate, KanbanCardCommentUpdate, KanbanCardCommentResponse,
KanbanCardAttachmentResponse, KanbanTimeEntryCreate, KanbanTimeEntryUpdate, KanbanTimeEntryResponse,
KanbanCustomFieldCreate, KanbanCustomFieldUpdate, KanbanCustomFieldResponse,
KanbanCustomFieldValueCreate, KanbanCustomFieldValueUpdate, KanbanCustomFieldValueResponse,
KanbanCardTemplateCreate, KanbanCardTemplateUpdate, KanbanCardTemplateResponse,
KanbanCardExtendedResponse, KanbanBoardExtendedResponse
)
from app.auth import get_current_user
import os
from pathlib import Path
from datetime import datetime
router = APIRouter(prefix="/kanban", tags=["Kanban"])
# Helper function to log card activity
def log_card_activity(
session: Session,
card_id: int,
user_id: int,
action: str,
field_name: Optional[str] = None,
old_value: Optional[str] = None,
new_value: Optional[str] = None
):
"""Log a card activity for the activity log"""
try:
# Convert assignee_id to names for better readability
if field_name == 'assignee_id':
old_name = None
new_name = None
if old_value and old_value != 'None':
try:
old_user = session.get(User, int(old_value))
old_name = old_user.full_name if old_user else f"User {old_value}"
except:
old_name = str(old_value)
if new_value and new_value != 'None':
try:
new_user = session.get(User, int(new_value))
new_name = new_user.full_name if new_user else f"User {new_value}"
except:
new_name = str(new_value)
old_value = old_name or "Nicht zugewiesen"
new_value = new_name or "Nicht zugewiesen"
field_name = 'Zugewiesen an'
activity = KanbanCardActivityLog(
card_id=card_id,
user_id=user_id,
action=action,
field_name=field_name,
old_value=str(old_value) if old_value is not None else None,
new_value=str(new_value) if new_value is not None else None
)
session.add(activity)
session.commit()
except Exception as e:
# Log errors but don't fail the main operation
print(f"Failed to log activity: {e}")
# Board endpoints
@router.post("/boards", response_model=KanbanBoardResponse, status_code=status.HTTP_201_CREATED)
def create_board(
board_data: KanbanBoardCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Create a new kanban board for a channel"""
# Check if channel exists and user has access
channel = session.get(Channel, board_data.channel_id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Channel not found"
)
# Check if user has access to the channel's department
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to this channel"
)
# Check if board already exists for this channel
existing_board = session.exec(
select(KanbanBoard).where(KanbanBoard.channel_id == board_data.channel_id)
).first()
if existing_board:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Board already exists for this channel"
)
new_board = KanbanBoard(
channel_id=board_data.channel_id,
name=board_data.name
)
session.add(new_board)
session.commit()
session.refresh(new_board)
return new_board
@router.get("/boards/{id}", response_model=KanbanBoardWithColumns)
def get_board_by_id_or_channel(
id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get kanban board by ID or by channel ID"""
# First try to get board directly by ID
board = session.get(KanbanBoard, id)
if board:
# Check access via channel
channel = session.get(Channel, board.channel_id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Board's associated channel not found"
)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to this board"
)
else:
# If not found as board ID, try as channel ID
channel = session.get(Channel, id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Board or channel not found"
)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to this channel"
)
# Get board for this channel
board = session.exec(
select(KanbanBoard).where(KanbanBoard.channel_id == id)
).first()
if not board:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No kanban board found for this channel"
)
# Load columns with cards
columns = session.exec(
select(KanbanColumn)
.where(KanbanColumn.board_id == board.id)
.order_by(KanbanColumn.position)
).all()
board_data = KanbanBoardWithColumns(
id=board.id,
channel_id=board.channel_id,
name=board.name,
created_at=board.created_at,
updated_at=board.updated_at,
columns=[]
)
for column in columns:
cards = session.exec(
select(KanbanCard)
.where(KanbanCard.column_id == column.id)
.where(KanbanCard.is_archived == False)
.order_by(KanbanCard.position)
).all()
column_data = KanbanColumnWithCards(
id=column.id,
board_id=column.board_id,
name=column.name,
position=column.position,
color=column.color,
created_at=column.created_at,
updated_at=column.updated_at,
cards=[]
)
for card in cards:
# Load assignee data explicitly
if card.assignee_id:
card.assignee = session.get(User, card.assignee_id)
# Calculate counts
attachments_count = session.exec(
select(func.count(KanbanCardAttachment.id))
.where(KanbanCardAttachment.card_id == card.id)
).one()
checklists_count = session.exec(
select(func.count(KanbanChecklist.id))
.where(KanbanChecklist.card_id == card.id)
).one()
comments_count = session.exec(
select(func.count(KanbanCardComment.id))
.where(KanbanCardComment.card_id == card.id)
).one()
card_response = KanbanCardResponse.from_orm(card)
card_response.attachments_count = attachments_count
card_response.checklists_count = checklists_count
card_response.comments_count = comments_count
column_data.cards.append(card_response)
board_data.columns.append(column_data)
return board_data
@router.put("/boards/{board_id}", response_model=KanbanBoardResponse)
def update_board(
board_id: int,
board_data: KanbanBoardUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Update a kanban board"""
board = session.get(KanbanBoard, board_id)
if not board:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Board not found"
)
# Check access via channel
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
update_data = board_data.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(board, field, value)
session.commit()
session.refresh(board)
return board
@router.get("/boards/by-id/{board_id}", response_model=KanbanBoardWithColumns)
def get_board_by_id(
board_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get kanban board by board ID"""
board = session.get(KanbanBoard, board_id)
if not board:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Board not found"
)
# Check access via channel
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to this board"
)
# Load columns with cards
columns = session.exec(
select(KanbanColumn)
.where(KanbanColumn.board_id == board.id)
.order_by(KanbanColumn.position)
).all()
board_data = KanbanBoardWithColumns.from_orm(board)
board_data.columns = []
for column in columns:
cards = session.exec(
select(KanbanCard)
.where(KanbanCard.column_id == column.id)
.where(KanbanCard.is_archived == False)
.order_by(KanbanCard.position)
).all()
# Load assignee data for all cards
for card in cards:
if card.assignee_id:
card.assignee = session.get(User, card.assignee_id)
column_data = KanbanColumnWithCards.from_orm(column)
column_data.cards = [KanbanCardResponse.from_orm(card) for card in cards]
board_data.columns.append(column_data)
return board_data
# Column endpoints
@router.post("/columns", response_model=KanbanColumnResponse, status_code=status.HTTP_201_CREATED)
def create_column(
column_data: KanbanColumnCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Create a new kanban column"""
# Check if board exists and user has access
board = session.get(KanbanBoard, column_data.board_id)
if not board:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Board not found"
)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
new_column = KanbanColumn(
board_id=column_data.board_id,
name=column_data.name,
position=column_data.position,
color=column_data.color
)
session.add(new_column)
session.commit()
session.refresh(new_column)
return new_column
@router.put("/columns/{column_id}", response_model=KanbanColumnResponse)
def update_column(
column_id: int,
column_data: KanbanColumnUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Update a kanban column"""
column = session.get(KanbanColumn, column_id)
if not column:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Column not found"
)
# Check access via board and channel
board = session.get(KanbanBoard, column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
update_data = column_data.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(column, field, value)
session.commit()
session.refresh(column)
return column
@router.delete("/columns/{column_id}")
def delete_column(
column_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Delete a kanban column"""
column = session.get(KanbanColumn, column_id)
if not column:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Column not found"
)
# Check access via board and channel
board = session.get(KanbanBoard, column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
session.delete(column)
session.commit()
return {"message": "Column deleted successfully"}
# Card endpoints
@router.post("/cards", response_model=KanbanCardResponse, status_code=status.HTTP_201_CREATED)
def create_card(
card_data: KanbanCardCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Create a new kanban card"""
# Check if column exists and user has access
column = session.get(KanbanColumn, card_data.column_id)
if not column:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Column not found"
)
board = session.get(KanbanBoard, column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
new_card = KanbanCard(
column_id=card_data.column_id,
title=card_data.title,
description=card_data.description,
assignee_id=card_data.assignee_id,
position=card_data.position,
due_date=card_data.due_date,
priority=card_data.priority,
labels=card_data.labels
)
session.add(new_card)
session.commit()
session.refresh(new_card)
# Log card creation
log_card_activity(
session,
new_card.id,
current_user.id,
'created',
None,
None,
new_card.title
)
return new_card
@router.put("/cards/{card_id}", response_model=KanbanCardResponse)
def update_card(
card_id: int,
card_data: KanbanCardUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Update a kanban card"""
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via column, board and channel
column = session.get(KanbanColumn, card.column_id)
board = session.get(KanbanBoard, column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
update_data = card_data.dict(exclude_unset=True)
# Log each field change
for field, value in update_data.items():
old_value = getattr(card, field, None)
if old_value != value:
log_card_activity(
session,
card_id,
current_user.id,
'updated',
field,
old_value,
value
)
setattr(card, field, value)
session.commit()
session.refresh(card)
return card
@router.delete("/cards/{card_id}")
def delete_card(
card_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Archive a kanban card (soft delete)"""
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via column, board and channel
column = session.get(KanbanColumn, card.column_id)
board = session.get(KanbanBoard, column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Archive the card instead of deleting
card.is_archived = True
# Log the archive action
log_card_activity(
session,
card_id,
current_user.id,
'archived',
None,
None,
card.title
)
session.commit()
return {"message": "Card archived successfully"}
@router.put("/cards/{card_id}/move")
def move_card(
card_id: int,
target_column_id: int,
new_position: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Move a card to a different column and/or position"""
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
target_column = session.get(KanbanColumn, target_column_id)
if not target_column:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Target column not found"
)
# Check access for both source and target
source_column = session.get(KanbanColumn, card.column_id)
source_board = session.get(KanbanBoard, source_column.board_id)
target_board = session.get(KanbanBoard, target_column.board_id)
if source_board.id != target_board.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot move cards between different boards"
)
channel = session.get(Channel, source_board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Update card position
old_column_id = card.column_id
old_position = card.position
card.column_id = target_column_id
card.position = new_position
# Log the move if column changed
if old_column_id != target_column_id:
old_column = session.get(KanbanColumn, old_column_id)
new_column = session.get(KanbanColumn, target_column_id)
log_card_activity(
session,
card_id,
current_user.id,
'moved',
'column',
old_column.name if old_column else 'Unknown',
new_column.name if new_column else 'Unknown'
)
session.commit()
session.refresh(card)
return {"message": "Card moved successfully"}
@router.get("/boards/{board_id}/archived-cards", response_model=List[KanbanCardExtendedResponse])
def get_archived_cards(
board_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get all archived cards for a board"""
board = session.get(KanbanBoard, board_id)
if not board:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Board not found"
)
# Check access via board -> channel -> department
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Get all columns of this board
columns = session.exec(
select(KanbanColumn).where(KanbanColumn.board_id == board_id)
).all()
archived_cards = []
for column in columns:
cards = session.exec(
select(KanbanCard)
.where(KanbanCard.column_id == column.id)
.where(KanbanCard.is_archived == True)
.order_by(KanbanCard.updated_at.desc())
).all()
for card in cards:
# Load assignee data
if card.assignee_id:
card.assignee = session.get(User, card.assignee_id)
# Add column name to card response
card_dict = KanbanCardExtendedResponse.from_orm(card).dict()
card_dict['column_name'] = column.name
archived_cards.append(KanbanCardExtendedResponse(**card_dict))
return archived_cards
# Checklist endpoints
@router.post("/checklists", response_model=KanbanChecklistResponse, status_code=status.HTTP_201_CREATED)
def create_checklist(
checklist_data: KanbanChecklistCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Create a new checklist for a card"""
# Check if card exists and user has access
card = session.get(KanbanCard, checklist_data.card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
new_checklist = KanbanChecklist(
card_id=checklist_data.card_id,
title=checklist_data.title,
position=checklist_data.position
)
session.add(new_checklist)
session.commit()
session.refresh(new_checklist)
return new_checklist
@router.get("/checklists/{checklist_id}", response_model=KanbanChecklistWithItems)
def get_checklist(
checklist_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get a checklist with its items"""
checklist = session.get(KanbanChecklist, checklist_id)
if not checklist:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Checklist not found"
)
# Check access via card -> board -> channel -> department
board = session.get(KanbanBoard, checklist.card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
return checklist
@router.put("/checklists/{checklist_id}", response_model=KanbanChecklistResponse)
def update_checklist(
checklist_id: int,
checklist_data: KanbanChecklistUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Update a checklist"""
checklist = session.get(KanbanChecklist, checklist_id)
if not checklist:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Checklist not found"
)
# Check access
board = session.get(KanbanBoard, checklist.card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Update fields
if checklist_data.title is not None:
checklist.title = checklist_data.title
if checklist_data.position is not None:
checklist.position = checklist_data.position
session.commit()
session.refresh(checklist)
return checklist
@router.delete("/checklists/{checklist_id}")
def delete_checklist(
checklist_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Delete a checklist"""
checklist = session.get(KanbanChecklist, checklist_id)
if not checklist:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Checklist not found"
)
# Check access
board = session.get(KanbanBoard, checklist.card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
session.delete(checklist)
session.commit()
return {"message": "Checklist deleted successfully"}
# Checklist Item endpoints
@router.post("/checklist-items", response_model=KanbanChecklistItemResponse, status_code=status.HTTP_201_CREATED)
def create_checklist_item(
item_data: KanbanChecklistItemCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Create a new checklist item"""
# Check if checklist exists and user has access
checklist = session.get(KanbanChecklist, item_data.checklist_id)
if not checklist:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Checklist not found"
)
# Check access via checklist -> card -> board -> channel -> department
board = session.get(KanbanBoard, checklist.card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
new_item = KanbanChecklistItem(
checklist_id=item_data.checklist_id,
title=item_data.title,
is_completed=item_data.is_completed,
position=item_data.position
)
session.add(new_item)
session.commit()
session.refresh(new_item)
return new_item
@router.put("/checklist-items/{item_id}", response_model=KanbanChecklistItemResponse)
def update_checklist_item(
item_id: int,
item_data: KanbanChecklistItemUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Update a checklist item"""
item = session.get(KanbanChecklistItem, item_id)
if not item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Checklist item not found"
)
# Check access via item -> checklist -> card -> board -> channel -> department
board = session.get(KanbanBoard, item.checklist.card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Update fields
if item_data.title is not None:
item.title = item_data.title
if item_data.is_completed is not None:
item.is_completed = item_data.is_completed
if item_data.position is not None:
item.position = item_data.position
session.commit()
session.refresh(item)
return item
@router.delete("/checklist-items/{item_id}")
def delete_checklist_item(
item_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Delete a checklist item"""
item = session.get(KanbanChecklistItem, item_id)
if not item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Checklist item not found"
)
# Check access
board = session.get(KanbanBoard, item.checklist.card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
session.delete(item)
session.commit()
return {"message": "Checklist item deleted successfully"}
@router.get("/cards/{card_id}/checklists", response_model=List[KanbanChecklistWithItems])
def get_card_checklists(
card_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get all checklists for a specific card"""
# Check if card exists and user has access
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Get all checklists for the card with their items
checklists = session.exec(
select(KanbanChecklist)
.where(KanbanChecklist.card_id == card_id)
.order_by(KanbanChecklist.position)
).all()
result = []
for checklist in checklists:
items = session.exec(
select(KanbanChecklistItem)
.where(KanbanChecklistItem.checklist_id == checklist.id)
.order_by(KanbanChecklistItem.position)
).all()
checklist_data = KanbanChecklistWithItems.from_orm(checklist)
checklist_data.items = [KanbanChecklistItemResponse.from_orm(item) for item in items]
result.append(checklist_data)
return result
# Comment endpoints
@router.post("/cards/{card_id}/comments", response_model=KanbanCardCommentResponse, status_code=status.HTTP_201_CREATED)
def create_card_comment(
card_id: int,
comment_data: KanbanCardCommentCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Create a comment on a kanban card"""
# Check if card exists and user has access
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
new_comment = KanbanCardComment(
card_id=card_id,
user_id=current_user.id,
content=comment_data.content
)
session.add(new_comment)
session.commit()
session.refresh(new_comment)
return KanbanCardCommentResponse.from_orm(new_comment)
@router.get("/cards/{card_id}/comments", response_model=List[KanbanCardCommentResponse])
def get_card_comments(
card_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get all comments for a kanban card"""
# Check if card exists and user has access
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
comments = session.exec(
select(KanbanCardComment)
.where(KanbanCardComment.card_id == card_id)
.order_by(KanbanCardComment.created_at)
).all()
return [KanbanCardCommentResponse.from_orm(comment) for comment in comments]
@router.get("/cards/{card_id}/activity")
def get_card_activity(
card_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get activity log for a kanban card"""
# Check if card exists and user has access
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
activities = session.exec(
select(KanbanCardActivityLog)
.where(KanbanCardActivityLog.card_id == card_id)
.order_by(KanbanCardActivityLog.created_at.desc())
).all()
# Format response with user information
result = []
for activity in activities:
user = session.get(User, activity.user_id)
result.append({
'id': activity.id,
'action': activity.action,
'field_name': activity.field_name,
'old_value': activity.old_value,
'new_value': activity.new_value,
'created_at': activity.created_at,
'user': {
'id': user.id,
'username': user.username,
'full_name': user.full_name
} if user else None
})
return result
@router.put("/comments/{comment_id}", response_model=KanbanCardCommentResponse)
def update_card_comment(
comment_id: int,
comment_data: KanbanCardCommentUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Update a comment (only by the author)"""
comment = session.get(KanbanCardComment, comment_id)
if not comment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Comment not found"
)
# Only the author can update their comment
if comment.user_id != current_user.id and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Can only update your own comments"
)
if comment_data.content is not None:
comment.content = comment_data.content
comment.updated_at = datetime.utcnow()
session.add(comment)
session.commit()
session.refresh(comment)
return KanbanCardCommentResponse.from_orm(comment)
@router.delete("/comments/{comment_id}")
def delete_card_comment(
comment_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Delete a comment (by author or admin)"""
comment = session.get(KanbanCardComment, comment_id)
if not comment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Comment not found"
)
# Only the author or admin can delete
if comment.user_id != current_user.id and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Can only delete your own comments"
)
session.delete(comment)
session.commit()
return {"message": "Comment deleted successfully"}
# Attachment endpoints
@router.post("/cards/{card_id}/attachments", response_model=KanbanCardAttachmentResponse, status_code=status.HTTP_201_CREATED)
def upload_card_attachment(
card_id: int,
file: UploadFile = File(...),
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Upload a file attachment to a kanban card"""
# Check if card exists and user has access
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Create uploads directory if it doesn't exist
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "kanban"
upload_dir.mkdir(parents=True, exist_ok=True)
# Generate unique filename
file_extension = Path(file.filename).suffix
unique_filename = f"{datetime.utcnow().timestamp()}_{current_user.id}_{file.filename}"
file_path = upload_dir / unique_filename
# Save file
try:
with open(file_path, "wb") as buffer:
content = file.file.read()
buffer.write(content)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to save file: {str(e)}"
)
# Create attachment record
attachment = KanbanCardAttachment(
card_id=card_id,
filename=unique_filename,
original_filename=file.filename,
mime_type=file.content_type or "application/octet-stream",
file_size=len(content),
file_path=str(file_path.relative_to(Path(__file__).parent.parent.parent)),
uploader_id=current_user.id
)
session.add(attachment)
session.commit()
session.refresh(attachment)
return KanbanCardAttachmentResponse.from_orm(attachment)
@router.get("/cards/{card_id}/attachments", response_model=List[KanbanCardAttachmentResponse])
def get_card_attachments(
card_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get all attachments for a kanban card"""
# Check if card exists and user has access
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
attachments = session.exec(
select(KanbanCardAttachment)
.where(KanbanCardAttachment.card_id == card_id)
.order_by(KanbanCardAttachment.uploaded_at)
).all()
return [KanbanCardAttachmentResponse.from_orm(att) for att in attachments]
@router.delete("/attachments/{attachment_id}")
def delete_card_attachment(
attachment_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Delete an attachment (by uploader or admin)"""
attachment = session.get(KanbanCardAttachment, attachment_id)
if not attachment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Attachment not found"
)
# Only the uploader or admin can delete
if attachment.uploader_id != current_user.id and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Can only delete your own attachments"
)
# Delete file from disk
try:
file_path = Path(__file__).parent.parent.parent / attachment.file_path
if file_path.exists():
file_path.unlink()
except Exception as e:
# Log error but don't fail the operation
print(f"Failed to delete file {attachment.file_path}: {e}")
session.delete(attachment)
session.commit()
return {"message": "Attachment deleted successfully"}
@router.get("/attachments/{attachment_id}/download")
def download_attachment(
attachment_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Download a file attachment"""
from fastapi.responses import FileResponse
attachment = session.get(KanbanCardAttachment, attachment_id)
if not attachment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Attachment not found"
)
# Check access via card -> board -> channel -> department
card = session.get(KanbanCard, attachment.card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
board = session.get(KanbanBoard, card.column.board_id)
if not board:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Board not found"
)
channel = session.get(Channel, board.channel_id)
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Channel not found"
)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Get file path
file_path = Path(__file__).parent.parent.parent / attachment.file_path
if not file_path.exists():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found on disk"
)
return FileResponse(
path=file_path,
filename=attachment.original_filename,
media_type=attachment.mime_type
)
# Time tracking endpoints
@router.post("/cards/{card_id}/time/start", response_model=KanbanTimeEntryResponse, status_code=status.HTTP_201_CREATED)
def start_time_tracking(
card_id: int,
time_data: KanbanTimeEntryCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Start time tracking for a kanban card"""
# Check if card exists and user has access
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Check if user already has a running timer for this card
running_entry = session.exec(
select(KanbanTimeEntry)
.where(
KanbanTimeEntry.card_id == card_id,
KanbanTimeEntry.user_id == current_user.id,
KanbanTimeEntry.is_running == True
)
).first()
if running_entry:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Time tracking already running for this card"
)
new_entry = KanbanTimeEntry(
card_id=card_id,
user_id=current_user.id,
description=time_data.description,
start_time=datetime.utcnow(),
is_running=True
)
session.add(new_entry)
session.commit()
session.refresh(new_entry)
return KanbanTimeEntryResponse.from_orm(new_entry)
@router.put("/time/{entry_id}/stop", response_model=KanbanTimeEntryResponse)
def stop_time_tracking(
entry_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Stop time tracking for a kanban card"""
entry = session.get(KanbanTimeEntry, entry_id)
if not entry:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Time entry not found"
)
# Only the user who started tracking can stop it
if entry.user_id != current_user.id and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Can only stop your own time tracking"
)
if not entry.is_running:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Time tracking is not running"
)
end_time = datetime.utcnow()
duration = int((end_time - entry.start_time).total_seconds() / 60) # Duration in minutes
entry.end_time = end_time
entry.duration_minutes = duration
entry.is_running = False
session.add(entry)
session.commit()
session.refresh(entry)
return KanbanTimeEntryResponse.from_orm(entry)
@router.get("/cards/{card_id}/time", response_model=List[KanbanTimeEntryResponse])
def get_card_time_entries(
card_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get all time entries for a kanban card"""
# Check if card exists and user has access
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
entries = session.exec(
select(KanbanTimeEntry)
.where(KanbanTimeEntry.card_id == card_id)
.order_by(KanbanTimeEntry.start_time.desc())
).all()
return [KanbanTimeEntryResponse.from_orm(entry) for entry in entries]
# Custom field endpoints
@router.post("/boards/{board_id}/custom-fields", response_model=KanbanCustomFieldResponse, status_code=status.HTTP_201_CREATED)
def create_custom_field(
board_id: int,
field_data: KanbanCustomFieldCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Create a custom field for a kanban board"""
# Check if board exists and user has access
board = session.get(KanbanBoard, board_id)
if not board:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Board not found"
)
# Check access via channel -> department
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
new_field = KanbanCustomField(
board_id=board_id,
name=field_data.name,
field_type=field_data.field_type,
options=field_data.options,
is_required=field_data.is_required,
position=field_data.position
)
session.add(new_field)
session.commit()
session.refresh(new_field)
return KanbanCustomFieldResponse.from_orm(new_field)
@router.get("/boards/{board_id}/custom-fields", response_model=List[KanbanCustomFieldResponse])
def get_board_custom_fields(
board_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get all custom fields for a kanban board"""
# Check if board exists and user has access
board = session.get(KanbanBoard, board_id)
if not board:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Board not found"
)
# Check access via channel -> department
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
fields = session.exec(
select(KanbanCustomField)
.where(KanbanCustomField.board_id == board_id)
.order_by(KanbanCustomField.position)
).all()
return [KanbanCustomFieldResponse.from_orm(field) for field in fields]
@router.put("/custom-fields/{field_id}", response_model=KanbanCustomFieldResponse)
def update_custom_field(
field_id: int,
field_data: KanbanCustomFieldUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Update a custom field"""
field = session.get(KanbanCustomField, field_id)
if not field:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Custom field not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, field.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Update fields
for attr, value in field_data.dict(exclude_unset=True).items():
if value is not None:
setattr(field, attr, value)
session.add(field)
session.commit()
session.refresh(field)
return KanbanCustomFieldResponse.from_orm(field)
@router.delete("/custom-fields/{field_id}")
def delete_custom_field(
field_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Delete a custom field"""
field = session.get(KanbanCustomField, field_id)
if not field:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Custom field not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, field.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
session.delete(field)
session.commit()
return {"message": "Custom field deleted successfully"}
# Custom field value endpoints
@router.put("/cards/{card_id}/custom-fields/{field_id}", response_model=KanbanCustomFieldValueResponse)
def set_custom_field_value(
card_id: int,
field_id: int,
value_data: KanbanCustomFieldValueCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Set or update a custom field value for a card"""
# Check if card and field exist
card = session.get(KanbanCard, card_id)
field = session.get(KanbanCustomField, field_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
if not field:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Custom field not found"
)
# Check if field belongs to the card's board
board = session.get(KanbanBoard, card.column.board_id)
if field.board_id != board.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Field does not belong to this board"
)
# Check access via channel -> department
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Find existing value or create new one
existing_value = session.exec(
select(KanbanCustomFieldValue)
.where(
KanbanCustomFieldValue.field_id == field_id,
KanbanCustomFieldValue.card_id == card_id
)
).first()
if existing_value:
existing_value.value = value_data.value
existing_value.updated_at = datetime.utcnow()
session.add(existing_value)
session.commit()
session.refresh(existing_value)
return KanbanCustomFieldValueResponse.from_orm(existing_value)
else:
new_value = KanbanCustomFieldValue(
field_id=field_id,
card_id=card_id,
value=value_data.value
)
session.add(new_value)
session.commit()
session.refresh(new_value)
return KanbanCustomFieldValueResponse.from_orm(new_value)
@router.get("/cards/{card_id}/custom-fields", response_model=List[KanbanCustomFieldValueResponse])
def get_card_custom_field_values(
card_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get all custom field values for a card"""
# Check if card exists and user has access
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
values = session.exec(
select(KanbanCustomFieldValue)
.where(KanbanCustomFieldValue.card_id == card_id)
).all()
return [KanbanCustomFieldValueResponse.from_orm(value) for value in values]
# Template endpoints
@router.post("/boards/{board_id}/templates", response_model=KanbanCardTemplateResponse, status_code=status.HTTP_201_CREATED)
def create_card_template(
board_id: int,
template_data: KanbanCardTemplateCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Create a card template for a kanban board"""
# Check if board exists and user has access
board = session.get(KanbanBoard, board_id)
if not board:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Board not found"
)
# Check access via channel -> department
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
new_template = KanbanCardTemplate(
board_id=board_id,
name=template_data.name,
description=template_data.description,
template_data=template_data.template_data,
is_default=template_data.is_default
)
session.add(new_template)
session.commit()
session.refresh(new_template)
return KanbanCardTemplateResponse.from_orm(new_template)
@router.get("/boards/{board_id}/templates", response_model=List[KanbanCardTemplateResponse])
def get_board_templates(
board_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get all templates for a kanban board"""
# Check if board exists and user has access
board = session.get(KanbanBoard, board_id)
if not board:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Board not found"
)
# Check access via channel -> department
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
templates = session.exec(
select(KanbanCardTemplate)
.where(KanbanCardTemplate.board_id == board_id)
.order_by(KanbanCardTemplate.name)
).all()
return [KanbanCardTemplateResponse.from_orm(template) for template in templates]
@router.post("/cards/from-template/{template_id}", response_model=KanbanCardResponse, status_code=status.HTTP_201_CREATED)
def create_card_from_template(
template_id: int,
column_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Create a card from a template"""
# Check if template and column exist
template = session.get(KanbanCardTemplate, template_id)
column = session.get(KanbanColumn, column_id)
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Template not found"
)
if not column:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Column not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, template.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Parse template data (assuming it's JSON)
import json
try:
template_dict = json.loads(template.template_data)
except json.JSONDecodeError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid template data"
)
# Create card from template
new_card = KanbanCard(
column_id=column_id,
title=template_dict.get('title', template.name),
description=template_dict.get('description', ''),
priority=template_dict.get('priority', 'medium'),
labels=template_dict.get('labels', None),
estimated_time=template_dict.get('estimated_time'),
assignee_id=template_dict.get('assignee_id')
)
session.add(new_card)
session.commit()
session.refresh(new_card)
return KanbanCardResponse.from_orm(new_card)
# Bulk operations endpoints
@router.post("/cards/bulk/move")
def bulk_move_cards(
move_data: dict, # {"card_ids": [1,2,3], "column_id": 5, "position": 0}
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Move multiple cards to a new column/position"""
card_ids = move_data.get('card_ids', [])
column_id = move_data.get('column_id')
position = move_data.get('position', 0)
if not card_ids or not column_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="card_ids and column_id are required"
)
# Check if target column exists
column = session.get(KanbanColumn, column_id)
if not column:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Target column not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Get and update cards
cards = session.exec(
select(KanbanCard).where(KanbanCard.id.in_(card_ids))
).all()
updated_cards = []
for i, card in enumerate(cards):
card.column_id = column_id
card.position = position + i
updated_cards.append(card)
session.add_all(updated_cards)
session.commit()
return {"message": f"Moved {len(updated_cards)} cards successfully"}
@router.delete("/cards/bulk")
def bulk_delete_cards(
delete_data: dict, # {"card_ids": [1,2,3]}
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Delete multiple cards"""
card_ids = delete_data.get('card_ids', [])
if not card_ids:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="card_ids are required"
)
# Get cards and check access
cards = session.exec(
select(KanbanCard).where(KanbanCard.id.in_(card_ids))
).all()
if not cards:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No cards found"
)
# Check access for all cards (they should all be from the same board)
board_id = cards[0].column.board_id
board = session.get(KanbanBoard, board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Delete cards (cascading will handle related data)
for card in cards:
session.delete(card)
session.commit()
return {"message": f"Deleted {len(cards)} cards successfully"}
# Search and filter endpoints
@router.get("/boards/{board_id}/search")
def search_cards(
board_id: int,
q: str = "", # Search query
assignee_id: Optional[int] = None,
priority: Optional[str] = None,
labels: Optional[str] = None,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Search and filter cards in a board"""
# Check if board exists and user has access
board = session.get(KanbanBoard, board_id)
if not board:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Board not found"
)
# Check access via channel -> department
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Build query
query = select(KanbanCard).where(KanbanCard.column.has(KanbanColumn.board_id == board_id))
# Apply filters
if q:
query = query.where(
(KanbanCard.title.contains(q)) |
(KanbanCard.description.contains(q))
)
if assignee_id:
query = query.where(KanbanCard.assignee_id == assignee_id)
if priority:
query = query.where(KanbanCard.priority == priority)
if labels:
# Search for cards that contain any of the specified labels
label_conditions = []
for label in labels.split(','):
label_conditions.append(KanbanCard.labels.contains(label.strip()))
if label_conditions:
query = query.where(or_(*label_conditions))
cards = session.exec(query).all()
return [KanbanCardResponse.from_orm(card) for card in cards]
# Extended card endpoint with all features
@router.get("/cards/{card_id}/extended", response_model=KanbanCardExtendedResponse)
def get_card_extended(
card_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user)
):
"""Get a card with all extended features (comments, attachments, time entries, custom fields)"""
# Check if card exists and user has access
card = session.get(KanbanCard, card_id)
if not card:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found"
)
# Check access via board -> channel -> department
board = session.get(KanbanBoard, card.column.board_id)
channel = session.get(Channel, board.channel_id)
user_departments = [dept.id for dept in current_user.departments]
if channel.department_id not in user_departments and current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Build extended response
response = KanbanCardExtendedResponse.from_orm(card)
# Load comments
comments = session.exec(
select(KanbanCardComment)
.where(KanbanCardComment.card_id == card_id)
.order_by(KanbanCardComment.created_at)
).all()
response.comments = [KanbanCardCommentResponse.from_orm(comment) for comment in comments]
# Load attachments
attachments = session.exec(
select(KanbanCardAttachment)
.where(KanbanCardAttachment.card_id == card_id)
.order_by(KanbanCardAttachment.uploaded_at)
).all()
response.attachments = [KanbanCardAttachmentResponse.from_orm(att) for att in attachments]
# Load time entries
time_entries = session.exec(
select(KanbanTimeEntry)
.where(KanbanTimeEntry.card_id == card_id)
.order_by(KanbanTimeEntry.start_time.desc())
).all()
response.time_entries = [KanbanTimeEntryResponse.from_orm(entry) for entry in time_entries]
# Load custom field values
custom_values = session.exec(
select(KanbanCustomFieldValue)
.where(KanbanCustomFieldValue.card_id == card_id)
).all()
response.custom_field_values = [KanbanCustomFieldValueResponse.from_orm(value) for value in custom_values]
# Load checklists with items
checklists = session.exec(
select(KanbanChecklist)
.where(KanbanChecklist.card_id == card_id)
.order_by(KanbanChecklist.position)
).all()
checklist_data = []
for checklist in checklists:
items = session.exec(
select(KanbanChecklistItem)
.where(KanbanChecklistItem.checklist_id == checklist.id)
.order_by(KanbanChecklistItem.position)
).all()
checklist_response = KanbanChecklistWithItems.from_orm(checklist)
checklist_response.items = [KanbanChecklistItemResponse.from_orm(item) for item in items]
checklist_data.append(checklist_response)
response.checklists = checklist_data
return response