from datetime import UTC, datetime from flask import Blueprint, jsonify, request from flask_jwt_extended import jwt_required from flask_pydantic import validate from app import db from app.decorators import (get_current_user_id, load_board_owned, load_card_owned, load_check_item_owned, load_checklist_owned, load_comment_owned, load_list_owned) from app.models import (Board, Card, CardLabel, CheckItem, Checklist, Comment, Label, List, User) from app.schemas import (BoardCreateRequest, CardCreateRequest, CheckItemCreateRequest, ChecklistCreateRequest, CommentCreateRequest, LabelCreateRequest, ListCreateRequest) kanban_bp = Blueprint("kanban", __name__) # ============================================================================= # BOARD ROUTES # ============================================================================= @kanban_bp.route("/boards", methods=["GET"]) @jwt_required() def get_boards(): """Get all boards for current user""" user_id = get_current_user_id() boards = Board.query.filter_by(user_id=user_id).all() return jsonify([board.to_dict() for board in boards]), 200 @kanban_bp.route("/boards/", methods=["GET"]) @jwt_required() @load_board_owned def get_board(board_id, board): """Get a single board with all its details""" # Get all lists for this board lists_data = [] for lst in board.lists.filter_by(closed=False).order_by(List.pos).all(): cards_data = [] for card in lst.cards.filter_by(closed=False).order_by(Card.pos).all(): card_dict = card.to_dict() # Add labels for this card card_dict["labels"] = [ label.to_dict() for label in ( db.session.query(Label) .join(CardLabel) .filter(CardLabel.card_id == card.id) .all() ) ] cards_data.append(card_dict) lists_data.append({**lst.to_dict(), "cards": cards_data}) board_dict = board.to_dict() board_dict["lists"] = lists_data board_dict["labels"] = [label.to_dict() for label in board.labels.all()] return jsonify(board_dict), 200 @kanban_bp.route("/boards", methods=["POST"]) @jwt_required() @validate(body=BoardCreateRequest) def create_board(body: BoardCreateRequest): """Create a new board""" user_id = get_current_user_id() board = Board( name=body.name, description=body.description, user_id=user_id, url=body.url, short_link=body.short_link, short_url=body.short_url, prefs=body.prefs, label_names=body.label_names, limits=body.limits, ) db.session.add(board) db.session.commit() return board.to_dict(), 201 @kanban_bp.route("/boards/", methods=["PUT"]) @jwt_required() @load_board_owned @validate(body=BoardCreateRequest) def update_board(board_id, board, body: BoardCreateRequest): """Update a board""" board.name = body.name if body.description is not None: board.description = body.description if request.json.get("closed") is not None: board.closed = request.json.get("closed") if body.url is not None: board.url = body.url if body.short_url is not None: board.short_url = body.short_url if body.prefs is not None: board.prefs = body.prefs if body.label_names is not None: board.label_names = body.label_names if body.limits is not None: board.limits = body.limits board.date_last_activity = datetime.now(UTC) db.session.commit() return board.to_dict(), 200 @kanban_bp.route("/boards/", methods=["DELETE"]) @jwt_required() @load_board_owned def delete_board(board_id, board): """Delete a board""" db.session.delete(board) db.session.commit() return {"message": "Board deleted"}, 200 # ============================================================================= # LIST ROUTES # ============================================================================= @kanban_bp.route("/boards//lists", methods=["POST"]) @jwt_required() @load_board_owned @validate(body=ListCreateRequest) def create_list(board_id, board, body: ListCreateRequest): """Create a new list in a board""" lst = List( name=body.name, board_id=board_id, pos=body.pos, ) db.session.add(lst) db.session.commit() return lst.to_dict(), 201 @kanban_bp.route("/lists/", methods=["PUT"]) @jwt_required() @load_list_owned @validate(body=ListCreateRequest) def update_list(list_id, lst, body: ListCreateRequest): """Update a list""" lst.name = body.name if request.json.get("closed") is not None: lst.closed = request.json.get("closed") lst.pos = body.pos db.session.commit() return lst.to_dict(), 200 @kanban_bp.route("/lists/", methods=["DELETE"]) @jwt_required() @load_list_owned def delete_list(list_id, lst): """Delete a list""" db.session.delete(lst) db.session.commit() return {"message": "List deleted"}, 200 # ============================================================================= # CARD ROUTES # ============================================================================= @kanban_bp.route("/lists//cards", methods=["POST"]) @jwt_required() @load_list_owned @validate(body=CardCreateRequest) def create_card(list_id, lst, body: CardCreateRequest): """Create a new card in a list""" card = Card( name=body.name, description=body.description, board_id=lst.board_id, list_id=list_id, pos=body.pos, due=body.due, due_complete=body.due_complete, badges=body.badges, cover=body.cover, desc_data=body.desc_data, ) db.session.add(card) db.session.commit() return card.to_dict(), 201 @kanban_bp.route("/cards/", methods=["GET"]) @jwt_required() @load_card_owned def get_card(card_id, card): """Get a single card with full details""" card_dict = card.to_dict() # Add labels card_dict["labels"] = [ label.to_dict() for label in ( db.session.query(Label) .join(CardLabel) .filter(CardLabel.card_id == card.id) .all() ) ] # Add checklists card_dict["checklists"] = [ { **checklist.to_dict(), "items": [item.to_dict() for item in checklist.check_items.all()], } for checklist in card.checklists.all() ] # Add comments card_dict["comments"] = [] for comment in card.comments.all(): comment_dict = comment.to_dict() user = db.session.get(User, comment.user_id) comment_dict["user"] = user.to_dict() if user else None card_dict["comments"].append(comment_dict) return jsonify(card_dict), 200 @kanban_bp.route("/cards/", methods=["PUT"]) @jwt_required() @load_card_owned @validate(body=CardCreateRequest) def update_card(card_id, card, body: CardCreateRequest): """Update a card""" card.name = body.name if body.description is not None: card.description = body.description if request.json.get("closed") is not None: card.closed = request.json.get("closed") card.pos = body.pos card.due = body.due card.due_complete = body.due_complete if body.badges is not None: card.badges = body.badges if body.cover is not None: card.cover = body.cover if body.desc_data is not None: card.desc_data = body.desc_data # Handle moving card to different list if "list_id" in request.json: new_list_id = request.json["list_id"] new_list = db.session.get(List, new_list_id) if new_list and new_list.board_id == card.board_id: card.list_id = new_list_id card.date_last_activity = datetime.now(UTC) board = db.session.get(Board, card.board_id) board.date_last_activity = datetime.now(UTC) db.session.commit() return card.to_dict(), 200 @kanban_bp.route("/cards/", methods=["DELETE"]) @jwt_required() @load_card_owned def delete_card(card_id, card): """Delete a card""" db.session.delete(card) db.session.commit() return {"message": "Card deleted"}, 200 # ============================================================================= # LABEL ROUTES # ============================================================================= @kanban_bp.route("/boards//labels", methods=["GET"]) @jwt_required() @load_board_owned def get_labels(board_id, board): """Get all labels for a board""" labels = board.labels.all() return [label.to_dict() for label in labels], 200 @kanban_bp.route("/boards//labels", methods=["POST"]) @jwt_required() @load_board_owned @validate(body=LabelCreateRequest) def create_label(board_id, board, body: LabelCreateRequest): """Create a new label in a board""" label = Label( name=body.name, color=body.color, board_id=board_id, ) db.session.add(label) db.session.commit() return label.to_dict(), 201 @kanban_bp.route("/cards//labels", methods=["POST"]) @jwt_required() @load_card_owned def add_label_to_card(card_id, card): """Add a label to a card""" data = request.json label_id = data.get("label_id") if not label_id: return {"error": "Label ID is required"}, 400 label = db.session.get(Label, label_id) if not label or label.board_id != card.board_id: return {"error": "Label not found or doesn't belong to this board"}, 404 # Check if already labeled existing = CardLabel.query.filter_by(card_id=card_id, label_id=label_id).first() if existing: return {"error": "Label already added to card"}, 400 card_label = CardLabel(card_id=card_id, label_id=label_id) db.session.add(card_label) db.session.commit() return card_label.to_dict(), 201 @kanban_bp.route("/cards//labels/", methods=["DELETE"]) @jwt_required() @load_card_owned def remove_label_from_card(card_id, card, label_id): """Remove a label from a card""" card_label = CardLabel.query.filter_by(card_id=card_id, label_id=label_id).first() if not card_label: return {"error": "Label not found on card"}, 404 db.session.delete(card_label) db.session.commit() return {"message": "Label removed from card"}, 200 # ============================================================================= # CHECKLIST ROUTES # ============================================================================= @kanban_bp.route("/cards//checklists", methods=["POST"]) @jwt_required() @load_card_owned @validate(body=ChecklistCreateRequest) def create_checklist(card_id, card, body: ChecklistCreateRequest): """Create a new checklist in a card""" checklist = Checklist( name=body.name, card_id=card_id, board_id=card.board_id, pos=body.pos, ) db.session.add(checklist) db.session.commit() return checklist.to_dict(), 201 @kanban_bp.route("/checklists//items", methods=["POST"]) @jwt_required() @load_checklist_owned @validate(body=CheckItemCreateRequest) def create_check_item(checklist_id, checklist, body: CheckItemCreateRequest): """Create a new check item in a checklist""" check_item = CheckItem( name=body.name, checklist_id=checklist_id, pos=body.pos, state=body.state, due=body.due, ) db.session.add(check_item) db.session.commit() return check_item.to_dict(), 201 @kanban_bp.route("/check-items/", methods=["PUT"]) @jwt_required() @load_check_item_owned @validate(body=CheckItemCreateRequest) def update_check_item(item_id, check_item, body: CheckItemCreateRequest): """Update a check item""" check_item.name = body.name check_item.state = body.state check_item.pos = body.pos check_item.due = body.due db.session.commit() return check_item.to_dict(), 200 @kanban_bp.route("/checklists/", methods=["DELETE"]) @jwt_required() @load_checklist_owned def delete_checklist(checklist_id, checklist): """Delete a checklist""" db.session.delete(checklist) db.session.commit() return {"message": "Checklist deleted"}, 200 @kanban_bp.route("/check-items/", methods=["DELETE"]) @jwt_required() @load_check_item_owned def delete_check_item(item_id, check_item): """Delete a check item""" db.session.delete(check_item) db.session.commit() return {"message": "Check item deleted"}, 200 # ============================================================================= # COMMENT ROUTES # ============================================================================= @kanban_bp.route("/cards//comments", methods=["GET"]) @jwt_required() @load_card_owned def get_comments(card_id, card): """Get all comments for a card""" comments = [] for comment in card.comments.order_by(Comment.created_at.desc()).all(): comment_dict = comment.to_dict() user = db.session.get(User, comment.user_id) comment_dict["user"] = user.to_dict() if user else None comments.append(comment_dict) return comments, 200 @kanban_bp.route("/cards//comments", methods=["POST"]) @jwt_required() @load_card_owned @validate(body=CommentCreateRequest) def create_comment(card_id, card, body: CommentCreateRequest): """Create a new comment on a card""" user_id = get_current_user_id() comment = Comment( text=body.text, card_id=card_id, user_id=user_id, ) db.session.add(comment) db.session.commit() return comment.to_dict(), 201 @kanban_bp.route("/comments/", methods=["PUT"]) @jwt_required() @load_comment_owned @validate(body=CommentCreateRequest) def update_comment(comment_id, comment, body: CommentCreateRequest): """Update a comment""" comment.text = body.text db.session.commit() return comment.to_dict(), 200 @kanban_bp.route("/comments/", methods=["DELETE"]) @jwt_required() @load_comment_owned def delete_comment(comment_id, comment): """Delete a comment""" db.session.delete(comment) db.session.commit() return {"message": "Comment deleted"}, 200