Add ballot questions to maple#2090
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
Mephistic
left a comment
There was a problem hiding this comment.
Looks good! None of my comments are blocking for this PR, but worth discussing for the follow-up.
Let's sync before we merge to make sure I've got all the items in the deploy checklist ready. AFAICT, it's just:
- Merge this + Deploy
- Run
syncBallotQuestions - Run
backfillBallotQuestionTestimonyId - Run
backfillTestimonyBallotQuestionId
Happy to move this to DEV for testing (though I can discuss with Matt V tonight whether we should hide the new functionality behind a feature flag before deploying to PROD).
| await db | ||
| .collection("/notificationEvents") | ||
| .doc(docId) | ||
| .update({ |
There was a problem hiding this comment.
Is this the desired behavior? If a Ballot Question goes from state A -> state B -> state C, I would expect to see three notifications (one for A, one for B, and one for C), not a single notification that is modified from state A to B to C.
There was a problem hiding this comment.
In the current model of ballot question states, a question can't move through three states - they would all begin as expectedOnBallot and then all three additional states are terminal (failedToAppear, approved, rejected). A ballot question can't move from A -> B -> C.
In the future if additional states are added, I think multiple meaningful movements wouldn't happen within the timeframe of a single round of email notifications (a month, at most). Coherent additional states might be "legislature" and "confirmedOnBallot" imo, so the path would be:
- legislature -> expectedOnBallot -> confirmedOnBallot -> approved/rejected (terminal) OR
- legislature -> expectedOnBallot -> failedToAppear (terminal)
In that second case, I don't think it's meaningful to the user that a question was expected before it failed. But that's open for discussion, of course! I can add to the agenda to talk about if and when we add new states.
| } | ||
|
|
||
| async function getBallotQuestionCounts(ballotQuestionId: string) { | ||
| const snapshot = await getDocs( |
There was a problem hiding this comment.
Given that we store counts on the Ballot Question docs themselves now, do we still need to handle this case?
There was a problem hiding this comment.
Good catch.
Now that counts are stored on the BQ doc, this is dead code.
I removed it in hyphacoop#52 and merged it here.
| ) | ||
| ) | ||
|
|
||
| return result.docs.reduce<BallotQuestionTestimonySummary>( |
There was a problem hiding this comment.
Same Q here - do we still need the fresh count here now that we're keeping the counts in the Ballot Question itself?
There was a problem hiding this comment.
Same fix applied in hyphacoop#52
It reads directly from the BQ doc now.
89227cf
Definitely let's do DEV before PROD! And I like the feature flag - since we've got all this ready to go before legislature has failed to pass the bills, and we were aiming to release at the same time the ballot questions move out of legislature.
|
Counts are now stored on BallotQuestion docs with defaults of 0, so the hasStored/getStored helpers and live query fallbacks are unreachable. Read counts directly from the doc.
…eries Remove dead collectionGroup fallback queries for BQ counts
…in styles/globals.css
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 :)