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

Added vote checks and modified approval election #352

Merged
merged 5 commits into from
Jan 24, 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
3 changes: 2 additions & 1 deletion src/services/election.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,9 @@ export class ElectionService extends Service implements ElectionServicePropertie
try {
switch (electionType) {
case ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION:
case ElectionResultsTypeNames.APPROVAL:
return result ? result[qIndex][cIndex] : null;
case ElectionResultsTypeNames.APPROVAL:
return result ? result[cIndex][1] : null;
case ElectionResultsTypeNames.MULTIPLE_CHOICE:
return result
.reduce((prev, cur) => {
Expand Down
31 changes: 26 additions & 5 deletions src/types/election/approval.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { MultiLanguage } from '../../util/lang';
import { IElectionParameters } from './election';
import { IElectionParameters, IVoteType } from './election';
import { UnpublishedElection } from './unpublished';
import { ElectionMetadata, ElectionMetadataTemplate, ElectionResultsTypeNames } from '../metadata';
import { Vote } from '../vote';

export interface IApprovalElectionParameters extends IElectionParameters {}

Expand All @@ -22,19 +23,27 @@ export class ApprovalElection extends UnpublishedElection {
return new ApprovalElection(params);
}

public addQuestion(title: string | MultiLanguage<string>, description: string | MultiLanguage<string>) {
public addQuestion(
title: string | MultiLanguage<string>,
description: string | MultiLanguage<string>,
choices: Array<{ title: string } | { title: MultiLanguage<string> }>
) {
if (this.questions.length > 0) {
throw new Error('This type of election can only have one question');
}

return super.addQuestion(
title,
description,
[...new Array(2)].map((_v, index) => ({
title: '',
choices.map((choice, index) => ({
title: typeof choice.title === 'string' ? { default: choice.title } : choice.title,
value: index,
}))
);
}

public generateVoteOptions(): object {
const maxCount = this.questions.length;
const maxCount = this.questions[0].choices.length;
const maxValue = 1;
const maxVoteOverwrites = this.voteType.maxVoteOverwrites;
const maxTotalCost = 0;
Expand Down Expand Up @@ -66,4 +75,16 @@ export class ApprovalElection extends UnpublishedElection {

return super.generateMetadata(metadata);
}

public static checkVote(vote: Vote, voteType: IVoteType): void {
if (voteType.maxCount != vote.votes.length) {
throw new Error('Invalid number of choices');
}

vote.votes.forEach((vote) => {
if (vote > voteType.maxValue) {
throw new Error('Invalid choice value');
}
});
}
}
25 changes: 23 additions & 2 deletions src/types/election/budget.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { MultiLanguage } from '../../util/lang';
import { IElectionParameters } from './election';
import { IElectionParameters, IVoteType } from './election';
import { UnpublishedElection } from './unpublished';
import { ElectionMetadata, ElectionMetadataTemplate, ElectionResultsTypeNames } from '../metadata';
import { ElectionMetadata, ElectionMetadataTemplate, ElectionResultsType, ElectionResultsTypeNames } from '../metadata';
import { Vote } from '../vote';

export interface IBudgetElectionParametersInfo extends IElectionParameters {
minStep?: number;
Expand Down Expand Up @@ -100,6 +101,26 @@ export class BudgetElection extends UnpublishedElection {
return super.generateMetadata(metadata);
}

public static checkVote(vote: Vote, resultsType: ElectionResultsType, voteType: IVoteType): void {
if (resultsType.name != ElectionResultsTypeNames.BUDGET) {
throw new Error('Invalid results type');
}

if (voteType.maxCount != vote.votes.length) {
throw new Error('Invalid number of choices');
}

if (!voteType.costFromWeight) {
const voteWeight = vote.votes.reduce((a, b) => BigInt(b) + BigInt(a), 0);
if (voteType.maxTotalCost < voteWeight) {
throw new Error('Too much budget spent');
}
if (resultsType.properties.forceFullBudget && voteType.maxTotalCost != voteWeight) {
throw new Error('Not full budget used');
}
}
}

get minStep(): number {
return this._minStep;
}
Expand Down
19 changes: 18 additions & 1 deletion src/types/election/multichoice.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { MultiLanguage } from '../../util/lang';
import { IElectionParameters } from './election';
import { IElectionParameters, IVoteType } from './election';
import { UnpublishedElection } from './unpublished';
import { ElectionMetadata, ElectionMetadataTemplate, ElectionResultsTypeNames } from '../metadata';
import { Vote } from '../vote';

export interface IMultiChoiceElectionParameters extends IElectionParameters {
maxNumberOfChoices: number;
Expand Down Expand Up @@ -88,6 +89,22 @@ export class MultiChoiceElection extends UnpublishedElection {
return super.generateMetadata(metadata);
}

public static checkVote(vote: Vote, voteType: IVoteType): void {
if (voteType.uniqueChoices && new Set(vote.votes).size !== vote.votes.length) {
throw new Error('Choices are not unique');
}

if (voteType.maxCount != vote.votes.length) {
throw new Error('Invalid number of choices');
}

vote.votes.forEach((vote) => {
if (vote > voteType.maxValue) {
throw new Error('Invalid choice value');
}
});
}

get maxNumberOfChoices(): number {
return this.voteType.maxCount;
}
Expand Down
36 changes: 35 additions & 1 deletion src/types/election/published.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Election, IElectionParameters, IElectionType, IVoteType } from './election';
import { MultiLanguage } from '../../util/lang';
import { ElectionResultsType, IQuestion } from '../metadata';
import { ElectionResultsType, ElectionResultsTypeNames, IQuestion } from '../metadata';
import { PublishedCensus } from '../census';
import { Vote } from '../vote';
import { MultiChoiceElection } from './multichoice';
import { BudgetElection } from './budget';
import { ApprovalElection } from './approval';

export enum ElectionStatus {
PROCESS_UNKNOWN = 'PROCESS_UNKNOWN',
Expand Down Expand Up @@ -106,6 +110,36 @@ export class PublishedElection extends Election {
}
}

public checkVote(vote: Vote): void {
switch (this.resultsType?.name) {
case ElectionResultsTypeNames.MULTIPLE_CHOICE:
return MultiChoiceElection.checkVote(vote, this.voteType);
case ElectionResultsTypeNames.APPROVAL:
return ApprovalElection.checkVote(vote, this.voteType);
case ElectionResultsTypeNames.BUDGET:
return BudgetElection.checkVote(vote, this.resultsType, this.voteType);
case ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION:
default:
return PublishedElection.checkVote(vote, this.voteType);
}
}

public static checkVote(vote: Vote, voteType: IVoteType): void {
if (voteType.uniqueChoices && new Set(vote.votes).size !== vote.votes.length) {
throw new Error('Choices are not unique');
}

if (voteType.maxCount < vote.votes.length) {
throw new Error('Invalid number of choices');
}

vote.votes.forEach((vote) => {
if (vote > voteType.maxValue) {
throw new Error('Invalid choice value');
}
});
}

get title(): MultiLanguage<string> {
return super.title;
}
Expand Down
67 changes: 57 additions & 10 deletions test/integration/election.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,14 @@ describe('Election integration tests', () => {
expect(election.census.weight).toEqual(BigInt(numVotes));
expect(election.status).toEqual(ElectionStatus.ONGOING);
expect(election.maxCensusSize).toEqual(numVotes);
expect(election.checkVote(new Vote([0]))).toBeUndefined();
expect(election.checkVote(new Vote([1]))).toBeUndefined();
expect(() => {
election.checkVote(new Vote([0, 1, 1]));
}).toThrow('Invalid number of choices');
expect(() => {
election.checkVote(new Vote([2]));
}).toThrow('Invalid choice value');
})
.then(() =>
Promise.all(
Expand Down Expand Up @@ -651,6 +659,16 @@ describe('Election integration tests', () => {
['0', '0', '0', '0', '0', '0', '0', '5'],
]);
expect(election.questions[0].numAbstains).toEqual('10');
expect(election.checkVote(new Vote([0, 5, 7]))).toBeUndefined();
expect(() => {
election.checkVote(new Vote([5, 5, 7]));
}).toThrow('Choices are not unique');
expect(() => {
election.checkVote(new Vote([0, 1, 5, 7]));
}).toThrow('Invalid number of choices');
expect(() => {
election.checkVote(new Vote([0, 15, 7]));
}).toThrow('Invalid choice value');
});
}, 850000);
it('should create a budget election without weights and have the correct values set', async () => {
Expand All @@ -665,6 +683,7 @@ describe('Election integration tests', () => {
census,
useCensusWeightAsBudget: false,
maxBudget: 20,
forceFullBudget: true,
});

election.addQuestion('This is a title', 'This is a description', [
Expand Down Expand Up @@ -697,7 +716,7 @@ describe('Election integration tests', () => {
participants.map(async (participant) => {
const pClient = new VocdoniSDKClient(clientParams(participant));
pClient.setElectionId(electionId);
const vote = new Vote([15, 3, 1, 0, 0]);
const vote = new Vote([15, 4, 1, 0, 0]);
return pClient.submitVote(vote);
})
)
Expand All @@ -712,10 +731,20 @@ describe('Election integration tests', () => {
expect(election.resultsType.properties).toStrictEqual({
useCensusWeightAsBudget: false,
maxBudget: 20,
forceFullBudget: false,
forceFullBudget: true,
minStep: 1,
});
expect(election.results).toStrictEqual([['75'], ['15'], ['5'], ['0'], ['0']]);
expect(election.results).toStrictEqual([['75'], ['20'], ['5'], ['0'], ['0']]);
expect(election.checkVote(new Vote([15, 4, 1, 0, 0]))).toBeUndefined();
expect(() => {
election.checkVote(new Vote([15, 3, 1, 0]));
}).toThrow('Invalid number of choices');
expect(() => {
election.checkVote(new Vote([18, 3, 1, 0, 0]));
}).toThrow('Too much budget spent');
expect(() => {
election.checkVote(new Vote([15, 3, 1, 0, 0]));
}).toThrow('Not full budget used');
});
}, 850000);
it('should create a budget election with weights and have the correct values set', async () => {
Expand Down Expand Up @@ -785,6 +814,10 @@ describe('Election integration tests', () => {
minStep: 1,
});
expect(election.results).toStrictEqual([['10'], ['0'], ['5'], ['0'], ['0']]);
expect(election.checkVote(new Vote([15, 4, 1, 0, 0]))).toBeUndefined();
expect(() => {
election.checkVote(new Vote([15, 3, 1, 0]));
}).toThrow('Invalid number of choices');
});
}, 850000);
it('should create an approval election and have the correct values set', async () => {
Expand All @@ -799,9 +832,17 @@ describe('Election integration tests', () => {
census,
});

election.addQuestion('Statement 1', 'This is a description');
election.addQuestion('Statement 2', 'This is a description');
election.addQuestion('Statement 3', 'This is a description');
election.addQuestion('Approve each point', 'This is a description', [
{
title: 'Statement 1',
},
{
title: 'Statement 2',
},
{
title: 'Statement 3',
},
]);

await client.createAccount();
await client
Expand All @@ -812,10 +853,10 @@ describe('Election integration tests', () => {
})
.then((electionId) =>
Promise.all(
participants.map(async (participant) => {
participants.map(async (participant, index) => {
const pClient = new VocdoniSDKClient(clientParams(participant));
pClient.setElectionId(electionId);
const vote = new Vote([1, 0, 1]);
const vote = new Vote([1, 0, index % 2]);
return pClient.submitVote(vote);
})
)
Expand All @@ -834,9 +875,15 @@ describe('Election integration tests', () => {
expect(election.results).toStrictEqual([
['0', '5'],
['5', '0'],
['0', '5'],
['3', '2'],
]);
console.log(election.questions);
expect(election.checkVote(new Vote([1, 0, 1]))).toBeUndefined();
expect(() => {
election.checkVote(new Vote([0, 1]));
}).toThrow('Invalid number of choices');
expect(() => {
election.checkVote(new Vote([0, 2, 1]));
}).toThrow('Invalid choice value');
});
}, 850000);
it('should create a quadratic election with 10 participants and the results should be correct', async () => {
Expand Down
Loading