from datetime import date, timedelta from fastapi import Depends from fastapi import APIRouter from sqlalchemy import desc, or_, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.core.dependencies import get_optional_user from app.database import get_db from app.models import Bill, BillBrief, Follow, TrendScore from app.models.user import User from app.schemas.schemas import BillSchema router = APIRouter() async def _get_trending(db: AsyncSession) -> list[dict]: # Try progressively wider windows so stale scores still surface results for days_back in (1, 3, 7, 30): trending_result = await db.execute( select(Bill) .options(selectinload(Bill.sponsor), selectinload(Bill.briefs), selectinload(Bill.trend_scores)) .join(TrendScore, Bill.bill_id == TrendScore.bill_id) .where(TrendScore.score_date >= date.today() - timedelta(days=days_back)) .order_by(desc(TrendScore.composite_score)) .limit(10) ) trending_bills = trending_result.scalars().unique().all() if trending_bills: return [_serialize_bill(b) for b in trending_bills] return [] def _serialize_bill(bill: Bill) -> dict: b = BillSchema.model_validate(bill) if bill.briefs: b.latest_brief = bill.briefs[0] if bill.trend_scores: b.latest_trend = bill.trend_scores[0] return b.model_dump() @router.get("") async def get_dashboard( db: AsyncSession = Depends(get_db), current_user: User | None = Depends(get_optional_user), ): trending = await _get_trending(db) if current_user is None: return {"feed": [], "trending": trending, "follows": {"bills": 0, "members": 0, "topics": 0}} # Load follows for the current user follows_result = await db.execute( select(Follow).where(Follow.user_id == current_user.id) ) follows = follows_result.scalars().all() followed_bill_ids = [f.follow_value for f in follows if f.follow_type == "bill"] followed_member_ids = [f.follow_value for f in follows if f.follow_type == "member"] followed_topics = [f.follow_value for f in follows if f.follow_type == "topic"] feed_bills: list[Bill] = [] seen_ids: set[str] = set() # 1. Directly followed bills if followed_bill_ids: result = await db.execute( select(Bill) .options(selectinload(Bill.sponsor), selectinload(Bill.briefs), selectinload(Bill.trend_scores)) .where(Bill.bill_id.in_(followed_bill_ids)) .order_by(desc(Bill.latest_action_date)) .limit(20) ) for bill in result.scalars().all(): if bill.bill_id not in seen_ids: feed_bills.append(bill) seen_ids.add(bill.bill_id) # 2. Bills from followed members if followed_member_ids: result = await db.execute( select(Bill) .options(selectinload(Bill.sponsor), selectinload(Bill.briefs), selectinload(Bill.trend_scores)) .where(Bill.sponsor_id.in_(followed_member_ids)) .order_by(desc(Bill.latest_action_date)) .limit(20) ) for bill in result.scalars().all(): if bill.bill_id not in seen_ids: feed_bills.append(bill) seen_ids.add(bill.bill_id) # 3. Bills matching followed topics (single query with OR across all topics) if followed_topics: result = await db.execute( select(Bill) .options(selectinload(Bill.sponsor), selectinload(Bill.briefs), selectinload(Bill.trend_scores)) .join(BillBrief, Bill.bill_id == BillBrief.bill_id) .where(or_(*[BillBrief.topic_tags.contains([t]) for t in followed_topics])) .order_by(desc(Bill.latest_action_date)) .limit(20) ) for bill in result.scalars().all(): if bill.bill_id not in seen_ids: feed_bills.append(bill) seen_ids.add(bill.bill_id) # Sort feed by latest action date feed_bills.sort(key=lambda b: b.latest_action_date or date.min, reverse=True) return { "feed": [_serialize_bill(b) for b in feed_bills[:50]], "trending": trending, "follows": { "bills": len(followed_bill_ids), "members": len(followed_member_ids), "topics": len(followed_topics), }, }