"""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//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//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//links/", 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//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/", 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//checklist-items//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, )