kanban-app/backend/app/routes/kanban/card_links.py
2026-05-01 18:03:00 +03:00

278 lines
7.9 KiB
Python

"""Routes for Card-to-Card linking operations."""
from flask import jsonify, request
from flask_jwt_extended import get_jwt_identity, jwt_required
from app import db
from app.models import Card
from app.models.card_label import CardLabel
from app.models.card_link import CardLink
from . import kanban_bp
@kanban_bp.route("/cards/<int:card_id>/links", methods=["GET"])
@jwt_required()
def get_card_links(card_id):
"""Get all cards linked to a card (both parent and child)."""
card = db.session.get(Card, card_id)
if not card:
return jsonify({"error": "Card not found"}), 404
# Cards where this card is the parent (this card -> child cards)
child_links = card.child_links.all()
# Cards where this card is the child (parent cards -> this card)
parent_links = card.parent_links.all()
return (
jsonify(
{
"child_cards": [
{
"id": link.id,
"card": link.child_card.to_dict(),
"created_at": (
link.created_at.isoformat() if link.created_at else None
),
}
for link in child_links
if link.child_card
],
"parent_cards": [
{
"id": link.id,
"card": link.parent_card.to_dict(),
"created_at": (
link.created_at.isoformat() if link.created_at else None
),
}
for link in parent_links
if link.parent_card
],
}
),
200,
)
@kanban_bp.route("/cards/<int:card_id>/links", methods=["POST"])
@jwt_required()
def link_existing_card(card_id):
"""Link an existing card to this card."""
card = db.session.get(Card, card_id)
if not card:
return jsonify({"error": "Card not found"}), 404
data = request.get_json()
target_card_id = data.get("child_card_id")
if not target_card_id:
return jsonify({"error": "child_card_id is required"}), 400
target_card = db.session.get(Card, target_card_id)
if not target_card:
return jsonify({"error": "Target card not found"}), 404
if card.id == target_card.id:
return jsonify({"error": "Cannot link a card to itself"}), 400
# Check if link already exists (in either direction)
existing = CardLink.query.filter(
db.or_(
db.and_(
CardLink.parent_card_id == card.id,
CardLink.child_card_id == target_card.id,
),
db.and_(
CardLink.parent_card_id == target_card.id,
CardLink.child_card_id == card.id,
),
)
).first()
if existing:
return jsonify({"error": "Cards are already linked"}), 409
user_id = get_jwt_identity()
# Create link: card is parent, target is child
link = CardLink(
parent_card_id=card.id,
child_card_id=target_card.id,
created_by=user_id,
)
db.session.add(link)
db.session.commit()
return jsonify(link.to_dict(include_cards=True)), 201
@kanban_bp.route("/cards/<int:card_id>/links/<int:link_id>", methods=["DELETE"])
@jwt_required()
def unlink_card(card_id, link_id):
"""Remove a link between cards."""
card = db.session.get(Card, card_id)
if not card:
return jsonify({"error": "Card not found"}), 404
link = db.session.get(CardLink, link_id)
if not link:
return jsonify({"error": "Link not found"}), 404
# Verify the link involves this card
if link.parent_card_id != card.id and link.child_card_id != card.id:
return jsonify({"error": "Link does not belong to this card"}), 403
link.soft_delete()
db.session.commit()
return jsonify({"message": "Cards unlinked successfully"}), 200
@kanban_bp.route("/cards/<int:card_id>/linked-cards", methods=["POST"])
@jwt_required()
def create_linked_card(card_id):
"""Create a new card linked to this card. Copies labels and epics."""
card = db.session.get(Card, card_id)
if not card:
return jsonify({"error": "Card not found"}), 404
data = request.get_json()
name = data.get("name")
list_id = data.get("list_id")
description = data.get("description", "")
copy_labels = data.get("copy_labels", True)
copy_epics = data.get("copy_epics", True)
if not name:
return jsonify({"error": "name is required"}), 400
if not list_id:
return jsonify({"error": "list_id is required"}), 400
user_id = get_jwt_identity()
# Get the highest position in the target list
max_pos = (
db.session.query(db.func.max(Card.pos)).filter(Card.list_id == list_id).scalar()
)
pos = (max_pos or 0) + 65536
# Create the new card
new_card = Card(
name=name,
description=description,
board_id=card.board_id,
list_id=list_id,
pos=pos,
epic_id=card.epic_id if copy_epics else None,
)
db.session.add(new_card)
db.session.flush()
# Copy labels if requested
if copy_labels:
parent_labels = CardLabel.query.filter_by(card_id=card.id).all()
for pl in parent_labels:
new_label = CardLabel(card_id=new_card.id, label_id=pl.label_id)
db.session.add(new_label)
# Create link: parent card -> new child card
link = CardLink(
parent_card_id=card.id,
child_card_id=new_card.id,
created_by=user_id,
)
db.session.add(link)
db.session.commit()
return (
jsonify(
{
"card": new_card.to_dict(),
"link": link.to_dict(),
}
),
201,
)
@kanban_bp.route("/card-links/<int:link_id>", methods=["DELETE"])
@jwt_required()
def delete_card_link(link_id):
"""Remove a link by link ID."""
link = db.session.get(CardLink, link_id)
if not link:
return jsonify({"error": "Link not found"}), 404
link.soft_delete()
db.session.commit()
return jsonify({"message": "Cards unlinked successfully"}), 200
@kanban_bp.route(
"/cards/<int:card_id>/checklist-items/<int:check_item_id>/convert-to-card",
methods=["POST"],
)
@jwt_required()
def convert_check_item_to_card(card_id, check_item_id):
"""Convert a checklist item to a new linked card."""
from app.models.check_item import CheckItem
card = db.session.get(Card, card_id)
if not card:
return jsonify({"error": "Card not found"}), 404
check_item = db.session.get(CheckItem, check_item_id)
if not check_item:
return jsonify({"error": "Check item not found"}), 404
data = request.get_json() or {}
list_id = data.get("list_id", card.list_id)
user_id = get_jwt_identity()
# Get the highest position in the target list
max_pos = (
db.session.query(db.func.max(Card.pos)).filter(Card.list_id == list_id).scalar()
)
pos = (max_pos or 0) + 65536
# Create the new card from checklist item name
new_card = Card(
name=check_item.name,
board_id=card.board_id,
list_id=list_id,
pos=pos,
epic_id=card.epic_id,
)
db.session.add(new_card)
db.session.flush()
# Copy labels from parent card
parent_labels = CardLabel.query.filter_by(card_id=card.id).all()
for pl in parent_labels:
new_label = CardLabel(card_id=new_card.id, label_id=pl.label_id)
db.session.add(new_label)
# Create link: parent card -> new child card
link = CardLink(
parent_card_id=card.id,
child_card_id=new_card.id,
created_by=user_id,
)
db.session.add(link)
# Remove the checklist item
check_item.soft_delete()
db.session.commit()
return (
jsonify(
{
"card": new_card.to_dict(),
"link": link.to_dict(),
}
),
201,
)