"""Routes for Epic CRUD operations.""" from flask import request from flask_jwt_extended import jwt_required from flask_pydantic import validate from app import db from app.decorators import serialize_list_response, serialize_response from app.models import Board, Card, Epic from app.schemas import (CardResponse, EpicCreateRequest, EpicResponse, EpicUpdateRequest) from . import kanban_bp @kanban_bp.route("/boards//epics", methods=["GET"]) @jwt_required() @serialize_list_response(EpicResponse) def get_board_epics(board_id): """Get all epics for a board.""" board = db.session.get(Board, board_id) if not board: return {"error": "Board not found"}, 404 epics = Epic.active().filter_by(board_id=board_id).all() return epics, 200 @kanban_bp.route("/boards//epics", methods=["POST"]) @jwt_required() @validate(body=EpicCreateRequest) @serialize_response(EpicResponse) def create_epic(board_id, body: EpicCreateRequest): """Create a new epic for a board.""" board = db.session.get(Board, board_id) if not board: return {"error": "Board not found"}, 404 epic = Epic( board_id=board_id, name=body.name, description=body.description, content=body.content, color=body.color, closed=False, pos=body.pos, depth_limit=body.depth_limit, parent_epic_id=body.parent_epic_id, completed_list_id=body.completed_list_id, metrics={"card_count": 0, "completed_cards_count": 0}, ) db.session.add(epic) db.session.commit() return epic, 201 @kanban_bp.route("/epics/", methods=["GET"]) @jwt_required() def get_epic(epic_id): """Get a specific epic with details.""" epic = db.session.get(Epic, epic_id) if not epic: return {"error": "Epic not found"}, 404 # Get cards for this epic cards = Card.active().filter_by(epic_id=epic_id).all() epic_dict = EpicResponse.model_validate(epic).model_dump() epic_dict["cards"] = [card.to_dict() for card in cards] return epic_dict, 200 def _update_epic_fields(epic: Epic, body: EpicUpdateRequest) -> None: """Update epic fields from request body.""" if body.name is not None: epic.name = body.name if body.description is not None: epic.description = body.description if body.content is not None: epic.content = body.content if body.color is not None: epic.color = body.color if body.closed is not None: epic.closed = body.closed if body.pos is not None: epic.pos = body.pos if body.depth_limit is not None: epic.depth_limit = body.depth_limit if body.parent_epic_id is not None: epic.parent_epic_id = body.parent_epic_id if body.completed_list_id is not None: epic.completed_list_id = body.completed_list_id @kanban_bp.route("/epics/", methods=["PUT"]) @jwt_required() @validate(body=EpicUpdateRequest) @serialize_response(EpicResponse) def update_epic(epic_id, body: EpicUpdateRequest): """Update an epic.""" epic = db.session.get(Epic, epic_id) if not epic: return {"error": "Epic not found"}, 404 _update_epic_fields(epic, body) db.session.commit() return epic, 200 @kanban_bp.route("/epics/", methods=["DELETE"]) @jwt_required() def delete_epic(epic_id): """Delete an epic.""" epic = db.session.get(Epic, epic_id) if not epic: return {"error": "Epic not found"}, 404 # Unlink all cards from this epic Card.active().filter_by(epic_id=epic_id).update({"epic_id": None}) # Delete epic epic.soft_delete() db.session.commit() return {"message": "Epic deleted successfully"}, 200 @kanban_bp.route("/cards//epics", methods=["POST"]) @jwt_required() @serialize_response(CardResponse) def add_epic_to_card(card_id): """Link an epic to a card.""" card = db.session.get(Card, card_id) if not card: return {"error": "Card not found"}, 404 data = request.get_json() epic_id = data.get("epic_id") if not epic_id: return {"error": "epic_id is required"}, 400 epic = db.session.get(Epic, epic_id) if not epic: return {"error": "Epic not found"}, 404 card.epic_id = epic_id db.session.commit() return card, 200 @kanban_bp.route("/cards//epics/", methods=["DELETE"]) @jwt_required() def remove_epic_from_card(card_id, epic_id): """Unlink an epic from a card.""" card = db.session.get(Card, card_id) if not card: return {"error": "Card not found"}, 404 if card.epic_id != epic_id: return {"error": "Card is not linked to this epic"}, 400 card.epic_id = None db.session.commit() return {"message": "Epic unlinked from card successfully"}, 200