elections app
Secure elections at chapter or org scope. Anonymous ballots, live tracking, runoffs.
Models (6)
Election— top-level container; FK to org or chapter; status (draft / open / closed); allow_runoffsElectionPosition— a position to fill; seats, candidacy_method (open / slate), thresholdElectionCandidate— a candidate for a position; name, statementVote— a single voter's choices; FK to election + voter (User), encrypted ballot blobVoteAuditLog— append-only record of vote events (cast, viewed, tabulated)ElectionResult— computed totals per position after close
Key endpoints
| URL | Purpose |
|---|---|
GET /api/organizations/<id>/elections/ | List org elections |
POST /api/organizations/<id>/elections/ | Create draft election |
POST /api/elections/<id>/positions/ | Add a position |
POST /api/elections/<id>/candidates/ | Add a candidate |
POST /api/elections/<id>/open/ | Open voting |
POST /api/elections/<id>/close/ | Close voting (early or scheduled) |
GET /api/elections/<id>/ballot/ | Get ballot for current user |
POST /api/elections/<id>/votes/ | Submit ballot |
GET /api/elections/<id>/results/ | Computed results (after close) |
GET /api/elections/<id>/results/export/ | XLSX export |
WS /ws/elections/<id>/ | Live tracking (officers): vote count + voter turnout |
Permissions
IsNationalAdmin— org-wide election creationIsChapterOfficer— chapter election creationIsEligibleVoter(custom) — submit a vote (checks status, region, chapter, custom criteria perElection.eligibility_filter)
Background tasks
publish_election_results(election_id)— runs at scheduled close time; computes totals, marks election closed, sends winner notificationssend_voting_reminders(election_id)— pings eligible voters who haven't voted
Notable patterns
Anonymous ballots
Vote.choices_blob is encrypted with a per-election Fernet key. Officers see whether a user voted (for quorum) but never how. After close, the system decrypts ballots in aggregate to compute results.
Eligibility filter
Election.eligibility_filter is a JSONField:
{
"statuses": ["undergraduate", "alumni_active"],
"regions": ["uuid-1", "uuid-2"],
"chapters": null,
"custom_field_match": {"key": "voting_delegate", "value": true}
}
Resolved into a queryset by apps/elections/services/eligibility.py. IsEligibleVoter.has_permission() checks the requesting user against this.
Runoffs
If Election.allow_runoffs=True and no candidate hits threshold:
publish_election_resultscreates a newElectionwith status=open and the top contenders only- New voting window (default 24h)
- Same eligibility, same anonymity
Live tracking
ws/elections/<id>/ is a Django Channels consumer (ElectionLiveConsumer). Broadcasts:
vote_count_updated— total ballots castvoter_turnout_updated— percent of eligible voters who voted
No info about who voted what — purely aggregate.
Audit log
Every vote action writes to VoteAuditLog:
vote_cast— voter ID + timestamp + IP hash (no choices)vote_viewed— officer / admin viewed (for compliance trail)tabulation_started/tabulation_completed— close events
Used for certification reports.
Code paths
- Models:
backend/apps/elections/models.py - Views:
backend/apps/elections/views.py - WebSocket consumer:
backend/apps/elections/consumers.py - Eligibility service:
backend/apps/elections/services/eligibility.py - Tabulation:
backend/apps/elections/services/tabulation.py