Skip to main content

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_runoffs
  • ElectionPosition — a position to fill; seats, candidacy_method (open / slate), threshold
  • ElectionCandidate — a candidate for a position; name, statement
  • Vote — a single voter's choices; FK to election + voter (User), encrypted ballot blob
  • VoteAuditLog — append-only record of vote events (cast, viewed, tabulated)
  • ElectionResult — computed totals per position after close

Key endpoints

URLPurpose
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 creation
  • IsChapterOfficer — chapter election creation
  • IsEligibleVoter (custom) — submit a vote (checks status, region, chapter, custom criteria per Election.eligibility_filter)

Background tasks

  • publish_election_results(election_id) — runs at scheduled close time; computes totals, marks election closed, sends winner notifications
  • send_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:

  1. publish_election_results creates a new Election with status=open and the top contenders only
  2. New voting window (default 24h)
  3. Same eligibility, same anonymity

Live tracking

ws/elections/<id>/ is a Django Channels consumer (ElectionLiveConsumer). Broadcasts:

  • vote_count_updated — total ballots cast
  • voter_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