Add ballot questions to maple#2090
Open
fastfadingviolets wants to merge 186 commits intocodeforboston:mainfrom
Open
Add ballot questions to maple#2090fastfadingviolets wants to merge 186 commits intocodeforboston:mainfrom
fastfadingviolets wants to merge 186 commits intocodeforboston:mainfrom
Conversation
Also adds an integration test
Also includes a clearer "ballotStatus" model as the ballot question progresses along the process.
Update ballot question docs for legislature testimony behavior
Clarify ballot question testimony behavior at legislature stage
Add browse ballot questions page
Add ballotQuestionIds to publishTestimony
Render ballot-question testimony correctly in profile lists
Ballot question follow-up fixes
This was
linked to
issues
Apr 10, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ballot Questions Feature
This PR delivers the MVP for the ballot questions feature, a new section of MAPLE that adds the concept of ballot questions to the site, mimicking the user experience already in-place for bills (i.e., read, contribute testimony/perspective, follow).
Data Model
The ballot question feature adds a thin
/ballotQuestionscollection that gives each petition its own URL and voter-facing metadata, without touching the existing structures for bills, hearings, or testimony.Data Additions
Ballot question documents are defined as YAML files committed to the repo:
A sync script must be manually called upon addition or edit of any YAML file. The script validates each YAML against the BallotQuestion type (f
unctions/src/ballotQuestions/types.ts) before writing and will throw on malformed input.Firestore: Database Indexes & Rules
Composite Indexes (
firestore.indexes.json)Three new composite indexes were added to support the query patterns the feature relies on:
ballotQuestions—electionYearASC +ballotStatusASC (COLLECTION scope): powers the browse page's filter-by-year and filter-by-status queries.publishedTestimony—ballotQuestionIdASC +publishedAtDESC (COLLECTION_GROUP scope): powers fetching all perspectives on a given ballot question, sorted by most recent.publishedTestimony—billIdASC +courtASC +ballotQuestionIdASC (COLLECTION scope): extends the existing bill+court testimony index to also accommodate theballotQuestionIdfield, supporting mixed-scope queries.A field override was also added to declare
ballotQuestionIdas indexable on thepublishedTestimonycollection group, enabling the collection-group-scoped queries above.Security Rules
A new top-level collection rule was added in
firestore.rules:Ballot question documents are publicly readable but not client-writable. All writes go through the admin SDK.
Typesense Changes
Ballot questions themselves are not added to Typesense as a separate search collection — the existing Firestore-native query/filter approach is sufficient for the browse page.
However, because one of the new Firestore composite indexes requires
ballotQuestionIdto be present onpublishedTestimonydocuments (Firestore excludes documents from composite indexes when an indexed field is absent), a backfill was required on thepublishedTestimonycollection. The scriptscripts/firebase-admin/backfillTestimonyBallotQuestionId.tsaddsballotQuestionId: nullto all legacy testimony documents that predate this feature. This makes existing testimony visible in ballot-question-scoped index queries, and also applies toarchivedTestimony. The script is idempotent and safe to re-run.Additionally,
ballotQuestionIdwas added as a field onBaseTestimonyincomponents/db/testimony/types.ts, so newly submitted perspectives can be tagged to a ballot question when appropriate.Back-End Changes
Database Service Layer (
components/db/api.ts)Two new methods were added to
DbService:getBallotQuestion({ id })— fetches a single ballot question by IDgetBallotQuestions()— fetches all ballot questions (used by the browse page)Notification Events (
functions/src/notifications/populateBallotQuestionNotificationEvents.ts)A Firestore-triggered Cloud Function was added on the
/ballotQuestions/{id}document path. When a ballot question'sballotStatuschanges, it creates or updates an entry in thenotificationEventscollection. This integrates with the existing digest notification pipeline so that users who follow a ballot question receive email updates when its status changes or when new perspectives are submitted.Digest Email Templates (
functions/src/email/partials/ballotQuestions/)Two Handlebars templates were added to the digest email system:
ballotQuestion.handlebars— renders a single ballot question card showing its title, court, and perspective counts broken down by position (endorse / neutral / oppose)ballotQuestions.handlebars— wraps multiple ballot question cards; caps display at 4 with a "View All" link when there are moreFront-End Changes
Pages
/pages/ballotQuestions/index.tsx— Browse page: a searchable, filterable grid of all ballot questions/pages/ballotQuestions/[id].tsx— Detail page: full single-question view with tabbed content and a sidebar perspective formComponents (
components/ballotquestions/)BrowseBallotQuestions— The browse grid, supporting:BallotQuestionDetails— The detail page shell, which:BallotQuestionHeader— The hero section at the top of each detail page, showing:YourTestimonyPanelOverviewTab— Shows:TestimoniesTab— Shows:ViewTestimonycomponent filtered to perspectives on this ballot questionYourTestimonyPanel— A conditional panel that:expectedOnBallotphasefailedToAppear,accepted,rejected)DescriptionBox,CommitteeHearing,BallotQuestionTabButton— Supporting display components used by the tabs and nav.status.ts— Exports categorized sets of ballot statuses (activePhases,terminalPhases) used across the header and testimony panel to determine what UI state to render.Tests
Unit and integration tests were added alongside the components:
BrowseBallotQuestions.test.tsxYourTestimonyPanel.test.tsxBallotQuestionTabLinks.test.tsxCommitteeHearing.test.tsxtests/integration/ballotQuestions.test.ts— integration tests verifying YAML sync and composite index queriesData Flow Summary
Known Issues
These issues are known and will be addressed on Monday :)