Skip to content

Enforce max_rankings on the draggable RCV ballot - #1578

Open
masiarek wants to merge 1 commit into
Equal-Vote:mainfrom
masiarek:fix/draggable-max-rankings
Open

masiarek wants to merge 1 commit into
Equal-Vote:mainfrom
masiarek:fix/draggable-max-rankings

Conversation

@masiarek

Copy link
Copy Markdown
Collaborator

Description

The draggable ranked (RCV) ballot ignored the election's max_rankings setting: a voter could drag any number of candidates into "Your Rankings", and only found out at submit time, when server-side validation rejected the ballot they had just built — with no hint while they were building it.

Where the cap was missing: packages/frontend/src/components/Election/Voting/DraggableIRVBallotView.tsxonDragEnd (lines ~78–104 on main) committed a rank for every candidate dropped into the ranked list, with no limit check.

What the server enforces: packages/shared/src/domain_model/Ballot.ts line 84 reads election.settings.max_rankings, and lines 116–124 reject any ranked-method score above it ("Scores out of bounds").

How the classic ranked ballot already enforces it: packages/frontend/src/components/Election/Voting/RankedBallotView.tsx (lines 31–39 on main) resolves the limit — min(max_rankings, REACT_APP_MAX_BALLOT_RANKS ?? 8), defaulting to REACT_APP_DEFAULT_BALLOT_RANKS ?? 6 when unset — and only renders that many rank columns (candidates.slice(0, maxRankings), line ~104), so exceeding the limit is impossible there.

Changes

  • Extracted the classic ballot's limit resolution into getMaxRankings() in packages/frontend/src/components/util.tsx, used by both ranked ballot views, so the two views (and the served-side rule they mirror) cannot drift.
  • DraggableIRVBallotView now refuses a drop into "Your Rankings" once the limit is reached — the candidate stays in "Available Candidates" and a warning Alert explains the limit (it clears when the voter frees up a slot or performs another drag). Reordering within the rankings and removing candidates remain allowed at the limit.
  • Added an "N of M rankings used" counter beside the "Your Rankings" heading, the draggable equivalent of the classic view's visible column count.
  • Two new i18n strings in the ballot section of en.yaml (ballot.rankings_used, ballot.max_rankings_reached); other locales fall back to English.
  • Moved RankedBallotView's draggable early-return below its hooks. It previously returned before them, so paging from a draggable IRV race to another ranked race in the same election changed the hook count between renders and crashed React ("Rendered more hooks than during the previous render" — white page). Found while verifying this fix on a multi-race election.

No server-side validation was changed.

Screenshots / Videos (frontend only)

No screenshots attached; verified manually as described below. The counter renders as grey text after the "Your Rankings" heading ("0 of 6 rankings used"), and the refusal shows a standard MUI warning alert under the rankings list: "You can rank up to 6 candidates on this ballot. To rank this candidate, first drag another one out of your rankings."

Manual verification (local dev server against the production API, election cxrf8v, 5 candidates, with the limit temporarily lowered to 3 locally to make it reachable):

  • Dragging a 4th candidate into "Your Rankings" is refused: the candidate stays in "Available Candidates", the alert appears, the counter stays at "3 of 3 rankings used".
  • Reordering within "Your Rankings" while at the limit still works.
  • Dragging a ranked candidate back out updates the counter to "2 of 3" and hides the alert; ranking a different candidate up to the limit works again with no spurious alert.
  • Keyboard drag-and-drop (space / arrows) goes through the same onDragEnd, so it is covered by the same guard.
  • Paging from the draggable IRV race to the election's Ranked Robin race and back no longer white-screens (the hooks fix).

npx tsc --noEmit passes in packages/frontend; eslint reports no new issues in the touched files (the frontend has no test suite or Storybook to extend).

Related Issues

Part of the draggable-ballot bug list in #1556 (draggable ranked ballot does not respect the maximum-rankings limit).

🤖 Generated with Claude Code

The draggable ranked (RCV) ballot let a voter rank any number of
candidates, but server-side validation
(packages/shared/src/domain_model/Ballot.ts) rejects any ranked ballot
with a score above election.settings.max_rankings — so a voter could
build a ballot that submit then rejected, with no hint while they were
building it. The classic ranked ballot already enforces the limit by
only offering that many rank columns.

- Extract the classic ballot's limit resolution (max_rankings capped at
  REACT_APP_MAX_BALLOT_RANKS, defaulting to REACT_APP_DEFAULT_BALLOT_RANKS
  when the setting is unset) into getMaxRankings() in components/util.tsx
  and use it from both ranked ballot views, so the two cannot drift.
- DraggableIRVBallotView now refuses a drop into "Your Rankings" once the
  limit is reached (reordering within the list and removing candidates
  stay allowed), shows a warning Alert explaining the limit, and displays
  an "N of M rankings used" counter next to the "Your Rankings" heading.
- New i18n strings ballot.rankings_used and ballot.max_rankings_reached
  in en.yaml; other locales fall back to English.
- Move RankedBallotView's draggable early-return below its hooks. It
  returned before them, so paging from a draggable IRV race to another
  ranked race in the same election changed the hook count between renders
  and crashed with "Rendered more hooks than during the previous render"
  (white page).

Verified manually with a local dev server against the production API
(election cxrf8v, with the limit temporarily lowered to exercise the
refusal): drops beyond the limit are refused with the alert shown, the
counter tracks, reorder/remove still work at the limit, and paging
between the draggable IRV race and the Ranked Robin race no longer
crashes. npx tsc --noEmit passes; eslint reports no new issues in the
touched files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@netlify

netlify Bot commented Aug 19, 2026

Copy link
Copy Markdown

Deploy Preview for bettervoting ready!

Name Link
🔨 Latest commit ac09340
🔍 Latest deploy log https://app.netlify.com/projects/bettervoting/deploys/6a861e241d11c50008ea929b
😎 Deploy Preview https://deploy-preview-1578--bettervoting.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@masiarek, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 663264b2-9a56-40b9-8fc3-3c3f9619889e

📥 Commits

Reviewing files that changed from the base of the PR and between 454a38a and ac09340.

📒 Files selected for processing (4)
  • packages/frontend/src/components/Election/Voting/DraggableIRVBallotView.tsx
  • packages/frontend/src/components/Election/Voting/RankedBallotView.tsx
  • packages/frontend/src/components/util.tsx
  • packages/frontend/src/i18n/en.yaml

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant