Skip to content

Commit

Permalink
Merge pull request #123 from c4dt/45
Browse files Browse the repository at this point in the history
Display total count instead of percent and order by count
  • Loading branch information
PascalinDe committed Feb 26, 2024
2 parents b5ad03e + 18be67e commit c819e31
Show file tree
Hide file tree
Showing 17 changed files with 634 additions and 188 deletions.
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) => {
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,
]);
});
}, 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"
},
"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

0 comments on commit c819e31

Please sign in to comment.