Files
PocketVeto/backend/app/api/share.py
Jack Levy 4c86a5b9ca feat: PocketVeto v1.0.0 — initial public release
Self-hosted US Congress monitoring platform with AI policy briefs,
bill/member/topic follows, ntfy + RSS + email notifications,
alignment scoring, collections, and draft-letter generator.

Authored by: Jack Levy
2026-03-15 01:35:01 -04:00

114 lines
3.9 KiB
Python

"""
Public share router — no authentication required.
Serves shareable read-only views for briefs and collections.
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database import get_db
from app.models.bill import Bill, BillDocument
from app.models.brief import BillBrief
from app.models.collection import Collection, CollectionBill
from app.schemas.schemas import (
BillSchema,
BriefSchema,
BriefShareResponse,
CollectionDetailSchema,
)
router = APIRouter()
# ── Brief share ───────────────────────────────────────────────────────────────
@router.get("/brief/{token}", response_model=BriefShareResponse)
async def get_shared_brief(
token: str,
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(BillBrief)
.options(
selectinload(BillBrief.bill).selectinload(Bill.sponsor),
selectinload(BillBrief.bill).selectinload(Bill.briefs),
selectinload(BillBrief.bill).selectinload(Bill.trend_scores),
)
.where(BillBrief.share_token == token)
)
brief = result.scalar_one_or_none()
if not brief:
raise HTTPException(status_code=404, detail="Brief not found")
bill = brief.bill
bill_schema = BillSchema.model_validate(bill)
if bill.briefs:
bill_schema.latest_brief = bill.briefs[0]
if bill.trend_scores:
bill_schema.latest_trend = bill.trend_scores[0]
doc_result = await db.execute(
select(BillDocument.bill_id).where(BillDocument.bill_id == bill.bill_id).limit(1)
)
bill_schema.has_document = doc_result.scalar_one_or_none() is not None
return BriefShareResponse(
brief=BriefSchema.model_validate(brief),
bill=bill_schema,
)
# ── Collection share ──────────────────────────────────────────────────────────
@router.get("/collection/{token}", response_model=CollectionDetailSchema)
async def get_shared_collection(
token: str,
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(Collection)
.options(
selectinload(Collection.collection_bills).selectinload(CollectionBill.bill).selectinload(Bill.briefs),
selectinload(Collection.collection_bills).selectinload(CollectionBill.bill).selectinload(Bill.trend_scores),
selectinload(Collection.collection_bills).selectinload(CollectionBill.bill).selectinload(Bill.sponsor),
)
.where(Collection.share_token == token)
)
collection = result.scalar_one_or_none()
if not collection:
raise HTTPException(status_code=404, detail="Collection not found")
cb_list = collection.collection_bills
bills = [cb.bill for cb in cb_list]
bill_ids = [b.bill_id for b in bills]
if bill_ids:
doc_result = await db.execute(
select(BillDocument.bill_id).where(BillDocument.bill_id.in_(bill_ids)).distinct()
)
bills_with_docs = {row[0] for row in doc_result}
else:
bills_with_docs = set()
bill_schemas = []
for bill in bills:
bs = BillSchema.model_validate(bill)
if bill.briefs:
bs.latest_brief = bill.briefs[0]
if bill.trend_scores:
bs.latest_trend = bill.trend_scores[0]
bs.has_document = bill.bill_id in bills_with_docs
bill_schemas.append(bs)
return CollectionDetailSchema(
id=collection.id,
name=collection.name,
slug=collection.slug,
is_public=collection.is_public,
share_token=collection.share_token,
bill_count=len(cb_list),
created_at=collection.created_at,
bills=bill_schemas,
)