Skip to content

WIP: Write in candidates#841

Draft
mikefranze wants to merge 6 commits intoEqual-Vote:mainfrom
mikefranze:Write-In-Candidates
Draft

WIP: Write in candidates#841
mikefranze wants to merge 6 commits intoEqual-Vote:mainfrom
mikefranze:Write-In-Candidates

Conversation

@mikefranze
Copy link
Collaborator

Description

Work in progress, do not merge! Just wanted to get some eyes on it.

This adds write in fields to the domain models, adds routes for getting the write in names and setting the processed write in names, and updates to the results processing to check for write ins in the ballots and assign the scores to the correct candidates.

In the race object, enable_write_in has been added which will be a setting in the race menu. This enables the write in field on the ballot and allows the backend to include them for tabulation.

On the score object in the ballot, a write in name has been added.

A new route getWriteIns provides the admin with a list of all write in names and their number of occurrences for each race. The admin then processes the names, groups any similar or misspelled names together, and approves or rejects them the candidates.

Admin then uses a new route setWriteInResults to save the processed write in data to the election.

While processing the results, if it comes across a vote with an invalid candidate ID but with a write in name, it checks if that write in name is an alias of an approved write in candidate and maps the scores. This required updating the code to check all of the candidate IDs of the votes and make sure they map to the candidate IDs in the race.

Copy link
Member

@ArendPeter ArendPeter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly nitpicks, but overall it looks good!

@@ -0,0 +1,12 @@
import { Uid } from "./Uid";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that @recursivesquircle is working on shorter IDs at the moment.
#733

Depending on when this is merged you may need to rebase so that the race id uses the new format

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know and I'll keep an eye out, but this is just the Uid type which is just a string which I doubt will change.

const candidateNames = race.candidates.map((Candidate: any) => (Candidate.candidate_name))
const candidateIDs = race.candidates.map((Candidate: any) => (Candidate.candidate_id))
const useWriteIns = race.enable_write_in && race.write_in_candidates && race.write_in_candidates.length>0
const writeInCandidates = useWriteIns && race.write_in_candidates ? race.write_in_candidates : []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, I think you can avoid the terinary expression by doing ```race.write_in_candidates ?? []``

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll give that a shot but I may have tried that. Typescript wasn't very cooperative in this section.

const writeInCandidateIndex = writeInCandidates.findIndex(candidate => candidate.aliases.includes(write_in_name) && candidate.approved)
if (writeInCandidateIndex >= 0) {
row[writeInCandidateIndex + candidateNames.length] = score.score
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an else clause to throw (or log) a warning?

Feels like the writeInCandidateIndex is expected to always resolve

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wouldn't in the case that the write in candidate wasn't approved by the admin. But maybe that should be a separate check? First check if they are an alias to a candidate, if not throw error and admin can revisit write in section. Then check if approved, then add score.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, maybe it would be better to have a counter for unprocessed write ins. That way the results can still be retrieved live and display with the results that X write in names are still unprocessed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated results to have an unprocessed write in candidate count

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Yeah I was just thinking that we should have some default write-in behavior in addition to allowing the admin to configure the aliases

Counting each new variation as it's own candidate is a good starting point, but I could also see us performing some minimal typo checks by default.

if (!election.races[race_index].enable_write_in) {
throw new BadRequest('Write-In not enabled for this race')
}
election.races[race_index].write_in_candidates = write_in_results.write_in_candidates
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also update write_in_candidates during the cast vote flow to ensure that any new write ins are added as new candidates or alias?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not update the election every time a vote is cast. I was thinking the write in processing would occur after all votes are entered. Or the admin can do processing as the election is running.

@masiarek
Copy link
Collaborator

Write-in issue number: #730

@jacksonloper jacksonloper marked this pull request as draft December 8, 2025 20:20
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.

3 participants