separate kanban routes to smaller files
This commit is contained in:
parent
56510d71d5
commit
94e157da4b
12 changed files with 682 additions and 521 deletions
|
|
@ -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
|
||||
14
backend/app/routes/kanban/__init__.py
Normal file
14
backend/app/routes/kanban/__init__.py
Normal 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"]
|
||||
120
backend/app/routes/kanban/boards.py
Normal file
120
backend/app/routes/kanban/boards.py
Normal 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
|
||||
128
backend/app/routes/kanban/cards.py
Normal file
128
backend/app/routes/kanban/cards.py
Normal 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
|
||||
87
backend/app/routes/kanban/checklists.py
Normal file
87
backend/app/routes/kanban/checklists.py
Normal 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
|
||||
72
backend/app/routes/kanban/comments.py
Normal file
72
backend/app/routes/kanban/comments.py
Normal 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
|
||||
79
backend/app/routes/kanban/labels.py
Normal file
79
backend/app/routes/kanban/labels.py
Normal 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
|
||||
55
backend/app/routes/kanban/lists.py
Normal file
55
backend/app/routes/kanban/lists.py
Normal 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
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
"""Pydantic schemas for request/response validation"""
|
||||
from app.schemas.board import BoardCreateRequest, BoardResponse
|
||||
from app.schemas.card import CardCreateRequest, CardResponse
|
||||
from app.schemas.board import (BoardCreateRequest, BoardResponse,
|
||||
BoardWithDetailsResponse)
|
||||
from app.schemas.card import (CardCreateRequest, CardResponse,
|
||||
CardWithDetailsResponse)
|
||||
from app.schemas.check_item import CheckItemCreateRequest, CheckItemResponse
|
||||
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.list_model import ListCreateRequest, ListResponse
|
||||
from app.schemas.product import ProductCreateRequest, ProductResponse
|
||||
|
|
@ -33,4 +36,7 @@ __all__ = [
|
|||
# Comment schemas
|
||||
"CommentCreateRequest",
|
||||
"CommentResponse",
|
||||
"BoardWithDetailsResponse",
|
||||
"CardWithDetailsResponse",
|
||||
"CommentWithUserResponse",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""Pydantic schemas for Board model"""
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
|
@ -66,3 +66,42 @@ class BoardResponse(BaseModel):
|
|||
label_names: Optional[Dict[str, str]] = None
|
||||
limits: Optional[Dict[str, Any]] = 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)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""Pydantic schemas for Card model"""
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
|
@ -70,3 +70,48 @@ class CardResponse(BaseModel):
|
|||
cover: Optional[str] = None
|
||||
desc_data: Optional[str] = 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)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""Pydantic schemas for Comment model"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
|
@ -42,3 +42,34 @@ class CommentResponse(BaseModel):
|
|||
user_id: int
|
||||
created_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
|
||||
|
|
|
|||
Loading…
Reference in a new issue