Skip to content

Commit

Permalink
[TECH-3124] Add max choosable option (#290)
Browse files Browse the repository at this point in the history
* Add max choosable option

Co-authored-by: Alexis Toledo <[email protected]>

---------

Co-authored-by: Alexis Toledo <[email protected]>
  • Loading branch information
av-mads and av-alexistoledo authored Mar 15, 2024
1 parent fe75b67 commit e03497c
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 12 deletions.
6 changes: 6 additions & 0 deletions lib/av_client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,11 @@ export interface ContestContent {
attachments?: Attachment[]
}

export interface Error {
message: string,
keys?: any
}

export interface ResultType {
name: string
}
Expand Down Expand Up @@ -434,6 +439,7 @@ export interface OptionContent {
maxSize: number
encoding: 'utf8'
}
maxChooseableSuboptions?: number
}

export interface ParentOption {
Expand Down
8 changes: 4 additions & 4 deletions lib/validators/contestSelectionValidator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ContestContent, ContestSelection } from '../av_client/types';
import { ContestContent, ContestSelection, Error } from '../av_client/types';
import SelectionPileValidator from './selectionPileValidator';

export default class ContestSelectionValidator {
Expand All @@ -14,16 +14,16 @@ export default class ContestSelectionValidator {
return this.allWeightUsed(contestSelection) && this.validate(contestSelection).length == 0;
}

validate(contestSelection: ContestSelection): string[] {
let errors: string[] = [];
validate(contestSelection: ContestSelection): Error[] {
let errors: Error[] = [];
const selectionPileValidator = new SelectionPileValidator(this.contest);

contestSelection.piles.forEach((pile) => {
errors = [...errors, ...selectionPileValidator.validate(pile)];
});

contestSelection.piles.forEach((pile) => {
if (!selectionPileValidator.isComplete(pile)) errors.push('A selection is not complete');
if (!selectionPileValidator.isComplete(pile)) errors.push({ message: 'A selection is not complete' });
});

return errors;
Expand Down
48 changes: 41 additions & 7 deletions lib/validators/selectionPileValidator.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { ContestContent, OptionSelection, OptionContent, SelectionPile } from '../av_client/types';
import { ContestContent, OptionSelection, OptionContent, SelectionPile, Error } from '../av_client/types';

class SelectionPileValidator {
private contest: ContestContent;
constructor(contest: ContestContent) {
this.contest = contest;
}

validate(selectionPile: SelectionPile): string[] {
const errors: string[] = [];
validate(selectionPile: SelectionPile): Error[] {
const errors: Error[] = [];

if (this.referenceMissing(selectionPile.optionSelections)) errors.push('invalid_reference');
if (this.tooManySelections(selectionPile.optionSelections)) errors.push('too_many');
if (this.blankNotAlone(selectionPile.optionSelections, selectionPile.explicitBlank)) errors.push('blank');
if (this.exclusiveNotAlone(selectionPile.optionSelections)) errors.push('exclusive');
if (this.referenceMissing(selectionPile.optionSelections)) errors.push({ message: 'invalid_reference'});
if (this.tooManySelections(selectionPile.optionSelections)) errors.push({ message: 'too_many'});
if (this.blankNotAlone(selectionPile.optionSelections, selectionPile.explicitBlank)) errors.push({message: 'blank'});
if (this.exclusiveNotAlone(selectionPile.optionSelections)) errors.push({ message: 'exclusive' });

errors.push(...this.exceededListVotes(selectionPile.optionSelections))

return errors;
}
Expand Down Expand Up @@ -58,6 +60,38 @@ class SelectionPileValidator {
return choices.length > this.contest.markingType.maxMarks;
}

private exceededListVotes(choices: OptionSelection[]) {
const options = this.recursiveFlattener(this.contest.options as OptionContent[]);

const optionsWithListLimit = options.map((op) => op?.maxChooseableSuboptions ? op : null)

const errors: Error[] = []

optionsWithListLimit.forEach(op => {
if (op?.maxChooseableSuboptions && this.selectedChildren(choices, [op]) > op.maxChooseableSuboptions) {
errors.push({message: "exceeded_list_limit", keys: { list_name: op.title, max_list_marks: op.maxChooseableSuboptions }})
}
})

return errors
}

private selectedChildren(choices: OptionSelection[], options?: OptionContent[], count = 0): number {
if (!options) return count

options.forEach(op => {
const childrenSelected = op?.children?.filter(child => this.selectedReferences(choices).includes(child.reference))

count += childrenSelected?.length || 0

if (op.children) {
count = this.selectedChildren(choices, op.children, count)
}
})

return count
}

private exclusiveNotAlone(choices: OptionSelection[]) {
if (this.selectedReferences(choices).length < 2) return false;

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "3.2.0",
"version": "4.0.0",
"name": "@aion-dk/js-client",
"license": "MIT",
"description": "Assembly Voting JS client",
Expand Down
179 changes: 179 additions & 0 deletions test/selection_pile_validator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import SelectionPileValidator from "../lib/validators/selectionPileValidator";

import { expect } from "chai";
import {
ContestConfig,
} from "../lib/av_client/types";

const contestOne: ContestConfig = {
address: "",
author: "",
parentAddress: "",
previousAddress: "",
registeredAt: "",
signature: "",
type: "ContestConfigItem",
content: {
reference: "contest-1",
markingType: {
minMarks: 1,
maxMarks: 3,
blankSubmission: "disabled",
encoding: {
codeSize: 1,
maxSize: 1,
cryptogramCount: 1,
},
},
resultType: {
name: "does not matter right now",
},
title: { en: "Contest 1" },
subtitle: { en: "Contest 1" },
description: { en: "Contest 1" },
options: [
{
reference: "parent-1",
code: 1,
title: { en: "Parent 1" },
subtitle: { en: "Parent 1" },
description: { en: "Parent 1" },
maxChooseableSuboptions: 2,
children: [
{
reference: "child-1",
code: 11,
title: { en: "Child 1" },
subtitle: { en: "Child 1" },
description: { en: "Child 1" },
},
{
reference: "child-2",
code: 12,
title: { en: "Child 2" },
subtitle: { en: "Child 2" },
description: { en: "Child 2" },
},
{
reference: "child-3",
code: 13,
title: { en: "Child 3" },
subtitle: { en: "Child 3" },
description: { en: "Child 3" },
}
]
},
{
reference: "parent-2",
code: 3,
title: { en: "Parent 2" },
subtitle: { en: "Parent 2" },
description: { en: "Parent 2" },
exclusive: true,
},
{
reference: "parent-3",
code: 4,
title: { en: "Parent 3" },
subtitle: { en: "Parent 3" },
description: { en: "Parent 3" },
},
],
},
};

const optionSelections = [
{ reference: "parent-1" }
]

const selectionPile = {
multiplier: 1,
optionSelections: optionSelections,
explicitBlank: false
}

const validator = new SelectionPileValidator(contestOne.content)

describe("validate", () => {
context("when given a valid selectionPile", () => {
it("returns no errors", () => {
expect(validator.validate(selectionPile)).to.have.lengthOf(0)
});
});

context("when given too many selections", () => {
const optionSelections = [
{ reference: "parent-1" },
{ reference: "parent-3" },
{ reference: "child-2" },
{ reference: "child-3" }
]

const selectionPile = {
multiplier: 1,
optionSelections: optionSelections,
explicitBlank: false
}

it("returns 'too_many' error", () => {
expect(validator.validate(selectionPile)).to.have.lengthOf(1)
expect(validator.validate(selectionPile)[0].message).to.equal("too_many")
});
})

context("with invalid reference", () => {
const optionSelections = [{ reference: "invalid"}]
const selectionPile = {
multiplier: 1,
optionSelections: optionSelections,
explicitBlank: false
}

it("returns 'invalid_reference' error", () => {
expect(validator.validate(selectionPile)).to.have.lengthOf(1)
expect(validator.validate(selectionPile)[0].message).to.equal("invalid_reference")
});
})

context("with blank not exclusive reference", () => {
const optionSelections = [ { reference: "blank" }, { reference: "parent-1" }]
const selectionPile = {
multiplier: 1,
optionSelections: optionSelections,
explicitBlank: true
}

it("returns 'blank' error", () => {
expect(validator.validate(selectionPile)).to.have.lengthOf(1)
expect(validator.validate(selectionPile)[0].message).to.equal("blank")
});
})

context("with exclusive not exclusive reference", () => {
const optionSelections = [ { reference: "parent-1" }, { reference: "parent-2" }]
const selectionPile = {
multiplier: 1,
optionSelections: optionSelections,
explicitBlank: false
}

it("returns 'exclusive' error", () => {
expect(validator.validate(selectionPile)).to.have.lengthOf(1)
expect(validator.validate(selectionPile)[0].message).to.equal("exclusive")
});
})

context("with exceeded list limit", () => {
const optionSelections = [ { reference: "child-1" }, { reference: "child-2" }, { reference: "child-3" } ]
const selectionPile = {
multiplier: 1,
optionSelections: optionSelections,
explicitBlank: false
}

it("returns 'exceeded_list_limit' error", () => {
expect(validator.validate(selectionPile)).to.have.lengthOf(1)
expect(validator.validate(selectionPile)[0].message).to.equal("exceeded_list_limit")
});
})
});

0 comments on commit e03497c

Please sign in to comment.