fix(members): link sponsors to bills and fix member search
- Poller now fetches bill detail on insert to get sponsor (list endpoint has no sponsor data) - Add backfill_sponsor_ids task + admin endpoint + UI button to fix the 1,616 existing bills with NULL sponsor_id - Member name search now matches both "Last, First" and "First Last" using split_part() on the stored name column; same fix applied to global search - Load Bill.sponsor relationship eagerly in get_member_bills to prevent MissingGreenlet error during Pydantic serialization - Remove .trim() on search onChange so spaces can be typed Authored-By: Jack Levy
This commit is contained in:
@@ -110,6 +110,13 @@ async def get_stats(
|
||||
|
||||
# ── Celery Tasks ──────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/backfill-sponsors")
|
||||
async def backfill_sponsors(current_user: User = Depends(get_current_admin)):
|
||||
from app.workers.congress_poller import backfill_sponsor_ids
|
||||
task = backfill_sponsor_ids.delay()
|
||||
return {"task_id": task.id, "status": "queued"}
|
||||
|
||||
|
||||
@router.post("/trigger-poll")
|
||||
async def trigger_poll(current_user: User = Depends(get_current_admin)):
|
||||
from app.workers.congress_poller import poll_congress_bills
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import desc, func, select
|
||||
from sqlalchemy import desc, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@@ -30,7 +30,15 @@ async def list_members(
|
||||
if state:
|
||||
query = query.where(Member.state == state)
|
||||
if q:
|
||||
query = query.where(Member.name.ilike(f"%{q}%"))
|
||||
# name is stored as "Last, First" — also match "First Last" order
|
||||
first_last = func.concat(
|
||||
func.split_part(Member.name, ", ", 2), " ",
|
||||
func.split_part(Member.name, ", ", 1),
|
||||
)
|
||||
query = query.where(or_(
|
||||
Member.name.ilike(f"%{q}%"),
|
||||
first_last.ilike(f"%{q}%"),
|
||||
))
|
||||
|
||||
total = await db.scalar(select(func.count()).select_from(query.subquery())) or 0
|
||||
query = query.order_by(Member.last_name, Member.first_name).offset((page - 1) * per_page).limit(per_page)
|
||||
@@ -62,7 +70,7 @@ async def get_member_bills(
|
||||
per_page: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
query = select(Bill).options(selectinload(Bill.briefs)).where(Bill.sponsor_id == bioguide_id)
|
||||
query = select(Bill).options(selectinload(Bill.briefs), selectinload(Bill.sponsor)).where(Bill.sponsor_id == bioguide_id)
|
||||
total = await db.scalar(select(func.count()).select_from(query.subquery())) or 0
|
||||
query = query.order_by(desc(Bill.introduced_date)).offset((page - 1) * per_page).limit(per_page)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy import func, or_, select, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
@@ -38,10 +38,17 @@ async def search(
|
||||
seen.add(b.bill_id)
|
||||
bills.append(b)
|
||||
|
||||
# Fuzzy member search
|
||||
# Fuzzy member search — matches "Last, First" and "First Last"
|
||||
first_last = func.concat(
|
||||
func.split_part(Member.name, ", ", 2), " ",
|
||||
func.split_part(Member.name, ", ", 1),
|
||||
)
|
||||
member_results = await db.execute(
|
||||
select(Member)
|
||||
.where(Member.name.ilike(f"%{q}%"))
|
||||
.where(or_(
|
||||
Member.name.ilike(f"%{q}%"),
|
||||
first_last.ilike(f"%{q}%"),
|
||||
))
|
||||
.order_by(Member.last_name)
|
||||
.limit(10)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user