Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display total count instead of percent and order by count #123

Merged
merged 6 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions web/frontend/src/pages/form/GroupedResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,15 @@ const GroupedResult: FC<GroupedResultProps> = ({ rankResult, selectResult, textR
const select = element as SelectQuestion;

if (selectResult.has(id)) {
res = countSelectResult(selectResult.get(id)).resultsInPercent.map((percent, index) => {
return { Candidate: select.Choices[index], Percentage: `${percent}%` };
});
res = countSelectResult(selectResult.get(id))
.map(([, totalCount], index) => {
Copy link
Member

Choose a reason for hiding this comment

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

The linter doesn't complain? I thought you'd have to write .map(([_, totalCount] here.

Copy link
Member Author

Choose a reason for hiding this comment

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

no, the linter is quite happy like that and actually made me change it to this 🤷‍♀️

return {
Candidate: select.Choices[index],
TotalCount: totalCount,
NumberOfBallots: selectResult.get(id).length, // number of combined ballots for this election
};
})
.sort((x, y) => y.TotalCount - x.TotalCount);
dataToDownload.push({ Title: element.Title.En, Results: res });
}
break;
Expand Down
30 changes: 28 additions & 2 deletions web/frontend/src/pages/form/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ type ProgressBarProps = {
children: string;
};

const ProgressBar: FC<ProgressBarProps> = ({ isBest, children }) => {
type SelectProgressBarProps = {
percent: string;
totalCount: number;
numberOfBallots: number;
isBest: boolean;
};

export const ProgressBar: FC<ProgressBarProps> = ({ isBest, children }) => {
return (
<div className="sm:ml-1 md:ml-2 w-3/5 sm:w-4/5">
<div className="h-min bg-white rounded-full mr-1 md:mr-2 w-full flex items-center">
Expand All @@ -21,4 +28,23 @@ const ProgressBar: FC<ProgressBarProps> = ({ isBest, children }) => {
);
};

export default ProgressBar;
export const SelectProgressBar: FC<SelectProgressBarProps> = ({
percent,
totalCount,
numberOfBallots,
isBest,
}) => {
return (
<div className="sm:ml-1 md:ml-2 w-3/5 sm:w-4/5">
<div className="h-min bg-white rounded-full mr-1 md:mr-2 w-full flex items-center">
<div
className={`${!isBest && totalCount !== 0 && 'bg-indigo-300'} ${
!isBest && totalCount === 0 && 'bg-indigo-100'
} ${isBest && 'bg-indigo-500'} flex-none mr-2 text-white h-2 sm:h-3 p-0.5 rounded-full`}
style={{ width: `${percent}%` }}></div>

<div className="text-gray-700 text-sm">{`${totalCount}/${numberOfBallots}`}</div>
</div>
</div>
);
};
2 changes: 1 addition & 1 deletion web/frontend/src/pages/form/components/RankResult.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { RankQuestion } from 'types/configuration';
import ProgressBar from './ProgressBar';
import { ProgressBar } from './ProgressBar';
import { countRankResult } from './utils/countResult';
import { default as i18n } from 'i18next';

Expand Down
36 changes: 24 additions & 12 deletions web/frontend/src/pages/form/components/SelectResult.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { SelectQuestion } from 'types/configuration';
import ProgressBar from './ProgressBar';
import { SelectProgressBar } from './ProgressBar';
import { countSelectResult } from './utils/countResult';
import { default as i18n } from 'i18next';

Expand All @@ -11,23 +11,33 @@ type SelectResultProps = {

// Display the results of a select question.
const SelectResult: FC<SelectResultProps> = ({ select, selectResult }) => {
const { resultsInPercent, maxIndices } = countSelectResult(selectResult);
const sortedResults = countSelectResult(selectResult)
.map((result, index) => {
const tempResult: [string, number, number] = [...result, index];
return tempResult;
})
.sort((x, y) => y[1] - x[1]);
const maxCount = sortedResults[0][1];

const displayResults = () => {
return resultsInPercent.map((percent, index) => {
const isBest = maxIndices.includes(index);

return sortedResults.map(([percent, totalCount, origIndex], index) => {
return (
<React.Fragment key={index}>
<div className="px-2 sm:px-4 break-words max-w-xs w-max">
<span>
{i18n.language === 'en' && select.ChoicesMap.get('en')[index]}
{i18n.language === 'fr' && select.ChoicesMap.get('fr')[index]}
{i18n.language === 'de' && select.ChoicesMap.get('de')[index]}
{
(select.ChoicesMap.has(i18n.language)
? select.ChoicesMap.get(i18n.language)
: select.ChoicesMap.get('en'))[origIndex]
}
</span>
:
</div>
<ProgressBar isBest={isBest}>{percent}</ProgressBar>
<SelectProgressBar
percent={percent}
totalCount={totalCount}
numberOfBallots={selectResult.length}
isBest={totalCount === maxCount}></SelectProgressBar>
</React.Fragment>
);
});
Expand Down Expand Up @@ -56,9 +66,11 @@ export const IndividualSelectResult: FC<SelectResultProps> = ({ select, selectRe
<div className="flex flex-row px-2 sm:px-4 break-words max-w-xs w-max">
<div className="h-4 w-4 mr-2 accent-indigo-500 ">{displayChoices(result, index)}</div>
<div>
{i18n.language === 'en' && select.ChoicesMap.get('en')[index]}
{i18n.language === 'fr' && select.ChoicesMap.get('fr')[index]}
{i18n.language === 'de' && select.ChoicesMap.get('de')[index]}
{
(select.ChoicesMap.has(i18n.language)
? select.ChoicesMap.get(i18n.language)
: select.ChoicesMap.get('en'))[index]
}
</div>
</div>
</React.Fragment>
Expand Down
2 changes: 1 addition & 1 deletion web/frontend/src/pages/form/components/TextResult.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { TextQuestion } from 'types/configuration';
import ProgressBar from './ProgressBar';
import { ProgressBar } from './ProgressBar';
import { countTextResult } from './utils/countResult';
import { default as i18n } from 'i18next';

Expand Down
37 changes: 13 additions & 24 deletions web/frontend/src/pages/form/components/utils/countResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,31 +42,20 @@ const countRankResult = (rankResult: number[][], rank: RankQuestion) => {
// percentage of the total number of votes and which candidate(s) in the
// select.Choices has the most votes
const countSelectResult = (selectResult: number[][]) => {
const resultsInPercent: string[] = [];
const maxIndices: number[] = [];
let max = 0;

const results = selectResult.reduce((a, b) => {
return a.map((value, index) => {
const current = value + b[index];

if (current >= max) {
max = current;
}
return current;
const results: [string, number][] = [];

selectResult
.reduce(
(tally, currBallot) => tally.map((currCount, index) => currCount + currBallot[index]),
new Array(selectResult[0].length).fill(0)
)
.forEach((totalCount) => {
results.push([
(Math.round((totalCount / selectResult.length) * 100 * 100) / 100).toFixed(2).toString(),
totalCount,
]);
Comment on lines +47 to +56
Copy link
Member

Choose a reason for hiding this comment

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

Or remove the let result altogether and do something like:

Suggested change
selectResult
.reduce(
(tally, currBallot) => tally.map((currCount, index) => currCount + currBallot[index]),
new Array(selectResult[0].length).fill(0)
)
.forEach((totalCount) => {
results.push([
(Math.round((totalCount / selectResult.length) * 100 * 100) / 100).toFixed(2).toString(),
totalCount,
]);
return selectResult
.reduce(
(tally, currBallot) => tally.map((currCount, index) => currCount + currBallot[index]),
new Array(selectResult[0].length).fill(0)
)
.map((totalCount) =>
[
(Math.round((totalCount / selectResult.length) * 100 * 100) / 100).toFixed(2).toString(),
totalCount,
]);

});
}, new Array(selectResult[0].length).fill(0));

results.forEach((count, index) => {
if (count === max) {
maxIndices.push(index);
}

const percentage = (count / selectResult.length) * 100;
const roundedPercentage = (Math.round(percentage * 100) / 100).toFixed(2);
resultsInPercent.push(roundedPercentage);
});
return { resultsInPercent, maxIndices };
return results;
};

// Count the number of votes for each candidate and returns the counts and the
Expand Down
7 changes: 6 additions & 1 deletion web/frontend/src/types/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ type TextResults = Map<ID, string[][]>;

interface DownloadedResults {
Title: string;
Results?: { Candidate: string; Percentage: string }[];
Results?: {
Candidate: string;
Percent?: string;
TotalCount?: number;
NumberOfBallots?: number;
}[];
}
interface BallotResults {
BallotNumber: number;
Expand Down
91 changes: 57 additions & 34 deletions web/frontend/tests/ballot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,45 +60,61 @@ test('Assert ballot is displayed properly', async ({ page }) => {
// TODO integrate localisation
i18n.changeLanguage('en'); // force 'en' for this test
await expect(content.locator('xpath=./div/div[3]/h3')).toContainText(Form.Configuration.Title.En);
const scaffold = Form.Configuration.Scaffold.at(0);
await expect(content.locator('xpath=./div/div[3]/div/div/h3')).toContainText(scaffold.Title.En);
const select = scaffold.Selects.at(0);
await expect(
content.locator('xpath=./div/div[3]/div/div/div/div/div/div[1]/div/h3')
).toContainText(select.Title.En);
await expect(
page.getByText(i18n.t('selectBetween', { minSelect: select.MinN, maxSelect: select.MaxN }))
).toBeVisible();
for (const choice of select.Choices.map((x) => JSON.parse(x))) {
await expect(page.getByRole('checkbox', { name: choice.en })).toBeVisible();
for (const [index, scaffold] of Form.Configuration.Scaffold.entries()) {
await expect(content.locator(`xpath=./div/div[3]/div/div[${index + 1}]/h3`)).toContainText(
scaffold.Title.En
);
const select = scaffold.Selects.at(0);
await expect(
content.locator(`xpath=./div/div[3]/div/div[${index + 1}]/div/div/div/div[1]/div[1]/h3`)
).toContainText(select.Title.En);
await expect(
page.getByText(i18n.t('selectBetween', { minSelect: select.MinN, maxSelect: select.MaxN }))
).toBeVisible();
for (const choice of select.Choices.map((x) => JSON.parse(x))) {
await expect(page.getByRole('checkbox', { name: choice.en })).toBeVisible();
}
}
i18n.changeLanguage(); // unset language for the other tests
});

test('Assert minimum/maximum number of choices are handled correctly', async ({ page }) => {
const content = await page.getByTestId('content');
const castVoteButton = await page.getByRole('button', { name: i18n.t('castVote') });
const select = Form.Configuration.Scaffold.at(0).Selects.at(0);
await test.step(
`Assert minimum number of choices (${select.MinN}) are handled correctly`,
async () => {
await castVoteButton.click();
await expect(
page.getByText(
i18n.t('minSelectError', { min: select.MinN, singularPlural: i18n.t('singularAnswer') })
)
).toBeVisible();
}
);
await test.step(
`Assert maximum number of choices (${select.MaxN}) are handled correctly`,
async () => {
for (const choice of select.Choices.map((x) => JSON.parse(x))) {
await page.getByRole('checkbox', { name: choice.en }).setChecked(true);
for (const [index, scaffold] of Form.Configuration.Scaffold.entries()) {
const select = scaffold.Selects.at(0);
await test.step(
`Assert minimum number of choices (${select.MinN}) are handled correctly`,
async () => {
await castVoteButton.click();
await expect(
content.locator(`xpath=./div/div[3]/div/div[${index + 1}]`).getByText(
i18n.t('minSelectError', {
min: select.MinN,
singularPlural: i18n.t('singularAnswer'),
})
)
).toBeVisible();
}
await castVoteButton.click();
await expect(page.getByText(i18n.t('maxSelectError', { max: select.MaxN }))).toBeVisible();
}
);
);
await test.step(
`Assert maximum number of choices (${select.MaxN}) are handled correctly`,
async () => {
for (const choice of select.Choices.map((x) => JSON.parse(x))) {
await page.getByRole('checkbox', { name: choice.en }).setChecked(true);
}
await castVoteButton.click();
await expect(
content.locator(`xpath=./div/div[3]/div/div[${index + 1}]`).getByText(
i18n.t('maxSelectError', {
max: select.MaxN,
singularPlural: i18n.t('singularAnswer'),
})
)
).toBeVisible();
}
);
}
});

test('Assert that correct number of choices are accepted', async ({ page, baseURL }) => {
Expand All @@ -109,15 +125,22 @@ test('Assert that correct number of choices are accepted', async ({ page, baseUR
request.url() === `${baseURL}/api/evoting/forms/${FORMID}/vote` &&
request.method() === 'POST' &&
body.UserID === null &&
body.Ballot.length === 1 &&
body.Ballot.length === 2 &&
body.Ballot.at(0).K.length === 32 &&
body.Ballot.at(0).C.length === 32
body.Ballot.at(0).C.length === 32 &&
body.Ballot.at(1).K.length === 32 &&
body.Ballot.at(1).C.length === 32
);
});
await page
.getByRole('checkbox', {
name: JSON.parse(Form.Configuration.Scaffold.at(0).Selects.at(0).Choices.at(0)).en,
})
.setChecked(true);
await page
.getByRole('checkbox', {
name: JSON.parse(Form.Configuration.Scaffold.at(1).Selects.at(0).Choices.at(0)).en,
})
.setChecked(true);
await page.getByRole('button', { name: i18n.t('castVote') }).click();
});
53 changes: 46 additions & 7 deletions web/frontend/tests/json/evoting/forms/canceled.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"Configuration": {
"Title": {
"En": "Colours",
"Fr": "",
"De": ""
"Fr": "Couleurs",
"De": "Farben"
},
"Scaffold": [
{
Expand Down Expand Up @@ -42,6 +42,43 @@
],
"Ranks": [],
"Texts": []
},
{
"ID": "1NqhDffw",
"Title": {
"En": "Colours",
"Fr": "Couleurs",
"De": "Farben"
},
"Order": [
"riJFjw0q"
],
"Subjects": [],
"Selects": [
{
"ID": "riJFjw0q",
"Title": {
"En": "CMYK",
"Fr": "CMJN",
"De": "CMYK"
Copy link
Member

Choose a reason for hiding this comment

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

Joke: Wir haben keine Übersetzung dafür? Sowas... "CMGS"?

Copy link
Member Author

Choose a reason for hiding this comment

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

https://www.din.de/de/wdc-beuth:din21:5463877

jedenfalls nicht in Deutschland 🤷‍♀️

},
"MaxN": 3,
"MinN": 1,
"Choices": [
"{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}",
"{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}",
"{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}",
"{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}"
],
"Hint": {
"En": "",
"Fr": "",
"De": ""
}
}
],
"Ranks": [],
"Texts": []
}
]
},
Expand All @@ -54,11 +91,13 @@
"grpc://dela-worker-2:2000",
"grpc://dela-worker-3:2000"
],
"ChunksPerBallot": 1,
"BallotSize": 23,
"ChunksPerBallot": 2,
"BallotSize": 48,
"Voters": [
"oUItDdhhEE",
"WZyqP1gssL",
"K7ZNvumBVc"
"brcLwsgGcU",
"JThb56JvGF",
"zXcZU5QNwn",
"bWxTfeq4t5"
]
}

Loading
Loading