Skip to content

Commit

Permalink
Merge pull request #159 from DemocracyDevelopers/IRVAuditBallotUISecond
Browse files Browse the repository at this point in the history
Redo pull request with code signing working
  • Loading branch information
vteague authored Jul 23, 2024
2 parents 082738a + a7fb835 commit 809285b
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 41 deletions.
14 changes: 13 additions & 1 deletion client/screen.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
50 changes: 11 additions & 39 deletions client/src/component/County/Audit/Wizard/BallotAuditStage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,7 +32,6 @@ const BallotNotFoundForm = (props: NotFoundProps) => {

<div>


<div className='not-found-header'>Are you looking at the right ballot?</div>
<div className='not-found-copy'>
Before making any selections below, first make sure the paper ballot you are examining
Expand Down Expand Up @@ -106,19 +106,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) {
Expand All @@ -131,7 +125,7 @@ const ContestChoices = (props: ChoicesProps) => {
return updateChoice;
}

const choiceForms = _.map(choices, choice => {
const pluralityChoiceForms = _.map(choices, choice => {
const checked = marks.choices[choice.name];

return (
Expand All @@ -147,9 +141,13 @@ const ContestChoices = (props: ChoicesProps) => {
);
});

function isPlurality() {
return description === 'PLURALITY';
}

return (
<div className='contest-choice-grid'>
{choiceForms}
<div className={isPlurality() ? 'plurality-contest-choice-grid' : ''}>
{isPlurality() ? pluralityChoiceForms : IrvChoiceForm(props)}
</div>
);
};
Expand Down Expand Up @@ -190,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') {
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 });
};
Expand Down Expand Up @@ -244,10 +219,11 @@ const BallotContestMarkForm = (props: MarkFormProps) => {
<ContestInfo contest={contest} />
<ContestChoices
key={contest.id}
choices={updateChoices(choices, description)}
choices={choices}
marks={contestMarks}
noConsensus={!!contestMarks.noConsensus}
updateBallotMarks={updateBallotMarks}
description={description}
/>

<div className='contest-choice-grid'>
Expand Down Expand Up @@ -319,7 +295,6 @@ interface BallotAuditStageState {
showDialog: boolean;
}


class BallotAuditStage extends React.Component<StageProps, BallotAuditStageState> {

constructor(props: StageProps) {
Expand All @@ -331,8 +306,6 @@ class BallotAuditStage extends React.Component<StageProps, BallotAuditStageState
this.setState({ showDialog: !this.state.showDialog });
}



public render() {

const {
Expand Down Expand Up @@ -395,7 +368,6 @@ class BallotAuditStage extends React.Component<StageProps, BallotAuditStageState
return <WaitingForNextBallot />;
}


return (
<div className='rla-page'>
<div>
Expand Down
123 changes: 123 additions & 0 deletions client/src/component/County/Audit/Wizard/IrvChoiceForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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;

function getNoRankButton(candidateName: string) {
function candidateHasRanking() {
for (let i = 0; i < numberOfRankChoices; i++) {
if (marks.choices[candidateName + `(${i + 1})`]) {
return true;
}
}
return false;
}

function uncheckCandidateRankings(e: React.ChangeEvent<HTMLInputElement>) {
const checkbox = e.target;

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 (
<div className='contest-choice-selection'>
<Checkbox
className='rla-contest-choice'
disabled={noConsensus}
checked={!candidateHasRanking()}
onChange={uncheckCandidateRankings}>
<span className='choice-name'>No Rank</span>
</Checkbox>
</div>
);
}

function updateChoiceByRanking(irvChoice: IrvChoiceFormRanking) {
function updateChoice(e: React.ChangeEvent<HTMLInputElement>) {
const checkbox = e.target;

updateBallotMarks({ choices: { [irvChoice.value]: checkbox.checked } });
}

return updateChoice;
}

function getRankings(candidateName: string, candidateDescription: string) {
const rankingOptions = [];

rankingOptions.push(
<div className='rla-contest-choice-name'>
<b>{candidateName}</b>
{candidateDescription ? <br /> : null}
{candidateDescription}
</div>,
);

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(
<div className='contest-choice-selection'>
<Checkbox
className='rla-contest-choice'
key={ranking.value}
value={ranking.value}
disabled={noConsensus}
checked={checked || false}
onChange={updateChoiceByRanking(ranking)}>
<span className='choice-name'>{ranking.label}</span>
</Checkbox>
</div>,
);
}

rankingOptions.push(getNoRankButton(candidateName));

return rankingOptions;
}

return (
<div className='irv-contest-choice-grid'
style={{gridTemplateColumns: `repeat(${numberOfRankChoices + 2}, 1fr)`}}>
<div style={{borderRight: '1px solid #ddd'}}>
Candidate
</div>
<div style={{gridColumn: `2 / ${numberOfRankChoices + 3}`}}>
Ranked Vote Choice
</div>
{_.map(choices, choice => {
return (
getRankings(choice.name, choice.description)
);
})}
</div>
);
};

export default IrvChoiceForm;
2 changes: 1 addition & 1 deletion client/src/component/County/Dashboard/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const CountyDashboardPage = (props: PageProps) => {
currentRoundNumber={ currentRoundNumber }
history={ history }
name={ countyInfo.name }
auditBoardButtonDisabled={ auditBoardButtonDisabled } />;
auditBoardButtonDisabled={ auditBoardButtonDisabled } />
</div>;
return <CountyLayout main={ main } />;
};
Expand Down
8 changes: 8 additions & 0 deletions client/src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit 809285b

Please sign in to comment.