Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions alembic/versions/dde1a0ded9f7_add_trello_tokens_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""add_trello_tokens_table

Revision ID: dde1a0ded9f7
Revises: b0c1d2e3f4a5
Create Date: 2026-04-10 12:25:24.527658

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "dde1a0ded9f7"
down_revision: str | Sequence[str] | None = "b0c1d2e3f4a5"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Create trello_tokens table; remove legacy trello columns from user_tokens."""
op.create_table(
"trello_tokens",
sa.Column("id", sa.Integer(), primary_key=True, index=True),
sa.Column(
"user_id",
sa.Integer(),
sa.ForeignKey("users.id"),
unique=True,
index=True,
nullable=False,
),
sa.Column("access_token", sa.Text(), nullable=False),
sa.Column("token_secret", sa.Text(), nullable=False),
sa.Column("trello_user_id", sa.String(), nullable=False),
sa.Column("username", sa.String(), nullable=True),
sa.Column("selected_board_id", sa.String(), nullable=True),
)
op.drop_column("user_tokens", "trello_workspace_id")
op.drop_column("user_tokens", "trello_board_id")


def downgrade() -> None:
"""Drop trello_tokens table; restore legacy trello columns on user_tokens."""
op.drop_table("trello_tokens")
op.add_column("user_tokens", sa.Column("trello_workspace_id", sa.String(), nullable=True))
op.add_column("user_tokens", sa.Column("trello_board_id", sa.String(), nullable=True))
51 changes: 31 additions & 20 deletions apps/integrations/jira/routes/api_routes/trello.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import os
from datetime import UTC, datetime, timedelta

from fastapi import APIRouter, Depends, Header, HTTPException
Expand Down Expand Up @@ -91,25 +92,20 @@ async def _get_trello_client_core(authorization: str, db: AsyncSession) -> Trell
raise HTTPException(status_code=401, detail="Invalid token")

result = await db.execute(
select(User).where(User.email == email).options(selectinload(User.jira_token))
select(User).where(User.email == email).options(selectinload(User.trello_token))
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=401, detail="User not found")

if not user.jira_token:
raise HTTPException(status_code=404, detail="Jira not connected")
if not user.trello_token:
raise HTTPException(status_code=404, detail="Trello not connected")

if not user.jira_token.trello_workspace_id:
raise HTTPException(
status_code=404,
detail="Trello not connected. Reconnect Jira to grant Trello access.",
)
api_key = os.getenv("TRELLO_API_KEY")
if not api_key:
raise HTTPException(status_code=500, detail="Trello API key not configured")

return TrelloClient(
access_token=user.jira_token.access_token,
workspace_id=user.jira_token.trello_workspace_id,
)
return TrelloClient(api_key=api_key, user_token=user.trello_token.access_token)
except HTTPException:
raise
except jwt.JWTError as e:
Expand All @@ -130,7 +126,7 @@ async def _get_current_user(authorization: str, db: AsyncSession) -> User:
payload = jwt.decode(token, secret_key, algorithms=[ALGORITHM])
email = payload.get("sub")
result = await db.execute(
select(User).where(User.email == email).options(selectinload(User.jira_token))
select(User).where(User.email == email).options(selectinload(User.trello_token))
)
return result.scalar_one_or_none()

Expand Down Expand Up @@ -161,11 +157,22 @@ class MoveCardRequest(BaseModel):


@router.get("/trello/boards")
async def list_trello_boards(client: TrelloClient = Depends(get_trello_client)):
"""Return all open boards for the authenticated Trello user."""
async def list_trello_boards(
authorization: str = Header(None),
db: AsyncSession = Depends(get_async_db),
client: TrelloClient = Depends(get_trello_client),
):
"""Return all open boards + the user's persisted board selection."""
try:
boards = await client.get_boards()
return {"boards": [{"id": b["id"], "name": b["name"]} for b in boards]}
user = await _get_current_user(authorization, db)
selected_board_id = (
user.trello_token.selected_board_id if user and user.trello_token else None
)
return {
"boards": [{"id": b["id"], "name": b["name"]} for b in boards],
"selected_board_id": selected_board_id,
}
except TrelloAuthError as e:
raise HTTPException(status_code=401, detail=str(e)) from e
except Exception as e:
Expand All @@ -181,9 +188,9 @@ async def select_trello_board(
):
"""Persist the user's selected Trello board."""
user = await _get_current_user(authorization, db)
if not user or not user.jira_token:
if not user or not user.trello_token:
raise HTTPException(status_code=404, detail="Trello not connected")
user.jira_token.trello_board_id = board_id
user.trello_token.selected_board_id = board_id
await db.commit()
return {"status": "ok", "board_id": board_id}

Expand All @@ -199,7 +206,7 @@ async def get_trello_stats(
try:
user = await _get_current_user(authorization, db)
resolved_board_id = board_id or (
user.jira_token.trello_board_id if user and user.jira_token else None
user.trello_token.selected_board_id if user and user.trello_token else None
)
if not resolved_board_id:
raise HTTPException(status_code=400, detail="No board selected")
Expand All @@ -208,12 +215,15 @@ async def get_trello_stats(
lists = board_data.get("lists", [])
cards = board_data.get("cards", [])

# Build list_id → (name, category) mapping
# Build list_id → (name, category) mapping + first list ID per category for moves
total = len(lists)
list_meta: dict[str, dict] = {}
category_list_ids: dict[str, str] = {} # first list ID per category (for card moves)
for i, lst in enumerate(lists):
cat = _classify_list(lst["name"], i, total)
list_meta[lst["id"]] = {"name": lst["name"], "category": cat}
if cat not in category_list_ids:
category_list_ids[cat] = lst["id"]

board: dict = {"todo": [], "inProgress": [], "done": []}
now = datetime.now(UTC)
Expand Down Expand Up @@ -267,6 +277,7 @@ async def get_trello_stats(
"overdue": str(overdue_count),
"highPriority": str(high_priority_count),
"board": board,
"list_ids": category_list_ids, # {todo: "listId", inProgress: "listId", done: "listId"}
"filters": {
"priorities": ["High", "Medium", "Low"],
"types": ["Card"],
Expand Down
2 changes: 2 additions & 0 deletions apps/integrations/jira/routes/auth_routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from apps.integrations.jira.routes.auth_routes.oauth_github import router as github_router
from apps.integrations.jira.routes.auth_routes.oauth_google import router as google_router
from apps.integrations.jira.routes.auth_routes.oauth_jira import router as jira_router
from apps.integrations.jira.routes.auth_routes.oauth_trello import router as trello_router
from apps.integrations.jira.routes.auth_routes.password import router as password_router
from apps.integrations.jira.routes.auth_routes.profile import router as profile_router
from apps.integrations.jira.routes.auth_routes.waitlist import router as waitlist_router
Expand All @@ -14,6 +15,7 @@
router.include_router(github_router)
router.include_router(google_router)
router.include_router(jira_router)
router.include_router(trello_router)
router.include_router(profile_router)
router.include_router(password_router)
router.include_router(waitlist_router)
Loading
Loading