feat(votes): Scrutin + RepresentativeVote models (AN roll-call votes)
Scrutins votes — backend (models, endpoints, seat directory)
AN roll-call votes end-to-end: models, import pipeline, aggregate endpoints, seat directory. Pairs with risefor-front !79 and law-tracker !6 (merged).
Models (migrations 0269–0274)
-
Scrutin+RepresentativeVote— idempotent import, per-scrutin integrity checks (8,434 scrutins / 1.27M votes vs AN decompte totals) -
Scrutin.lawFK — plugin-gated model import + plugin-gated migration 0272 (0224/0255 pattern; no crash on non-law-tracker instances) -
isAmendmentVoteclassifier v2 — apostrophe/typo-tolerant, calibrated on all 8,434 titles (7,219 amendment / 1,215 law-level, 0 mismatches) -
Representative.placeHemicycle+ migration 0274 — populated byimport_elected_gouvfrom the ANmandature.placeHemicycle(577/577 deputies, DB == AN verified)
Endpoints
-
/scrutins/{id}/votes/— nested container (votes NOT in the list serializer — timeout lesson) -
/scrutins/{id}/party-tallies/— per-group tallies + size/absent + seatDirectory (occupant identity + party urlid for every official seat) + per-group voter seats (numPlace= real AN seat) -
/representatives/{id}/vote-history/— server-side join, 200/page, newest first
Tooling
-
sync_rep_seatscommand — derives deputy seats from vote numPlace mode (≥70% confidence, dry-run default) - Absent counter = group directory chairs − group voter seats → always matches the amber chairs on the hemicycle (SOC 25=25, DEM 11=11, Horizons 15=15 verified)
Prod runbook (after merge)
python manage.py migrate risefor_lobbying # 0269–0274 (0272 auto-skips without plugin)
env/bin/python law-tracker/.../update-data.py --type suivi-loi # laws + scrutins + votes
env/bin/python law-tracker/.../update-data.py --type deputee # deputies + seats
then restart backend + purge CF /static/*.