From 8cce73fffc5bf0d0a12e2567c340ffe6d20e1d80 Mon Sep 17 00:00:00 2001 From: Charlie Carlton Date: Tue, 9 Jul 2024 10:57:13 -0600 Subject: [PATCH 1/8] Basic design for IRV Audit ballot --- client/screen.css | 14 +- .../County/Audit/Wizard/BallotAuditStage.tsx | 42 +++--- .../County/Audit/Wizard/IrvChoiceForm.tsx | 135 ++++++++++++++++++ client/src/types/index.d.ts | 8 ++ 4 files changed, 178 insertions(+), 21 deletions(-) create mode 100644 client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx diff --git a/client/screen.css b/client/screen.css index 6fd6fe7f..0d7a3633 100644 --- a/client/screen.css +++ b/client/screen.css @@ -611,13 +611,19 @@ th.rla-county-contest-info { color: #666; } -.contest-choice-grid { +.plurality-contest-choice-grid { display: grid; grid-column-gap: 1rem; grid-template-columns: 1fr 1fr 1fr; margin-bottom: 20px; } +.irv-contest-choice-grid { + display: grid; + grid-column-gap: 1rem; + margin-bottom: 20px; +} + .contest-choice-review-grid { display: grid; grid-column-gap: 1rem; @@ -634,6 +640,12 @@ th.rla-county-contest-info { margin-bottom: 20px; } +.rla-contest-choice-name { + display: flex; + align-items: center; + border-right: 1px solid #ddd; +} + .edit-button { padding-right: 40px; text-align: right; diff --git a/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx b/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx index e4152251..0aa38a9b 100644 --- a/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx +++ b/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx @@ -1,8 +1,8 @@ -import * as React from 'react'; +import * as React from "react"; import * as _ from 'lodash'; -import { Button, Checkbox, Dialog, EditableText, Intent } from '@blueprintjs/core'; +import { Button, Checkbox, EditableText, Intent, Dialog } from '@blueprintjs/core'; import BackButton from './BackButton'; import WaitingForNextBallot from './WaitingForNextBallot'; @@ -10,6 +10,7 @@ import WaitingForNextBallot from './WaitingForNextBallot'; import CommentIcon from '../../../CommentIcon'; import ballotNotFound from 'corla/action/county/ballotNotFound'; +import IrvChoiceForm from "corla/component/County/Audit/Wizard/IrvChoiceForm"; interface NotFoundProps { notFound: OnClick; @@ -106,19 +107,13 @@ const ContestInfo = ({ contest }: ContestInfoProps) => { ); }; -interface ChoicesProps { - choices: ContestChoice[]; - marks: County.ACVRContest; - noConsensus: boolean; - updateBallotMarks: OnClick; -} - const ContestChoices = (props: ChoicesProps) => { const { choices, marks, noConsensus, updateBallotMarks, + description, } = props; function updateChoiceByName(name: string) { @@ -131,7 +126,7 @@ const ContestChoices = (props: ChoicesProps) => { return updateChoice; } - const choiceForms = _.map(choices, choice => { + const pluralityChoiceForms = _.map(choices, choice => { const checked = marks.choices[choice.name]; return ( @@ -147,9 +142,13 @@ const ContestChoices = (props: ChoicesProps) => { ); }); + function isPlurality() { + return description === 'PLURALITY'; + } + return ( -
- {choiceForms} +
+ {isPlurality() ? pluralityChoiceForms : IrvChoiceForm(props)}
); }; @@ -191,7 +190,7 @@ const BallotContestMarkForm = (props: MarkFormProps) => { const contestMarks = acvr[contest.id]; const updateChoices = (candidates: ContestChoice[], desc: string): ContestChoice[] => { - if (desc === 'PLURALITY') { + if (desc === 'PLURALITY' || desc === 'IRV') { return candidates; } // Replace each candidate 'c' in 'cands' with 'c(1)', 'c(2)', etc. @@ -244,10 +243,11 @@ const BallotContestMarkForm = (props: MarkFormProps) => {
@@ -258,12 +258,14 @@ const BallotContestMarkForm = (props: MarkFormProps) => { No audit board consensus
-
- - Blank vote - no mark -
+ {description === 'PLURALITY' ? ( +
+ + Blank vote - no mark +
+ ) : null}
diff --git a/client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx b/client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx new file mode 100644 index 00000000..622f96d5 --- /dev/null +++ b/client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx @@ -0,0 +1,135 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import {Checkbox} from '@blueprintjs/core'; + +interface IrvChoiceFormRanking { + name: string; + rank: number; + value: string; + label: string; +} + +const IrvChoiceForm = (props: ChoicesProps) => { + const { + choices, + marks, + noConsensus, + updateBallotMarks, + description, + } = props; + + const numberOfRankChoices = choices.length; + const noRankSuffix: string = '(none)'; + const noRankRanking: number = -1; + + function getNoRankButton(candidateName: string) { + const noRankValue = `${candidateName}${noRankSuffix}`; + const noRank: IrvChoiceFormRanking = {label: 'No Rank', name: candidateName, rank: noRankRanking, + value: noRankValue}; + + const checked = marks.choices[noRankValue]; + + return ( +
+ + {noRank.label} + +
+ ); + } + + function updateChoiceByRanking(irvChoice: IrvChoiceFormRanking) { + function updateChoice(e: React.ChangeEvent) { + const checkbox = e.target; + + // Special conditions exist for 'No Rank' + if (checkbox.checked) { + handleNoRankButton(irvChoice); + } + + updateBallotMarks({ choices: { [irvChoice.value]: checkbox.checked } }); + } + + // tslint:disable-next-line:no-console + console.log(marks); + + return updateChoice; + } + + function handleNoRankButton(irvChoice: IrvChoiceFormRanking) { + // 'No Rank' Checked + if (irvChoice.rank === noRankRanking) { + // Uncheck any other rankings in that candidate's row + for (let i = 0; i < numberOfRankChoices; i++) { + marks.choices[irvChoice.name + `(${i + 1})`] = false; + } + // Another ranking checked + } else if (marks.choices[irvChoice.name + noRankSuffix]) { + // Uncheck 'No Rank' for that row if it was previously checked + marks.choices[irvChoice.name + noRankSuffix] = false; + } + } + + function getRankings(candidateName: string, candidateDescription: string) { + const rankingOptions = []; + + rankingOptions.push( +
+ {candidateName} + {candidateDescription ?
: null} + {candidateDescription} +
, + ); + + for (let i = 0; i < numberOfRankChoices; i++) { + const rank = i + 1; + const ranking: IrvChoiceFormRanking = {label: (rank).toString(), name: candidateName, rank, + value: candidateName + `(${rank})`}; + const checked = marks.choices[ranking.value]; + + rankingOptions.push( +
+ + {ranking.label} + +
, + ); + } + + rankingOptions.push(getNoRankButton(candidateName)); + + return rankingOptions; + } + + return ( +
+
+ Candidate +
+
+ Ranked Vote Choice +
+ {_.map(choices, (choice, index) => { + return ( + getRankings(choice.name, choice.description) + ); + })} +
+ ); +}; + +export default IrvChoiceForm; diff --git a/client/src/types/index.d.ts b/client/src/types/index.d.ts index d6e9c1ae..18f4817f 100644 --- a/client/src/types/index.d.ts +++ b/client/src/types/index.d.ts @@ -174,6 +174,14 @@ interface CountyInfo { name: string; } +interface ChoicesProps { + choices: ContestChoice[]; + marks: County.ACVRContest; + noConsensus: boolean; + updateBallotMarks: OnClick; + description: string; +} + // TODO: Narrow type. type OnClick = (...args: any[]) => any; From 8eed2f5138e742aa652e456ff20ffdd42d1e5c92 Mon Sep 17 00:00:00 2001 From: Charlie Carlton Date: Tue, 9 Jul 2024 10:58:07 -0600 Subject: [PATCH 2/8] Remove floating semicolon next to audit board button --- client/src/component/County/Dashboard/Page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/component/County/Dashboard/Page.tsx b/client/src/component/County/Dashboard/Page.tsx index d11872e2..27c98cfb 100644 --- a/client/src/component/County/Dashboard/Page.tsx +++ b/client/src/component/County/Dashboard/Page.tsx @@ -44,7 +44,7 @@ const CountyDashboardPage = (props: PageProps) => { currentRoundNumber={ currentRoundNumber } history={ history } name={ countyInfo.name } - auditBoardButtonDisabled={ auditBoardButtonDisabled } />; + auditBoardButtonDisabled={ auditBoardButtonDisabled } /> ; return ; }; From 3064a9dd1eda0544b1984bd08e58809b7cfdc134 Mon Sep 17 00:00:00 2001 From: Charlie Carlton Date: Tue, 9 Jul 2024 11:01:58 -0600 Subject: [PATCH 3/8] Fix tslint issues --- .../County/Audit/Wizard/BallotAuditStage.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx b/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx index 0aa38a9b..054aafc1 100644 --- a/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx +++ b/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx @@ -1,8 +1,8 @@ -import * as React from "react"; +import * as React from 'react'; import * as _ from 'lodash'; -import { Button, Checkbox, EditableText, Intent, Dialog } from '@blueprintjs/core'; +import { Button, Checkbox, Dialog, EditableText, Intent } from '@blueprintjs/core'; import BackButton from './BackButton'; import WaitingForNextBallot from './WaitingForNextBallot'; @@ -10,7 +10,7 @@ import WaitingForNextBallot from './WaitingForNextBallot'; import CommentIcon from '../../../CommentIcon'; import ballotNotFound from 'corla/action/county/ballotNotFound'; -import IrvChoiceForm from "corla/component/County/Audit/Wizard/IrvChoiceForm"; +import IrvChoiceForm from 'corla/component/County/Audit/Wizard/IrvChoiceForm'; interface NotFoundProps { notFound: OnClick; @@ -32,7 +32,6 @@ const BallotNotFoundForm = (props: NotFoundProps) => {
-
Are you looking at the right ballot?
Before making any selections below, first make sure the paper ballot you are examining @@ -321,7 +320,6 @@ interface BallotAuditStageState { showDialog: boolean; } - class BallotAuditStage extends React.Component { constructor(props: StageProps) { @@ -333,8 +331,6 @@ class BallotAuditStage extends React.Component; } - return (
From d398c59dcdeb416af6bc2e8782767689508efad3 Mon Sep 17 00:00:00 2001 From: Charlie Carlton Date: Tue, 9 Jul 2024 13:17:44 -0600 Subject: [PATCH 4/8] Remove unused function --- .../County/Audit/Wizard/BallotAuditStage.tsx | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx b/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx index 054aafc1..84eeb14e 100644 --- a/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx +++ b/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx @@ -188,29 +188,6 @@ const BallotContestMarkForm = (props: MarkFormProps) => { const acvr = countyState.acvrs![currentBallot.id]; const contestMarks = acvr[contest.id]; - const updateChoices = (candidates: ContestChoice[], desc: string): ContestChoice[] => { - if (desc === 'PLURALITY' || desc === 'IRV') { - return candidates; - } - // Replace each candidate 'c' in 'cands' with 'c(1)', 'c(2)', etc. - // For now, let's assume the number of votes on the ballot can be as high as - // the number of candidates. (This is not correct, but will be good enough - // for prototyping purposes). - const ranks = candidates.length; - - const newChoices: ContestChoice[] = []; - candidates.forEach(c => { - for (let i = 1; i <= ranks; i++) { - const newChoice: ContestChoice = { - description: c.description, - name: c.name + '(' + i + ')', - }; - newChoices.push(newChoice); - } - }); - return newChoices; - }; - const updateComments = (comments: string) => { updateBallotMarks({ comments }); }; From ba7f3d9456379a3461fd53381e5a2741f79480a8 Mon Sep 17 00:00:00 2001 From: Charlie Carlton Date: Tue, 9 Jul 2024 14:53:35 -0600 Subject: [PATCH 5/8] Don't give 'no rank' buttons a value to be recorded during audit --- .../County/Audit/Wizard/IrvChoiceForm.tsx | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx b/client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx index 622f96d5..4190fb11 100644 --- a/client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx +++ b/client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx @@ -20,26 +20,36 @@ const IrvChoiceForm = (props: ChoicesProps) => { } = props; const numberOfRankChoices = choices.length; - const noRankSuffix: string = '(none)'; - const noRankRanking: number = -1; function getNoRankButton(candidateName: string) { - const noRankValue = `${candidateName}${noRankSuffix}`; - const noRank: IrvChoiceFormRanking = {label: 'No Rank', name: candidateName, rank: noRankRanking, - value: noRankValue}; + function candidateHasRanking() { + for (let i = 0; i < numberOfRankChoices; i++) { + if (marks.choices[candidateName + `(${i + 1})`]) { + return true; + } + } + return false; + } + + function uncheckCandidateRankings(e: React.ChangeEvent) { + const checkbox = e.target; - const checked = marks.choices[noRankValue]; + if (checkbox.checked) { + // Uncheck any other rankings in that candidate's row + for (let i = 0; i < numberOfRankChoices; i++) { + updateBallotMarks({ choices: { [candidateName + `(${i + 1})`]: false } }); + } + } + } return (
- {noRank.label} + checked={!candidateHasRanking()} + onChange={uncheckCandidateRankings}> + No Rank
); @@ -49,11 +59,6 @@ const IrvChoiceForm = (props: ChoicesProps) => { function updateChoice(e: React.ChangeEvent) { const checkbox = e.target; - // Special conditions exist for 'No Rank' - if (checkbox.checked) { - handleNoRankButton(irvChoice); - } - updateBallotMarks({ choices: { [irvChoice.value]: checkbox.checked } }); } @@ -63,20 +68,6 @@ const IrvChoiceForm = (props: ChoicesProps) => { return updateChoice; } - function handleNoRankButton(irvChoice: IrvChoiceFormRanking) { - // 'No Rank' Checked - if (irvChoice.rank === noRankRanking) { - // Uncheck any other rankings in that candidate's row - for (let i = 0; i < numberOfRankChoices; i++) { - marks.choices[irvChoice.name + `(${i + 1})`] = false; - } - // Another ranking checked - } else if (marks.choices[irvChoice.name + noRankSuffix]) { - // Uncheck 'No Rank' for that row if it was previously checked - marks.choices[irvChoice.name + noRankSuffix] = false; - } - } - function getRankings(candidateName: string, candidateDescription: string) { const rankingOptions = []; @@ -123,7 +114,7 @@ const IrvChoiceForm = (props: ChoicesProps) => {
Ranked Vote Choice
- {_.map(choices, (choice, index) => { + {_.map(choices, choice => { return ( getRankings(choice.name, choice.description) ); From e20419b203258bd1b9c45787891e5b184af79743 Mon Sep 17 00:00:00 2001 From: Charlie Carlton Date: Tue, 9 Jul 2024 14:54:57 -0600 Subject: [PATCH 6/8] Remove console.log --- client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx b/client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx index 4190fb11..4311fe7e 100644 --- a/client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx +++ b/client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx @@ -62,9 +62,6 @@ const IrvChoiceForm = (props: ChoicesProps) => { updateBallotMarks({ choices: { [irvChoice.value]: checkbox.checked } }); } - // tslint:disable-next-line:no-console - console.log(marks); - return updateChoice; } From 054893fe2230e77a2946a8e3297e6b37664c9375 Mon Sep 17 00:00:00 2001 From: Charlie Carlton Date: Tue, 9 Jul 2024 15:32:23 -0600 Subject: [PATCH 7/8] Add Blank Vote button back in for IRV races --- .../County/Audit/Wizard/BallotAuditStage.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx b/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx index 84eeb14e..ce460b7f 100644 --- a/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx +++ b/client/src/component/County/Audit/Wizard/BallotAuditStage.tsx @@ -234,14 +234,12 @@ const BallotContestMarkForm = (props: MarkFormProps) => { No audit board consensus
- {description === 'PLURALITY' ? ( -
- - Blank vote - no mark -
- ) : null} +
+ + Blank vote - no mark +
From a108a3dfea4d0cb4923d9d67eb655525b2c3df5e Mon Sep 17 00:00:00 2001 From: charliecarlton Date: Thu, 8 Aug 2024 10:12:43 -0600 Subject: [PATCH 8/8] Fix castexception for json audit report download by outputting stream to array before writing --- .../freeandfair/corla/query/ExportQueries.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/server/eclipse-project/src/main/java/us/freeandfair/corla/query/ExportQueries.java b/server/eclipse-project/src/main/java/us/freeandfair/corla/query/ExportQueries.java index 4d2ad2d3..48c9b325 100644 --- a/server/eclipse-project/src/main/java/us/freeandfair/corla/query/ExportQueries.java +++ b/server/eclipse-project/src/main/java/us/freeandfair/corla/query/ExportQueries.java @@ -128,24 +128,21 @@ public static void jsonOut(final String query, final OutputStream os) { // interleave an object separator (the comma and line break) into the stream // of json objects to create valid json thx! // https://stackoverflow.com/a/25624818 + // call .skip() to remove the first separator final Stream results = - q.stream().flatMap(i -> Stream.of(new String[] {",\n"}, i)).skip(1); // remove - // the - // first - // separator + q.stream().flatMap(i -> Stream.of(",\n", i)).skip(1); - // write json by hand to preserve streaming writes in case of big data try { os.write("[".getBytes(StandardCharsets.UTF_8)); - results.forEach(line -> { + Object[] resultsArr = results.toArray(); + + for(Object line : resultsArr) { try { - // the object array is the columns, but in this case there is only - // one, so we take it at index 0 - os.write(line[0].toString().getBytes(StandardCharsets.UTF_8)); + os.write(line.toString().getBytes(StandardCharsets.UTF_8)); } catch (java.io.IOException e) { LOGGER.error(e.getMessage()); } - }); + } os.write("]".getBytes(StandardCharsets.UTF_8)); } catch (java.io.IOException e) {