278 lines
7.9 KiB
Python
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,
|
|
)
|