add decorators to fetch entity securely
This commit is contained in:
parent
806447e221
commit
56510d71d5
12 changed files with 730 additions and 388 deletions
159
backend/app/decorators.py
Normal file
159
backend/app/decorators.py
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import abort, g
|
||||||
|
from flask_jwt_extended import get_jwt_identity
|
||||||
|
|
||||||
|
from app.models import Board, Card, CheckItem, Checklist, Comment, List
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_user_id():
|
||||||
|
"""Helper to consistently get user ID"""
|
||||||
|
# Cache in g if you want to avoid decoding JWT multiple times per request
|
||||||
|
if not hasattr(g, "jwt_identity"):
|
||||||
|
g.jwt_identity = int(get_jwt_identity())
|
||||||
|
return g.jwt_identity
|
||||||
|
|
||||||
|
|
||||||
|
def load_board_owned(f):
|
||||||
|
"""
|
||||||
|
Loads a Board and ensures it belongs to the current user.
|
||||||
|
Injects 'board' into the route kwargs.
|
||||||
|
Aborts with 404 if not found or not owned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
user_id = get_current_user_id()
|
||||||
|
board_id = kwargs.get("board_id")
|
||||||
|
|
||||||
|
# SECURE QUERY: Filter by ID *and* User ID in the DB
|
||||||
|
board = Board.query.filter_by(id=board_id, user_id=user_id).first()
|
||||||
|
|
||||||
|
if not board:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
kwargs["board"] = board
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def load_card_owned(f):
|
||||||
|
"""
|
||||||
|
Loads a Card and ensures its Parent Board belongs to the current user.
|
||||||
|
Injects 'card' into the route kwargs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
user_id = get_current_user_id()
|
||||||
|
card_id = kwargs.get("card_id")
|
||||||
|
|
||||||
|
# Join Board to check ownership securely in one query
|
||||||
|
card = (
|
||||||
|
Card.query.join(Board)
|
||||||
|
.filter(Card.id == card_id, Board.user_id == user_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not card:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
kwargs["card"] = card
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def load_list_owned(f):
|
||||||
|
"""Loads a List ensuring Parent Board ownership."""
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
user_id = get_current_user_id()
|
||||||
|
list_id = kwargs.get("list_id")
|
||||||
|
|
||||||
|
lst = (
|
||||||
|
List.query.join(Board)
|
||||||
|
.filter(List.id == list_id, Board.user_id == user_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not lst:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
kwargs["lst"] = lst
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def load_checklist_owned(f):
|
||||||
|
"""Loads a Checklist ensuring Parent Board ownership."""
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
user_id = get_current_user_id()
|
||||||
|
checklist_id = kwargs.get("checklist_id")
|
||||||
|
|
||||||
|
checklist = (
|
||||||
|
Checklist.query.join(Card)
|
||||||
|
.join(Board)
|
||||||
|
.filter(Checklist.id == checklist_id, Board.user_id == user_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not checklist:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
kwargs["checklist"] = checklist
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def load_check_item_owned(f):
|
||||||
|
"""Loads a CheckItem ensuring Parent Board ownership."""
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
user_id = get_current_user_id()
|
||||||
|
item_id = kwargs.get("item_id")
|
||||||
|
|
||||||
|
check_item = (
|
||||||
|
CheckItem.query.join(Checklist)
|
||||||
|
.join(Card)
|
||||||
|
.join(Board)
|
||||||
|
.filter(CheckItem.id == item_id, Board.user_id == user_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not check_item:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
kwargs["check_item"] = check_item
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def load_comment_owned(f):
|
||||||
|
"""
|
||||||
|
Loads a Comment ensuring the Comment itself belongs to the user.
|
||||||
|
(Based on schema where Comment has user_id)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
user_id = get_current_user_id()
|
||||||
|
comment_id = kwargs.get("comment_id")
|
||||||
|
|
||||||
|
comment = Comment.query.filter_by(id=comment_id, user_id=user_id).first()
|
||||||
|
|
||||||
|
if not comment:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
kwargs["comment"] = comment
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
from flask_jwt_extended import get_jwt_identity, jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
|
from flask_pydantic import validate
|
||||||
|
|
||||||
from app import db
|
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,
|
from app.models import (Board, Card, CardLabel, CheckItem, Checklist, Comment,
|
||||||
Label, List, User)
|
Label, List, User)
|
||||||
|
from app.schemas import (BoardCreateRequest, CardCreateRequest,
|
||||||
|
CheckItemCreateRequest, ChecklistCreateRequest,
|
||||||
|
CommentCreateRequest, LabelCreateRequest,
|
||||||
|
ListCreateRequest)
|
||||||
|
|
||||||
kanban_bp = Blueprint("kanban", __name__)
|
kanban_bp = Blueprint("kanban", __name__)
|
||||||
|
|
||||||
|
|
@ -18,24 +27,16 @@ kanban_bp = Blueprint("kanban", __name__)
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def get_boards():
|
def get_boards():
|
||||||
"""Get all boards for current user"""
|
"""Get all boards for current user"""
|
||||||
user_id = int(get_jwt_identity())
|
user_id = get_current_user_id()
|
||||||
boards = Board.query.filter_by(user_id=user_id).all()
|
boards = Board.query.filter_by(user_id=user_id).all()
|
||||||
return jsonify([board.to_dict() for board in boards]), 200
|
return jsonify([board.to_dict() for board in boards]), 200
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/boards/<int:board_id>", methods=["GET"])
|
@kanban_bp.route("/boards/<int:board_id>", methods=["GET"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def get_board(board_id):
|
@load_board_owned
|
||||||
|
def get_board(board_id, board):
|
||||||
"""Get a single board with all its details"""
|
"""Get a single board with all its details"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
board = db.session.get(Board, board_id)
|
|
||||||
|
|
||||||
if not board:
|
|
||||||
return jsonify({"error": "Board not found"}), 404
|
|
||||||
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
# Get all lists for this board
|
# Get all lists for this board
|
||||||
lists_data = []
|
lists_data = []
|
||||||
for lst in board.lists.filter_by(closed=False).order_by(List.pos).all():
|
for lst in board.lists.filter_by(closed=False).order_by(List.pos).all():
|
||||||
|
|
@ -65,79 +66,67 @@ def get_board(board_id):
|
||||||
|
|
||||||
@kanban_bp.route("/boards", methods=["POST"])
|
@kanban_bp.route("/boards", methods=["POST"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def create_board():
|
@validate(body=BoardCreateRequest)
|
||||||
|
def create_board(body: BoardCreateRequest):
|
||||||
"""Create a new board"""
|
"""Create a new board"""
|
||||||
user_id = int(get_jwt_identity())
|
user_id = get_current_user_id()
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
if not data or not data.get("name"):
|
|
||||||
return jsonify({"error": "Board name is required"}), 400
|
|
||||||
|
|
||||||
board = Board(
|
board = Board(
|
||||||
name=data["name"],
|
name=body.name,
|
||||||
description=data.get("description"),
|
description=body.description,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
url=data.get("url"),
|
url=body.url,
|
||||||
short_link=data.get("short_link"),
|
short_link=body.short_link,
|
||||||
short_url=data.get("short_url"),
|
short_url=body.short_url,
|
||||||
prefs=data.get("prefs"),
|
prefs=body.prefs,
|
||||||
label_names=data.get("label_names"),
|
label_names=body.label_names,
|
||||||
limits=data.get("limits"),
|
limits=body.limits,
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(board)
|
db.session.add(board)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(board.to_dict()), 201
|
return board.to_dict(), 201
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/boards/<int:board_id>", methods=["PUT"])
|
@kanban_bp.route("/boards/<int:board_id>", methods=["PUT"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def update_board(board_id):
|
@load_board_owned
|
||||||
|
@validate(body=BoardCreateRequest)
|
||||||
|
def update_board(board_id, board, body: BoardCreateRequest):
|
||||||
"""Update a board"""
|
"""Update a board"""
|
||||||
user_id = int(get_jwt_identity())
|
board.name = body.name
|
||||||
board = db.session.get(Board, board_id)
|
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
|
||||||
|
|
||||||
if not board:
|
|
||||||
return jsonify({"error": "Board not found"}), 404
|
|
||||||
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
board.name = data.get("name", board.name)
|
|
||||||
board.description = data.get("description", board.description)
|
|
||||||
board.closed = data.get("closed", board.closed)
|
|
||||||
board.url = data.get("url", board.url)
|
|
||||||
board.short_url = data.get("short_url", board.short_url)
|
|
||||||
board.prefs = data.get("prefs", board.prefs)
|
|
||||||
board.label_names = data.get("label_names", board.label_names)
|
|
||||||
board.limits = data.get("limits", board.limits)
|
|
||||||
board.date_last_activity = datetime.now(UTC)
|
board.date_last_activity = datetime.now(UTC)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(board.to_dict()), 200
|
return board.to_dict(), 200
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/boards/<int:board_id>", methods=["DELETE"])
|
@kanban_bp.route("/boards/<int:board_id>", methods=["DELETE"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def delete_board(board_id):
|
@load_board_owned
|
||||||
|
def delete_board(board_id, board):
|
||||||
"""Delete a board"""
|
"""Delete a board"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
board = db.session.get(Board, board_id)
|
|
||||||
|
|
||||||
if not board:
|
|
||||||
return jsonify({"error": "Board not found"}), 404
|
|
||||||
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
db.session.delete(board)
|
db.session.delete(board)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({"message": "Board deleted"}), 200
|
return {"message": "Board deleted"}, 200
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -147,73 +136,47 @@ def delete_board(board_id):
|
||||||
|
|
||||||
@kanban_bp.route("/boards/<int:board_id>/lists", methods=["POST"])
|
@kanban_bp.route("/boards/<int:board_id>/lists", methods=["POST"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def create_list(board_id):
|
@load_board_owned
|
||||||
|
@validate(body=ListCreateRequest)
|
||||||
|
def create_list(board_id, board, body: ListCreateRequest):
|
||||||
"""Create a new list in a board"""
|
"""Create a new list in a board"""
|
||||||
user_id = int(get_jwt_identity())
|
lst = List(
|
||||||
board = db.session.get(Board, board_id)
|
name=body.name,
|
||||||
|
board_id=board_id,
|
||||||
if not board:
|
pos=body.pos,
|
||||||
return jsonify({"error": "Board not found"}), 404
|
)
|
||||||
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
if not data or not data.get("name"):
|
|
||||||
return jsonify({"error": "List name is required"}), 400
|
|
||||||
|
|
||||||
lst = List(name=data["name"], board_id=board_id, pos=data.get("pos", 65535.0))
|
|
||||||
|
|
||||||
db.session.add(lst)
|
db.session.add(lst)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(lst.to_dict()), 201
|
return lst.to_dict(), 201
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/lists/<int:list_id>", methods=["PUT"])
|
@kanban_bp.route("/lists/<int:list_id>", methods=["PUT"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def update_list(list_id):
|
@load_list_owned
|
||||||
|
@validate(body=ListCreateRequest)
|
||||||
|
def update_list(list_id, lst, body: ListCreateRequest):
|
||||||
"""Update a list"""
|
"""Update a list"""
|
||||||
user_id = int(get_jwt_identity())
|
lst.name = body.name
|
||||||
lst = db.session.get(List, list_id)
|
if request.json.get("closed") is not None:
|
||||||
|
lst.closed = request.json.get("closed")
|
||||||
if not lst:
|
lst.pos = body.pos
|
||||||
return jsonify({"error": "List not found"}), 404
|
|
||||||
|
|
||||||
board = db.session.get(Board, lst.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
lst.name = data.get("name", lst.name)
|
|
||||||
lst.closed = data.get("closed", lst.closed)
|
|
||||||
lst.pos = data.get("pos", lst.pos)
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(lst.to_dict()), 200
|
return lst.to_dict(), 200
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/lists/<int:list_id>", methods=["DELETE"])
|
@kanban_bp.route("/lists/<int:list_id>", methods=["DELETE"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def delete_list(list_id):
|
@load_list_owned
|
||||||
|
def delete_list(list_id, lst):
|
||||||
"""Delete a list"""
|
"""Delete a list"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
lst = db.session.get(List, list_id)
|
|
||||||
|
|
||||||
if not lst:
|
|
||||||
return jsonify({"error": "List not found"}), 404
|
|
||||||
|
|
||||||
board = db.session.get(Board, lst.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
db.session.delete(lst)
|
db.session.delete(lst)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({"message": "List deleted"}), 200
|
return {"message": "List deleted"}, 200
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -223,59 +186,34 @@ def delete_list(list_id):
|
||||||
|
|
||||||
@kanban_bp.route("/lists/<int:list_id>/cards", methods=["POST"])
|
@kanban_bp.route("/lists/<int:list_id>/cards", methods=["POST"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def create_card(list_id):
|
@load_list_owned
|
||||||
|
@validate(body=CardCreateRequest)
|
||||||
|
def create_card(list_id, lst, body: CardCreateRequest):
|
||||||
"""Create a new card in a list"""
|
"""Create a new card in a list"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
lst = db.session.get(List, list_id)
|
|
||||||
|
|
||||||
if not lst:
|
|
||||||
return jsonify({"error": "List not found"}), 404
|
|
||||||
|
|
||||||
board = db.session.get(Board, lst.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
if not data or not data.get("name"):
|
|
||||||
return jsonify({"error": "Card name is required"}), 400
|
|
||||||
|
|
||||||
card = Card(
|
card = Card(
|
||||||
name=data["name"],
|
name=body.name,
|
||||||
description=data.get("description"),
|
description=body.description,
|
||||||
board_id=lst.board_id,
|
board_id=lst.board_id,
|
||||||
list_id=list_id,
|
list_id=list_id,
|
||||||
pos=data.get("pos", 65535.0),
|
pos=body.pos,
|
||||||
due=data.get("due"),
|
due=body.due,
|
||||||
due_complete=data.get("due_complete", False),
|
due_complete=body.due_complete,
|
||||||
badges=data.get("badges"),
|
badges=body.badges,
|
||||||
cover=data.get("cover"),
|
cover=body.cover,
|
||||||
desc_data=data.get("desc_data"),
|
desc_data=body.desc_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update board activity
|
|
||||||
board.date_last_activity = datetime.now(UTC)
|
|
||||||
|
|
||||||
db.session.add(card)
|
db.session.add(card)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(card.to_dict()), 201
|
return card.to_dict(), 201
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/cards/<int:card_id>", methods=["GET"])
|
@kanban_bp.route("/cards/<int:card_id>", methods=["GET"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def get_card(card_id):
|
@load_card_owned
|
||||||
|
def get_card(card_id, card):
|
||||||
"""Get a single card with full details"""
|
"""Get a single card with full details"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
card = db.session.get(Card, card_id)
|
|
||||||
|
|
||||||
if not card:
|
|
||||||
return jsonify({"error": "Card not found"}), 404
|
|
||||||
|
|
||||||
board = db.session.get(Board, card.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
card_dict = card.to_dict()
|
card_dict = card.to_dict()
|
||||||
|
|
||||||
# Add labels
|
# Add labels
|
||||||
|
|
@ -311,63 +249,50 @@ def get_card(card_id):
|
||||||
|
|
||||||
@kanban_bp.route("/cards/<int:card_id>", methods=["PUT"])
|
@kanban_bp.route("/cards/<int:card_id>", methods=["PUT"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def update_card(card_id):
|
@load_card_owned
|
||||||
|
@validate(body=CardCreateRequest)
|
||||||
|
def update_card(card_id, card, body: CardCreateRequest):
|
||||||
"""Update a card"""
|
"""Update a card"""
|
||||||
user_id = int(get_jwt_identity())
|
card.name = body.name
|
||||||
card = db.session.get(Card, card_id)
|
if body.description is not None:
|
||||||
|
card.description = body.description
|
||||||
if not card:
|
if request.json.get("closed") is not None:
|
||||||
return jsonify({"error": "Card not found"}), 404
|
card.closed = request.json.get("closed")
|
||||||
|
card.pos = body.pos
|
||||||
board = db.session.get(Board, card.board_id)
|
card.due = body.due
|
||||||
if board.user_id != user_id:
|
card.due_complete = body.due_complete
|
||||||
return jsonify({"error": "Access denied"}), 403
|
if body.badges is not None:
|
||||||
|
card.badges = body.badges
|
||||||
data = request.get_json()
|
if body.cover is not None:
|
||||||
|
card.cover = body.cover
|
||||||
card.name = data.get("name", card.name)
|
if body.desc_data is not None:
|
||||||
card.description = data.get("description", card.description)
|
card.desc_data = body.desc_data
|
||||||
card.closed = data.get("closed", card.closed)
|
|
||||||
card.pos = data.get("pos", card.pos)
|
|
||||||
card.due = data.get("due")
|
|
||||||
card.due_complete = data.get("due_complete", card.due_complete)
|
|
||||||
card.badges = data.get("badges", card.badges)
|
|
||||||
card.cover = data.get("cover", card.cover)
|
|
||||||
card.desc_data = data.get("desc_data", card.desc_data)
|
|
||||||
|
|
||||||
# Handle moving card to different list
|
# Handle moving card to different list
|
||||||
if "list_id" in data:
|
if "list_id" in request.json:
|
||||||
new_list_id = data["list_id"]
|
new_list_id = request.json["list_id"]
|
||||||
new_list = db.session.get(List, new_list_id)
|
new_list = db.session.get(List, new_list_id)
|
||||||
if new_list and new_list.board_id == card.board_id:
|
if new_list and new_list.board_id == card.board_id:
|
||||||
card.list_id = new_list_id
|
card.list_id = new_list_id
|
||||||
|
|
||||||
card.date_last_activity = datetime.now(UTC)
|
card.date_last_activity = datetime.now(UTC)
|
||||||
|
board = db.session.get(Board, card.board_id)
|
||||||
board.date_last_activity = datetime.now(UTC)
|
board.date_last_activity = datetime.now(UTC)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(card.to_dict()), 200
|
return card.to_dict(), 200
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/cards/<int:card_id>", methods=["DELETE"])
|
@kanban_bp.route("/cards/<int:card_id>", methods=["DELETE"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def delete_card(card_id):
|
@load_card_owned
|
||||||
|
def delete_card(card_id, card):
|
||||||
"""Delete a card"""
|
"""Delete a card"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
card = db.session.get(Card, card_id)
|
|
||||||
|
|
||||||
if not card:
|
|
||||||
return jsonify({"error": "Card not found"}), 404
|
|
||||||
|
|
||||||
board = db.session.get(Board, card.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
db.session.delete(card)
|
db.session.delete(card)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({"message": "Card deleted"}), 200
|
return {"message": "Card deleted"}, 200
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -377,112 +302,71 @@ def delete_card(card_id):
|
||||||
|
|
||||||
@kanban_bp.route("/boards/<int:board_id>/labels", methods=["GET"])
|
@kanban_bp.route("/boards/<int:board_id>/labels", methods=["GET"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def get_labels(board_id):
|
@load_board_owned
|
||||||
|
def get_labels(board_id, board):
|
||||||
"""Get all labels for a board"""
|
"""Get all labels for a board"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
board = db.session.get(Board, board_id)
|
|
||||||
|
|
||||||
if not board:
|
|
||||||
return jsonify({"error": "Board not found"}), 404
|
|
||||||
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
labels = board.labels.all()
|
labels = board.labels.all()
|
||||||
return jsonify([label.to_dict() for label in labels]), 200
|
return [label.to_dict() for label in labels], 200
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/boards/<int:board_id>/labels", methods=["POST"])
|
@kanban_bp.route("/boards/<int:board_id>/labels", methods=["POST"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def create_label(board_id):
|
@load_board_owned
|
||||||
|
@validate(body=LabelCreateRequest)
|
||||||
|
def create_label(board_id, board, body: LabelCreateRequest):
|
||||||
"""Create a new label in a board"""
|
"""Create a new label in a board"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
board = db.session.get(Board, board_id)
|
|
||||||
|
|
||||||
if not board:
|
|
||||||
return jsonify({"error": "Board not found"}), 404
|
|
||||||
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
if not data or not data.get("name"):
|
|
||||||
return jsonify({"error": "Label name is required"}), 400
|
|
||||||
|
|
||||||
label = Label(
|
label = Label(
|
||||||
name=data["name"],
|
name=body.name,
|
||||||
color=data.get("color"),
|
color=body.color,
|
||||||
board_id=board_id,
|
board_id=board_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(label)
|
db.session.add(label)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(label.to_dict()), 201
|
return label.to_dict(), 201
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/cards/<int:card_id>/labels", methods=["POST"])
|
@kanban_bp.route("/cards/<int:card_id>/labels", methods=["POST"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def add_label_to_card(card_id):
|
@load_card_owned
|
||||||
|
def add_label_to_card(card_id, card):
|
||||||
"""Add a label to a card"""
|
"""Add a label to a card"""
|
||||||
user_id = int(get_jwt_identity())
|
data = request.json
|
||||||
card = db.session.get(Card, card_id)
|
|
||||||
|
|
||||||
if not card:
|
|
||||||
return jsonify({"error": "Card not found"}), 404
|
|
||||||
|
|
||||||
board = db.session.get(Board, card.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
label_id = data.get("label_id")
|
label_id = data.get("label_id")
|
||||||
|
|
||||||
if not label_id:
|
if not label_id:
|
||||||
return jsonify({"error": "Label ID is required"}), 400
|
return {"error": "Label ID is required"}, 400
|
||||||
|
|
||||||
label = db.session.get(Label, label_id)
|
label = db.session.get(Label, label_id)
|
||||||
if not label or label.board_id != card.board_id:
|
if not label or label.board_id != card.board_id:
|
||||||
return (
|
return {"error": "Label not found or doesn't belong to this board"}, 404
|
||||||
jsonify({"error": "Label not found or doesn't belong to this board"}),
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if already labeled
|
# Check if already labeled
|
||||||
existing = CardLabel.query.filter_by(card_id=card_id, label_id=label_id).first()
|
existing = CardLabel.query.filter_by(card_id=card_id, label_id=label_id).first()
|
||||||
if existing:
|
if existing:
|
||||||
return jsonify({"error": "Label already added to card"}), 400
|
return {"error": "Label already added to card"}, 400
|
||||||
|
|
||||||
card_label = CardLabel(card_id=card_id, label_id=label_id)
|
card_label = CardLabel(card_id=card_id, label_id=label_id)
|
||||||
db.session.add(card_label)
|
db.session.add(card_label)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(card_label.to_dict()), 201
|
return card_label.to_dict(), 201
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/cards/<int:card_id>/labels/<int:label_id>", methods=["DELETE"])
|
@kanban_bp.route("/cards/<int:card_id>/labels/<int:label_id>", methods=["DELETE"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def remove_label_from_card(card_id, label_id):
|
@load_card_owned
|
||||||
|
def remove_label_from_card(card_id, card, label_id):
|
||||||
"""Remove a label from a card"""
|
"""Remove a label from a card"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
card = db.session.get(Card, card_id)
|
|
||||||
|
|
||||||
if not card:
|
|
||||||
return jsonify({"error": "Card not found"}), 404
|
|
||||||
|
|
||||||
board = db.session.get(Board, card.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
card_label = CardLabel.query.filter_by(card_id=card_id, label_id=label_id).first()
|
card_label = CardLabel.query.filter_by(card_id=card_id, label_id=label_id).first()
|
||||||
if not card_label:
|
if not card_label:
|
||||||
return jsonify({"error": "Label not found on card"}), 404
|
return {"error": "Label not found on card"}, 404
|
||||||
|
|
||||||
db.session.delete(card_label)
|
db.session.delete(card_label)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({"message": "Label removed from card"}), 200
|
return {"message": "Label removed from card"}, 200
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -492,139 +376,79 @@ def remove_label_from_card(card_id, label_id):
|
||||||
|
|
||||||
@kanban_bp.route("/cards/<int:card_id>/checklists", methods=["POST"])
|
@kanban_bp.route("/cards/<int:card_id>/checklists", methods=["POST"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def create_checklist(card_id):
|
@load_card_owned
|
||||||
|
@validate(body=ChecklistCreateRequest)
|
||||||
|
def create_checklist(card_id, card, body: ChecklistCreateRequest):
|
||||||
"""Create a new checklist in a card"""
|
"""Create a new checklist in a card"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
card = db.session.get(Card, card_id)
|
|
||||||
|
|
||||||
if not card:
|
|
||||||
return jsonify({"error": "Card not found"}), 404
|
|
||||||
|
|
||||||
board = db.session.get(Board, card.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
if not data or not data.get("name"):
|
|
||||||
return jsonify({"error": "Checklist name is required"}), 400
|
|
||||||
|
|
||||||
checklist = Checklist(
|
checklist = Checklist(
|
||||||
name=data["name"],
|
name=body.name,
|
||||||
card_id=card_id,
|
card_id=card_id,
|
||||||
board_id=card.board_id,
|
board_id=card.board_id,
|
||||||
pos=data.get("pos", 0),
|
pos=body.pos,
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(checklist)
|
db.session.add(checklist)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(checklist.to_dict()), 201
|
return checklist.to_dict(), 201
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/checklists/<int:checklist_id>/items", methods=["POST"])
|
@kanban_bp.route("/checklists/<int:checklist_id>/items", methods=["POST"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def create_check_item(checklist_id):
|
@load_checklist_owned
|
||||||
|
@validate(body=CheckItemCreateRequest)
|
||||||
|
def create_check_item(checklist_id, checklist, body: CheckItemCreateRequest):
|
||||||
"""Create a new check item in a checklist"""
|
"""Create a new check item in a checklist"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
checklist = db.session.get(Checklist, checklist_id)
|
|
||||||
|
|
||||||
if not checklist:
|
|
||||||
return jsonify({"error": "Checklist not found"}), 404
|
|
||||||
|
|
||||||
card = db.session.get(Card, checklist.card_id)
|
|
||||||
board = db.session.get(Board, card.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
if not data or not data.get("name"):
|
|
||||||
return jsonify({"error": "Check item name is required"}), 400
|
|
||||||
|
|
||||||
check_item = CheckItem(
|
check_item = CheckItem(
|
||||||
name=data["name"],
|
name=body.name,
|
||||||
checklist_id=checklist_id,
|
checklist_id=checklist_id,
|
||||||
pos=data.get("pos", 0),
|
pos=body.pos,
|
||||||
state=data.get("state", "incomplete"),
|
state=body.state,
|
||||||
due=data.get("due"),
|
due=body.due,
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(check_item)
|
db.session.add(check_item)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(check_item.to_dict()), 201
|
return check_item.to_dict(), 201
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/check-items/<int:item_id>", methods=["PUT"])
|
@kanban_bp.route("/check-items/<int:item_id>", methods=["PUT"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def update_check_item(item_id):
|
@load_check_item_owned
|
||||||
|
@validate(body=CheckItemCreateRequest)
|
||||||
|
def update_check_item(item_id, check_item, body: CheckItemCreateRequest):
|
||||||
"""Update a check item"""
|
"""Update a check item"""
|
||||||
user_id = int(get_jwt_identity())
|
check_item.name = body.name
|
||||||
check_item = db.session.get(CheckItem, item_id)
|
check_item.state = body.state
|
||||||
|
check_item.pos = body.pos
|
||||||
if not check_item:
|
check_item.due = body.due
|
||||||
return jsonify({"error": "Check item not found"}), 404
|
|
||||||
|
|
||||||
checklist = db.session.get(Checklist, check_item.checklist_id)
|
|
||||||
card = db.session.get(Card, checklist.card_id)
|
|
||||||
board = db.session.get(Board, card.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
check_item.name = data.get("name", check_item.name)
|
|
||||||
check_item.state = data.get("state", check_item.state)
|
|
||||||
check_item.pos = data.get("pos", check_item.pos)
|
|
||||||
check_item.due = data.get("due")
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(check_item.to_dict()), 200
|
return check_item.to_dict(), 200
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/checklists/<int:checklist_id>", methods=["DELETE"])
|
@kanban_bp.route("/checklists/<int:checklist_id>", methods=["DELETE"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def delete_checklist(checklist_id):
|
@load_checklist_owned
|
||||||
|
def delete_checklist(checklist_id, checklist):
|
||||||
"""Delete a checklist"""
|
"""Delete a checklist"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
checklist = db.session.get(Checklist, checklist_id)
|
|
||||||
|
|
||||||
if not checklist:
|
|
||||||
return jsonify({"error": "Checklist not found"}), 404
|
|
||||||
|
|
||||||
card = db.session.get(Card, checklist.card_id)
|
|
||||||
board = db.session.get(Board, card.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
db.session.delete(checklist)
|
db.session.delete(checklist)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({"message": "Checklist deleted"}), 200
|
return {"message": "Checklist deleted"}, 200
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/check-items/<int:item_id>", methods=["DELETE"])
|
@kanban_bp.route("/check-items/<int:item_id>", methods=["DELETE"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def delete_check_item(item_id):
|
@load_check_item_owned
|
||||||
|
def delete_check_item(item_id, check_item):
|
||||||
"""Delete a check item"""
|
"""Delete a check item"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
check_item = db.session.get(CheckItem, item_id)
|
|
||||||
|
|
||||||
if not check_item:
|
|
||||||
return jsonify({"error": "Check item not found"}), 404
|
|
||||||
|
|
||||||
checklist = db.session.get(Checklist, check_item.checklist_id)
|
|
||||||
card = db.session.get(Card, checklist.card_id)
|
|
||||||
board = db.session.get(Board, card.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
db.session.delete(check_item)
|
db.session.delete(check_item)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({"message": "Check item deleted"}), 200
|
return {"message": "Check item deleted"}, 200
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -634,18 +458,9 @@ def delete_check_item(item_id):
|
||||||
|
|
||||||
@kanban_bp.route("/cards/<int:card_id>/comments", methods=["GET"])
|
@kanban_bp.route("/cards/<int:card_id>/comments", methods=["GET"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def get_comments(card_id):
|
@load_card_owned
|
||||||
|
def get_comments(card_id, card):
|
||||||
"""Get all comments for a card"""
|
"""Get all comments for a card"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
card = db.session.get(Card, card_id)
|
|
||||||
|
|
||||||
if not card:
|
|
||||||
return jsonify({"error": "Card not found"}), 404
|
|
||||||
|
|
||||||
board = db.session.get(Board, card.board_id)
|
|
||||||
if board.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
comments = []
|
comments = []
|
||||||
for comment in card.comments.order_by(Comment.created_at.desc()).all():
|
for comment in card.comments.order_by(Comment.created_at.desc()).all():
|
||||||
comment_dict = comment.to_dict()
|
comment_dict = comment.to_dict()
|
||||||
|
|
@ -653,72 +468,48 @@ def get_comments(card_id):
|
||||||
comment_dict["user"] = user.to_dict() if user else None
|
comment_dict["user"] = user.to_dict() if user else None
|
||||||
comments.append(comment_dict)
|
comments.append(comment_dict)
|
||||||
|
|
||||||
return jsonify(comments), 200
|
return comments, 200
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/cards/<int:card_id>/comments", methods=["POST"])
|
@kanban_bp.route("/cards/<int:card_id>/comments", methods=["POST"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def create_comment(card_id):
|
@load_card_owned
|
||||||
|
@validate(body=CommentCreateRequest)
|
||||||
|
def create_comment(card_id, card, body: CommentCreateRequest):
|
||||||
"""Create a new comment on a card"""
|
"""Create a new comment on a card"""
|
||||||
user_id = int(get_jwt_identity())
|
user_id = get_current_user_id()
|
||||||
card = db.session.get(Card, card_id)
|
|
||||||
|
|
||||||
if not card:
|
comment = Comment(
|
||||||
return jsonify({"error": "Card not found"}), 404
|
text=body.text,
|
||||||
|
card_id=card_id,
|
||||||
board = db.session.get(Board, card.board_id)
|
user_id=user_id,
|
||||||
if board.user_id != user_id:
|
)
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
if not data or not data.get("text"):
|
|
||||||
return jsonify({"error": "Comment text is required"}), 400
|
|
||||||
|
|
||||||
comment = Comment(text=data["text"], card_id=card_id, user_id=user_id)
|
|
||||||
|
|
||||||
db.session.add(comment)
|
db.session.add(comment)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(comment.to_dict()), 201
|
return comment.to_dict(), 201
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/comments/<int:comment_id>", methods=["PUT"])
|
@kanban_bp.route("/comments/<int:comment_id>", methods=["PUT"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def update_comment(comment_id):
|
@load_comment_owned
|
||||||
|
@validate(body=CommentCreateRequest)
|
||||||
|
def update_comment(comment_id, comment, body: CommentCreateRequest):
|
||||||
"""Update a comment"""
|
"""Update a comment"""
|
||||||
user_id = int(get_jwt_identity())
|
comment.text = body.text
|
||||||
comment = db.session.get(Comment, comment_id)
|
|
||||||
|
|
||||||
if not comment:
|
|
||||||
return jsonify({"error": "Comment not found"}), 404
|
|
||||||
|
|
||||||
if comment.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
comment.text = data.get("text", comment.text)
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(comment.to_dict()), 200
|
return comment.to_dict(), 200
|
||||||
|
|
||||||
|
|
||||||
@kanban_bp.route("/comments/<int:comment_id>", methods=["DELETE"])
|
@kanban_bp.route("/comments/<int:comment_id>", methods=["DELETE"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def delete_comment(comment_id):
|
@load_comment_owned
|
||||||
|
def delete_comment(comment_id, comment):
|
||||||
"""Delete a comment"""
|
"""Delete a comment"""
|
||||||
user_id = int(get_jwt_identity())
|
|
||||||
comment = db.session.get(Comment, comment_id)
|
|
||||||
|
|
||||||
if not comment:
|
|
||||||
return jsonify({"error": "Comment not found"}), 404
|
|
||||||
|
|
||||||
if comment.user_id != user_id:
|
|
||||||
return jsonify({"error": "Access denied"}), 403
|
|
||||||
|
|
||||||
db.session.delete(comment)
|
db.session.delete(comment)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({"message": "Comment deleted"}), 200
|
return {"message": "Comment deleted"}, 200
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,36 @@
|
||||||
"""Pydantic schemas for request/response validation"""
|
"""Pydantic schemas for request/response validation"""
|
||||||
|
from app.schemas.board import BoardCreateRequest, BoardResponse
|
||||||
|
from app.schemas.card import CardCreateRequest, CardResponse
|
||||||
|
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.label import LabelCreateRequest, LabelResponse
|
||||||
|
from app.schemas.list_model import ListCreateRequest, ListResponse
|
||||||
from app.schemas.product import ProductCreateRequest, ProductResponse
|
from app.schemas.product import ProductCreateRequest, ProductResponse
|
||||||
|
|
||||||
__all__ = ["ProductCreateRequest", "ProductResponse"]
|
__all__ = [
|
||||||
|
# Product schemas
|
||||||
|
"ProductCreateRequest",
|
||||||
|
"ProductResponse",
|
||||||
|
# Board schemas
|
||||||
|
"BoardCreateRequest",
|
||||||
|
"BoardResponse",
|
||||||
|
# List schemas
|
||||||
|
"ListCreateRequest",
|
||||||
|
"ListResponse",
|
||||||
|
# Card schemas
|
||||||
|
"CardCreateRequest",
|
||||||
|
"CardResponse",
|
||||||
|
# Label schemas
|
||||||
|
"LabelCreateRequest",
|
||||||
|
"LabelResponse",
|
||||||
|
# Checklist schemas
|
||||||
|
"ChecklistCreateRequest",
|
||||||
|
"ChecklistResponse",
|
||||||
|
# CheckItem schemas
|
||||||
|
"CheckItemCreateRequest",
|
||||||
|
"CheckItemResponse",
|
||||||
|
# Comment schemas
|
||||||
|
"CommentCreateRequest",
|
||||||
|
"CommentResponse",
|
||||||
|
]
|
||||||
|
|
|
||||||
68
backend/app/schemas/board.py
Normal file
68
backend/app/schemas/board.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
"""Pydantic schemas for Board model"""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class BoardCreateRequest(BaseModel):
|
||||||
|
"""Schema for creating a new board"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"name": "Project Alpha",
|
||||||
|
"description": "Main project board",
|
||||||
|
"url": "https://trello.com/b/example",
|
||||||
|
"short_link": "abc123",
|
||||||
|
"short_url": "https://trello.com/c/abc123",
|
||||||
|
"prefs": {"permissionLevel": "private"},
|
||||||
|
"label_names": {"green": "Done", "yellow": "In Progress"},
|
||||||
|
"limits": {"lists": {"open": 10}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
name: str = Field(..., min_length=1, max_length=200, description="Board name")
|
||||||
|
description: Optional[str] = Field(None, description="Board description")
|
||||||
|
url: Optional[str] = Field(None, max_length=500, description="Board URL")
|
||||||
|
short_link: Optional[str] = Field(None, max_length=100, description="Short link")
|
||||||
|
short_url: Optional[str] = Field(None, max_length=500, description="Short URL")
|
||||||
|
prefs: Optional[Dict[str, Any]] = Field(None, description="Board preferences")
|
||||||
|
label_names: Optional[Dict[str, str]] = Field(None, description="Label names")
|
||||||
|
limits: Optional[Dict[str, Any]] = Field(None, description="Board limits")
|
||||||
|
|
||||||
|
|
||||||
|
class BoardResponse(BaseModel):
|
||||||
|
"""Schema for board response"""
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
72
backend/app/schemas/card.py
Normal file
72
backend/app/schemas/card.py
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
"""Pydantic schemas for Card model"""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class CardCreateRequest(BaseModel):
|
||||||
|
"""Schema for creating a new card"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"name": "Task 1",
|
||||||
|
"description": "Complete the task",
|
||||||
|
"pos": 65535.0,
|
||||||
|
"due": "2024-12-31T23:59:59",
|
||||||
|
"due_complete": False,
|
||||||
|
"badges": {"votes": 0},
|
||||||
|
"cover": "https://example.com/cover.jpg",
|
||||||
|
"desc_data": "<p>Task description</p>",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
name: str = Field(..., min_length=1, max_length=200, description="Card name")
|
||||||
|
description: Optional[str] = Field(None, description="Card description")
|
||||||
|
pos: float = Field(default=65535.0, description="Card position")
|
||||||
|
due: Optional[datetime] = Field(None, description="Card due date")
|
||||||
|
due_complete: bool = Field(default=False, description="Due complete status")
|
||||||
|
badges: Optional[Dict[str, Any]] = Field(None, description="Card badges")
|
||||||
|
cover: Optional[str] = Field(None, description="Cover image URL")
|
||||||
|
desc_data: Optional[str] = Field(None, description="Description data (HTML)")
|
||||||
|
|
||||||
|
|
||||||
|
class CardResponse(BaseModel):
|
||||||
|
"""Schema for card response"""
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
52
backend/app/schemas/check_item.py
Normal file
52
backend/app/schemas/check_item.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""Pydantic schemas for CheckItem model"""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class CheckItemCreateRequest(BaseModel):
|
||||||
|
"""Schema for creating a new check item"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"name": "First step",
|
||||||
|
"pos": 0,
|
||||||
|
"state": "incomplete",
|
||||||
|
"due": "2024-12-31T23:59:59",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
name: str = Field(..., min_length=1, max_length=200, description="Check item name")
|
||||||
|
pos: int = Field(default=0, description="Check item position")
|
||||||
|
state: str = Field(
|
||||||
|
default="incomplete", description="State: complete or incomplete"
|
||||||
|
)
|
||||||
|
due: Optional[datetime] = Field(None, description="Due date")
|
||||||
|
|
||||||
|
|
||||||
|
class CheckItemResponse(BaseModel):
|
||||||
|
"""Schema for check item response"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
from_attributes=True,
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "First step",
|
||||||
|
"pos": 0,
|
||||||
|
"state": "incomplete",
|
||||||
|
"due": "2024-12-31T23:59:59",
|
||||||
|
"checklist_id": 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
pos: int
|
||||||
|
state: str
|
||||||
|
due: Optional[datetime] = None
|
||||||
|
checklist_id: int
|
||||||
41
backend/app/schemas/checklist.py
Normal file
41
backend/app/schemas/checklist.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
"""Pydantic schemas for Checklist model"""
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class ChecklistCreateRequest(BaseModel):
|
||||||
|
"""Schema for creating a new checklist"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"name": "Steps to complete",
|
||||||
|
"pos": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
name: str = Field(..., min_length=1, max_length=200, description="Checklist name")
|
||||||
|
pos: int = Field(default=0, description="Checklist position")
|
||||||
|
|
||||||
|
|
||||||
|
class ChecklistResponse(BaseModel):
|
||||||
|
"""Schema for checklist response"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
from_attributes=True,
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "Steps to complete",
|
||||||
|
"pos": 0,
|
||||||
|
"card_id": 1,
|
||||||
|
"board_id": 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
pos: int
|
||||||
|
card_id: int
|
||||||
|
board_id: int
|
||||||
44
backend/app/schemas/comment.py
Normal file
44
backend/app/schemas/comment.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
"""Pydantic schemas for Comment model"""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class CommentCreateRequest(BaseModel):
|
||||||
|
"""Schema for creating a new comment"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"text": "This is a comment",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
text: str = Field(..., min_length=1, description="Comment text")
|
||||||
|
|
||||||
|
|
||||||
|
class CommentResponse(BaseModel):
|
||||||
|
"""Schema for comment response"""
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
text: str
|
||||||
|
card_id: int
|
||||||
|
user_id: int
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
41
backend/app/schemas/label.py
Normal file
41
backend/app/schemas/label.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
"""Pydantic schemas for Label model"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class LabelCreateRequest(BaseModel):
|
||||||
|
"""Schema for creating a new label"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"name": "High Priority",
|
||||||
|
"color": "#ff0000",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
name: str = Field(..., min_length=1, max_length=100, description="Label name")
|
||||||
|
color: Optional[str] = Field(None, max_length=7, description="Label color (hex)")
|
||||||
|
|
||||||
|
|
||||||
|
class LabelResponse(BaseModel):
|
||||||
|
"""Schema for label response"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
from_attributes=True,
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "High Priority",
|
||||||
|
"color": "#ff0000",
|
||||||
|
"board_id": 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
color: Optional[str] = None
|
||||||
|
board_id: int
|
||||||
41
backend/app/schemas/list_model.py
Normal file
41
backend/app/schemas/list_model.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
"""Pydantic schemas for List model"""
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class ListCreateRequest(BaseModel):
|
||||||
|
"""Schema for creating a new list"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"name": "To Do",
|
||||||
|
"pos": 65535.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
name: str = Field(..., min_length=1, max_length=200, description="List name")
|
||||||
|
pos: float = Field(default=65535.0, description="List position")
|
||||||
|
|
||||||
|
|
||||||
|
class ListResponse(BaseModel):
|
||||||
|
"""Schema for list response"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
from_attributes=True,
|
||||||
|
json_schema_extra={
|
||||||
|
"example": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "To Do",
|
||||||
|
"closed": False,
|
||||||
|
"pos": 65535.0,
|
||||||
|
"board_id": 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
closed: bool
|
||||||
|
pos: float
|
||||||
|
board_id: int
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
Flask==3.0.0
|
Flask==3.0.0
|
||||||
Flask-CORS==4.0.0
|
Flask-CORS
|
||||||
Flask-SQLAlchemy==3.1.1
|
Flask-SQLAlchemy
|
||||||
Flask-Migrate==4.0.5
|
Flask-Migrate==4.0.5
|
||||||
Flask-JWT-Extended==4.5.3
|
Flask-JWT-Extended==4.5.3
|
||||||
|
flask-pydantic==0.14.0
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
Werkzeug==3.0.1
|
Werkzeug==3.0.1
|
||||||
|
|
@ -14,4 +15,4 @@ pytest-flask==1.3.0
|
||||||
pytest-cov==4.1.0
|
pytest-cov==4.1.0
|
||||||
pytest-mock==3.12.0
|
pytest-mock==3.12.0
|
||||||
factory-boy==3.3.0
|
factory-boy==3.3.0
|
||||||
faker==20.1.0
|
faker==20.1.0
|
||||||
|
|
@ -98,7 +98,7 @@ class TestBoardRoutes:
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
data = response.get_json()
|
data = response.get_json()
|
||||||
assert "error" in data
|
assert "validation_error" in data
|
||||||
|
|
||||||
def test_create_board_unauthorized(self, client, db_session):
|
def test_create_board_unauthorized(self, client, db_session):
|
||||||
"""Test creating a board without authentication"""
|
"""Test creating a board without authentication"""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue