separate kanban routes to smaller files

This commit is contained in:
david 2026-02-26 13:02:37 +03:00
parent 56510d71d5
commit 94e157da4b
12 changed files with 682 additions and 521 deletions

View file

@ -1,515 +0,0 @@
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/<int:board_id>", 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/<int:board_id>", 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/<int:board_id>", 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/<int:board_id>/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/<int:list_id>", 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/<int:list_id>", 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/<int:list_id>/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/<int:card_id>", 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/<int:card_id>", 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/<int:card_id>", 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/<int:board_id>/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/<int:board_id>/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/<int:card_id>/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/<int:card_id>/labels/<int:label_id>", 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/<int:card_id>/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/<int:checklist_id>/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/<int:item_id>", 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/<int:checklist_id>", 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/<int:item_id>", 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/<int:card_id>/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/<int:card_id>/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/<int:comment_id>", 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/<int:comment_id>", 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

View file

@ -0,0 +1,14 @@
from flask import Blueprint
# Create the kanban blueprint that will be used by all route modules
kanban_bp = Blueprint("kanban", __name__)
# Import all route modules to register their routes to this blueprint
# fmt: off
from . import (boards, cards, checklists, comments, labels, # noqa: F401 E402
lists)
# fmt: on
__all__ = ["kanban_bp"]

View file

