feat: ZIP → rep lookup, member page redesign, letter improvements
ZIP lookup (GET /api/members/by-zip/{zip}):
- Two-step geocoding: Nominatim (ZIP → lat/lng) then Census TIGERweb
Legislative identify (lat/lng → congressional district via GEOID)
- Handles at-large states (AK, DE, MT, ND, SD, VT, WY)
- Added rep_lookup health check to admin External API Health panel
congress_api.py fixes:
- parse_member_from_api: normalize state full name → 2-letter code
(Congress.gov returns "Florida", DB expects "FL")
- parse_member_from_api: read district from top-level data field,
not current_term (district is not inside the term object)
Celery beat: schedule sync_members daily at 1 AM UTC so chamber,
district, and contact info stay current without manual triggering
Members page redesign: photo avatars, party/state/chamber chips,
phone + website links, ZIP lookup form to find your reps
Draft letter improvements: pass rep_name from ZIP lookup so letter
opens with "Dear Representative Franklin," instead of generic salutation;
add has_document filter to bills list endpoint
UX additions: HelpTip component, How It Works page, "How it works"
sidebar nav link, collections page description copy
Authored-By: Jack Levy
This commit is contained in:
@@ -272,9 +272,10 @@ async def api_health(current_user: User = Depends(get_current_admin)):
|
||||
asyncio.to_thread(_test_govinfo),
|
||||
asyncio.to_thread(_test_newsapi),
|
||||
asyncio.to_thread(_test_gnews),
|
||||
asyncio.to_thread(_test_rep_lookup),
|
||||
return_exceptions=True,
|
||||
)
|
||||
keys = ["congress_gov", "govinfo", "newsapi", "google_news"]
|
||||
keys = ["congress_gov", "govinfo", "newsapi", "google_news", "rep_lookup"]
|
||||
return {
|
||||
k: r if isinstance(r, dict) else {"status": "error", "detail": str(r)}
|
||||
for k, r in zip(keys, results)
|
||||
@@ -370,6 +371,54 @@ def _test_gnews() -> dict:
|
||||
return {"status": "error", "detail": str(exc)}
|
||||
|
||||
|
||||
def _test_rep_lookup() -> dict:
|
||||
import re as _re
|
||||
import requests as req
|
||||
def _call():
|
||||
# Step 1: Nominatim ZIP → lat/lng
|
||||
r1 = req.get(
|
||||
"https://nominatim.openstreetmap.org/search",
|
||||
params={"postalcode": "20001", "country": "US", "format": "json", "limit": "1"},
|
||||
headers={"User-Agent": "PocketVeto/1.0"},
|
||||
timeout=10,
|
||||
)
|
||||
r1.raise_for_status()
|
||||
places = r1.json()
|
||||
if not places:
|
||||
return {"status": "error", "detail": "Nominatim: no result for test ZIP 20001"}
|
||||
lat, lng = places[0]["lat"], places[0]["lon"]
|
||||
half = 0.5
|
||||
# Step 2: TIGERweb identify → congressional district
|
||||
r2 = req.get(
|
||||
"https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/Legislative/MapServer/identify",
|
||||
params={
|
||||
"f": "json",
|
||||
"geometry": f"{lng},{lat}",
|
||||
"geometryType": "esriGeometryPoint",
|
||||
"sr": "4326",
|
||||
"layers": "all",
|
||||
"tolerance": "2",
|
||||
"mapExtent": f"{float(lng)-half},{float(lat)-half},{float(lng)+half},{float(lat)+half}",
|
||||
"imageDisplay": "100,100,96",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
r2.raise_for_status()
|
||||
results = r2.json().get("results", [])
|
||||
for item in results:
|
||||
attrs = item.get("attributes", {})
|
||||
cd_field = next((k for k in attrs if _re.match(r"CD\d+FP$", k)), None)
|
||||
if cd_field:
|
||||
district = str(int(str(attrs[cd_field]))) if str(attrs[cd_field]).strip("0") else "At-large"
|
||||
return {"status": "ok", "detail": f"Nominatim + TIGERweb reachable — district {district} found for ZIP 20001"}
|
||||
layers = [r.get("layerName") for r in results]
|
||||
return {"status": "error", "detail": f"Reachable but no CD field found. Layers: {layers}"}
|
||||
try:
|
||||
return _timed(_call)
|
||||
except Exception as exc:
|
||||
return {"status": "error", "detail": str(exc)}
|
||||
|
||||
|
||||
@router.get("/task-status/{task_id}")
|
||||
async def get_task_status(task_id: str, current_user: User = Depends(get_current_admin)):
|
||||
from app.workers.celery_app import celery_app
|
||||
|
||||
Reference in New Issue
Block a user