"""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.models import Board, Card, Epic from app.schemas import EpicCreateRequest, EpicResponse, EpicUpdateRequest from . import kanban_bp @kanban_bp.route("/boards//epics", methods=["GET"]) @jwt_required() 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.query.filter_by(board_id=board_id).all() return [EpicResponse.model_validate(epic).model_dump() for epic in epics], 200 @kanban_bp.route("/boards//epics", methods=["POST"]) @jwt_required() @validate(body=EpicCreateRequest) 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 EpicResponse.model_validate(epic).model_dump(), 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.query.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) 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 EpicResponse.model_validate(epic).model_dump(), 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.query.filter_by(epic_id=epic_id).update({"epic_id": None}) # Delete epic db.session.delete(epic) db.session.commit() return {"message": "Epic deleted successfully"}, 200 @kanban_bp.route("/cards//epics", methods=["POST"]) @jwt_required() 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.to_dict(), 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