@ -0,0 +1,120 @@
from datetime import UTC, datetime
from flask import 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
from app.models import Board, Card, CardLabel, Label, List
from app.schemas import (BoardCreateRequest, BoardResponse,
BoardWithDetailsResponse)
from . import kanban_bp
@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 [BoardResponse.model_validate(board).model_dump() for board in boards], 200
@kanban_bp.route("/boards/<int:board_id>", 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()]
response = BoardWithDetailsResponse(**board_dict)
return response.model_dump(), 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 BoardResponse.model_validate(board).model_dump(), 201
@kanban_bp.route("/boards/<int:board_id>", 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 BoardResponse.model_validate(board).model_dump(), 200
@kanban_bp.route("/boards/<int:board_id>", 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

View file

@ -0,0 +1,128 @@
from datetime import UTC, datetime
from flask import request
from flask_jwt_extended import jwt_required
from flask_pydantic import validate
from app import db
from app.decorators import load_card_owned, load_list_owned
from app.models import Board, Card, CardLabel, Label, List
from app.schemas import (CardCreateRequest, CardResponse,
CardWithDetailsResponse)
from . import kanban_bp
@kanban_bp.route("/lists/<int:list_id>/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 CardResponse.model_validate(card).model_dump(), 201
@kanban_bp.route("/cards/<int:card_id>", methods=["GET"])
@jwt_required()
@load_card_owned
def get_card(card_id, card):
"""Get a single card with full details"""
from app.models import User
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)
response = CardWithDetailsResponse(**card_dict)
return response.model_dump(), 200
@kanban_bp.route("/cards/<int:card_id>", 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 CardResponse.model_validate(card).model_dump(), 200
@kanban_bp.route("/cards/<int:card_id>", 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

View file

@ -0,0 +1,87 @@
from flask_jwt_extended import jwt_required
from flask_pydantic import validate
from app import db
from app.decorators import (load_card_owned, load_check_item_owned,
load_checklist_owned)
from app.models import CheckItem, Checklist
from app.schemas import CheckItemCreateRequest, ChecklistCreateRequest
from . import kanban_bp
@kanban_bp.route("/cards/<int:card_id>/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/<int:checklist_id>/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/<int:item_id>", 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/<int:checklist_id>", 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/<int:item_id>", 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

View file

@ -0,0 +1,72 @@
from flask_jwt_extended import jwt_required
from flask_pydantic import validate
from app import db
from app.decorators import load_card_owned, load_comment_owned
from app.models import Comment, User
from app.schemas import (CommentCreateRequest, CommentResponse,
CommentWithUserResponse)
from . import kanban_bp
@kanban_bp.route("/cards/<int:card_id>/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
response = CommentWithUserResponse(**comment_dict)
comments.append(response.model_dump())
return comments, 200
@kanban_bp.route("/cards/<int:card_id>/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"""
from app.decorators import get_current_user_id
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 CommentResponse.model_validate(comment).model_dump(), 201
@kanban_bp.route("/comments/<int:comment_id>", 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 CommentResponse.model_validate(comment).model_dump(), 200
@kanban_bp.route("/comments/<int:comment_id>", 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

View file

@ -0,0 +1,79 @@
from flask import request
from flask_jwt_extended import jwt_required
from flask_pydantic import validate
from app import db
from app.decorators import load_board_owned, load_card_owned
from app.models import CardLabel, Label
from app.schemas import LabelCreateRequest
from . import kanban_bp
@kanban_bp.route("/boards/<int:board_id>/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/<int:board_id>/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/<int:card_id>/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/<int:card_id>/labels/<int:label_id>", 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

View file

@ -0,0 +1,55 @@
from flask import request
from flask_jwt_extended import jwt_required
from flask_pydantic import validate
from app import db
from app.decorators import load_board_owned, load_list_owned
from app.models import List
from app.schemas import ListCreateRequest
from . import kanban_bp
@kanban_bp.route("/boards/<int:board_id>/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/<int:list_id>", 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/<int:list_id>", 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

View file

@ -1,9 +1,12 @@
"""Pydantic schemas for request/response validation""" """Pydantic schemas for request/response validation"""
from app.schemas.board import BoardCreateRequest, BoardResponse from app.schemas.board import (BoardCreateRequest, BoardResponse,
from app.schemas.card import CardCreateRequest, CardResponse BoardWithDetailsResponse)
from app.schemas.card import (CardCreateRequest, CardResponse,
CardWithDetailsResponse)
from app.schemas.check_item import CheckItemCreateRequest, CheckItemResponse from app.schemas.check_item import CheckItemCreateRequest, CheckItemResponse
from app.schemas.checklist import ChecklistCreateRequest, ChecklistResponse from app.schemas.checklist import ChecklistCreateRequest, ChecklistResponse
from app.schemas.comment import CommentCreateRequest, CommentResponse from app.schemas.comment import (CommentCreateRequest, CommentResponse,
CommentWithUserResponse)
from app.schemas.label import LabelCreateRequest, LabelResponse from app.schemas.label import LabelCreateRequest, LabelResponse
from app.schemas.list_model import ListCreateRequest, ListResponse from app.schemas.list_model import ListCreateRequest, ListResponse
from app.schemas.product import ProductCreateRequest, ProductResponse from app.schemas.product import ProductCreateRequest, ProductResponse
@ -33,4 +36,7 @@ __all__ = [
# Comment schemas # Comment schemas
"CommentCreateRequest", "CommentCreateRequest",
"CommentResponse", "CommentResponse",
"BoardWithDetailsResponse",
"CardWithDetailsResponse",
"CommentWithUserResponse",
] ]

View file

@ -1,6 +1,6 @@
"""Pydantic schemas for Board model""" """Pydantic schemas for Board model"""
from datetime import datetime from datetime import datetime
from typing import Any, Dict, Optional from typing import Any, Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
@ -66,3 +66,42 @@ class BoardResponse(BaseModel):
label_names: Optional[Dict[str, str]] = None label_names: Optional[Dict[str, str]] = None
limits: Optional[Dict[str, Any]] = None limits: Optional[Dict[str, Any]] = None
date_last_activity: Optional[datetime] = None date_last_activity: Optional[datetime] = None
class BoardWithDetailsResponse(BaseModel):
"""Schema for board response with lists, cards, and labels"""
model_config = ConfigDict(
from_attributes=True,
json_schema_extra={
"example": {
"id": 1,
"name": "Project Alpha",
"description": "Main project board",
"closed": False,
"url": "https://trello.com/b/example",
"short_link": "abc123",
"short_url": "https://trello.com/c/abc123",
"prefs": {"permissionLevel": "private"},
"label_names": {"green": "Done"},
"limits": {"lists": {"open": 10}},
"date_last_activity": "2024-01-15T10:30:00",
"lists": [],
"labels": [],
}
},
)
id: int
name: str
description: Optional[str] = None
closed: bool
url: Optional[str] = None
short_link: Optional[str] = None
short_url: Optional[str] = None
prefs: Optional[Dict[str, Any]] = None
label_names: Optional[Dict[str, str]] = None
limits: Optional[Dict[str, Any]] = None
date_last_activity: Optional[datetime] = None
lists: List[Dict[str, Any]] = Field(default_factory=list)
labels: List[Dict[str, Any]] = Field(default_factory=list)

View file

@ -1,6 +1,6 @@
"""Pydantic schemas for Card model""" """Pydantic schemas for Card model"""
from datetime import datetime from datetime import datetime
from typing import Any, Dict, Optional from typing import Any, Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
@ -70,3 +70,48 @@ class CardResponse(BaseModel):
cover: Optional[str] = None cover: Optional[str] = None
desc_data: Optional[str] = None desc_data: Optional[str] = None
date_last_activity: Optional[datetime] = None date_last_activity: Optional[datetime] = None
class CardWithDetailsResponse(BaseModel):
"""Schema for card response with labels, checklists, and comments"""
model_config = ConfigDict(
from_attributes=True,
json_schema_extra={
"example": {
"id": 1,
"name": "Task 1",
"description": "Complete the task",
"closed": False,
"pos": 65535.0,
"list_id": 1,
"board_id": 1,
"due": "2024-12-31T23:59:59",
"due_complete": False,
"badges": {"votes": 0},
"cover": "https://example.com/cover.jpg",
"desc_data": "<p>Task description</p>",
"date_last_activity": "2024-01-15T10:30:00",
"labels": [],
"checklists": [],
"comments": [],
}
},
)
id: int
name: str
description: Optional[str] = None
closed: bool
pos: float
list_id: int
board_id: int
due: Optional[datetime] = None
due_complete: bool
badges: Optional[Dict[str, Any]] = None
cover: Optional[str] = None
desc_data: Optional[str] = None
date_last_activity: Optional[datetime] = None
labels: List[Dict[str, Any]] = Field(default_factory=list)
checklists: List[Dict[str, Any]] = Field(default_factory=list)
comments: List[Dict[str, Any]] = Field(default_factory=list)

View file

@ -1,6 +1,6 @@
"""Pydantic schemas for Comment model""" """Pydantic schemas for Comment model"""
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Any, Dict, Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
@ -42,3 +42,34 @@ class CommentResponse(BaseModel):
user_id: int user_id: int
created_at: Optional[datetime] = None created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None updated_at: Optional[datetime] = None
class CommentWithUserResponse(BaseModel):
"""Schema for comment response with user information"""
model_config = ConfigDict(
from_attributes=True,
json_schema_extra={
"example": {
"id": 1,
"text": "This is a comment",
"card_id": 1,
"user_id": 1,
"created_at": "2024-01-15T10:30:00",
"updated_at": "2024-01-15T10:30:00",
"user": {
"id": 1,
"email": "user@example.com",
"username": "johndoe",
},
}
},
)
id: int
text: str
card_id: int
user_id: int
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
user: Optional[Dict[str, Any]] = None