Fix projection of analog polls & prevent percent base 'entitled' for analog polls

This commit is contained in:
Joshua Sangmeister 2021-06-28 13:56:38 +02:00
parent 0322436cf5
commit 5d13f94e40
12 changed files with 83 additions and 40 deletions

View File

@ -108,7 +108,7 @@
"karma-jasmine": "~4.0.1", "karma-jasmine": "~4.0.1",
"karma-jasmine-html-reporter": "^1.4.0", "karma-jasmine-html-reporter": "^1.4.0",
"npm-license-crawler": "^0.2.1", "npm-license-crawler": "^0.2.1",
"prettier": "^2.1.1", "prettier": "~2.1.1",
"protractor": "^7.0.0", "protractor": "^7.0.0",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"ts-node": "~9.0.0", "ts-node": "~9.0.0",

View File

@ -43,7 +43,7 @@
</table> </table>
<!-- Chart --> <!-- Chart -->
<div *ngIf="!poll.hasSpecialVoteValues" class="doughnut-chart"> <div *ngIf="showChart()" class="doughnut-chart">
<os-charts type="doughnut" [data]="chartData"></os-charts> <os-charts type="doughnut" [data]="chartData"></os-charts>
</div> </div>
</ng-container> </ng-container>

View File

@ -72,6 +72,10 @@ export class MotionPollDetailContentComponent extends BaseComponent {
super(titleService, translate); super(titleService, translate);
} }
public showChart(): boolean {
return this.pollService.showChart(this.poll);
}
private setTableData(): void { private setTableData(): void {
this.tableData = this.pollService.generateTableData(this.poll); this.tableData = this.pollService.generateTableData(this.poll);
} }

View File

@ -109,7 +109,11 @@ export class MotionPollService extends PollService {
} }
public showChart(poll: PollData): boolean { public showChart(poll: PollData): boolean {
return poll && poll.options && poll.options.some(option => option.yes >= 0 && option.no >= 0); return (
poll &&
poll.options &&
poll.options.some(option => option.yes >= 0 && option.no >= 0 && option.abstain >= 0)
);
} }
public getPercentBase(poll: PollData): number { public getPercentBase(poll: PollData): number {

View File

@ -273,7 +273,9 @@ export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S
public ngOnDestroy(): void { public ngOnDestroy(): void {
super.ngOnDestroy(); super.ngOnDestroy();
this.entitledUsersSubscription.unsubscribe(); if (this.entitledUsersSubscription) {
this.entitledUsersSubscription = null; this.entitledUsersSubscription.unsubscribe();
this.entitledUsersSubscription = null;
}
} }
} }

View File

@ -172,7 +172,7 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
this.patchForm(this.contentForm); this.patchForm(this.contentForm);
} }
this.updatePollValues(this.contentForm.value); this.updatePollValues(this.contentForm.value);
this.updatePercentBases(this.pollMethodControl.value); this.updatePercentBases(this.pollMethodControl.value, this.pollTypeControl.value);
this.subscriptions.push( this.subscriptions.push(
// changes to whole form // changes to whole form
@ -183,13 +183,12 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
}), }),
// poll method changes // poll method changes
this.pollMethodControl.valueChanges.subscribe((method: AssignmentPollMethod) => { this.pollMethodControl.valueChanges.subscribe((method: AssignmentPollMethod) => {
if (method) { this.updatePercentBases(method, this.pollTypeControl.value);
this.updatePercentBases(method); this.setWarning();
this.setWarning();
}
}), }),
// poll type changes // poll type changes
this.pollTypeControl.valueChanges.subscribe(() => { this.pollTypeControl.valueChanges.subscribe((type: PollType) => {
this.updatePercentBases(this.pollMethodControl.value, type);
this.setWarning(); this.setWarning();
}) })
); );
@ -226,11 +225,12 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
} }
/** /**
* updates the available percent bases according to the pollmethod * updates the available percent bases according to the pollmethod and type
* @param method the currently chosen pollmethod * @param method the currently chosen pollmethod
* @param type the currently chosen type
*/ */
private updatePercentBases(method: AssignmentPollMethod): void { private updatePercentBases(method: AssignmentPollMethod, type: PollType): void {
if (method) { if (method || type) {
let forbiddenBases = []; let forbiddenBases = [];
if (method === AssignmentPollMethod.YN) { if (method === AssignmentPollMethod.YN) {
forbiddenBases = [PercentBase.YNA, AssignmentPollPercentBase.Y]; forbiddenBases = [PercentBase.YNA, AssignmentPollPercentBase.Y];
@ -239,6 +239,9 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
} else if (method === AssignmentPollMethod.Y || AssignmentPollMethod.N) { } else if (method === AssignmentPollMethod.Y || AssignmentPollMethod.N) {
forbiddenBases = [PercentBase.YN, PercentBase.YNA]; forbiddenBases = [PercentBase.YN, PercentBase.YNA];
} }
if (type === PollType.Analog) {
forbiddenBases.push(PercentBase.Entitled);
}
const bases = {}; const bases = {};
for (const [key, value] of Object.entries(this.percentBases)) { for (const [key, value] of Object.entries(this.percentBases)) {
@ -248,7 +251,7 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
} }
// update value in case that its no longer valid // update value in case that its no longer valid
const percentBaseControl = this.contentForm.get('onehundred_percent_base'); const percentBaseControl = this.contentForm.get('onehundred_percent_base');
percentBaseControl.setValue(this.getNormedPercentBase(percentBaseControl.value, method)); percentBaseControl.setValue(this.getNormedPercentBase(percentBaseControl.value, method, type));
this.validPercentBases = bases; this.validPercentBases = bases;
} }
@ -256,7 +259,8 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
private getNormedPercentBase( private getNormedPercentBase(
base: AssignmentPollPercentBase, base: AssignmentPollPercentBase,
method: AssignmentPollMethod method: AssignmentPollMethod,
type: PollType
): AssignmentPollPercentBase { ): AssignmentPollPercentBase {
if ( if (
method === AssignmentPollMethod.YN && method === AssignmentPollMethod.YN &&
@ -270,6 +274,8 @@ export class PollFormComponent<T extends ViewBasePoll, S extends PollService>
(base === AssignmentPollPercentBase.YN || base === AssignmentPollPercentBase.YNA) (base === AssignmentPollPercentBase.YN || base === AssignmentPollPercentBase.YNA)
) { ) {
return AssignmentPollPercentBase.Y; return AssignmentPollPercentBase.Y;
} else if (type === PollType.Analog && base === AssignmentPollPercentBase.Entitled) {
return AssignmentPollPercentBase.Cast;
} }
return base; return base;
} }

View File

@ -112,10 +112,6 @@ export abstract class ViewBasePoll<
} }
} }
public get hasSpecialVoteValues(): boolean {
return this.poll.isAnalog && this.options.some(option => option.votes.some(vote => vote.weight < 0));
}
public abstract get pollmethodVerbose(): string; public abstract get pollmethodVerbose(): string;
public abstract get percentBaseVerbose(): string; public abstract get percentBaseVerbose(): string;

View File

@ -37,10 +37,6 @@ export class MotionPollSlideComponent extends BasePollSlideComponentDirective<Mo
return MotionPoll.DECIMAL_FIELDS; return MotionPoll.DECIMAL_FIELDS;
} }
public showChart(): boolean {
return this.pollService.showChart(this.pollData);
}
public getTableData(): PollTableData[] { public getTableData(): PollTableData[] {
return this.pollService.generateTableData(this.pollData); return this.pollService.generateTableData(this.pollData);
} }

View File

@ -110,8 +110,8 @@ class AssignmentPollSerializer(BasePollSerializer):
validated_data.pop("assignment", None) validated_data.pop("assignment", None)
return super().update(instance, validated_data) return super().update(instance, validated_data)
def norm_100_percent_base_to_pollmethod( def norm_100_percent_base(
self, onehundred_percent_base, pollmethod, old_100_percent_base=None self, onehundred_percent_base, pollmethod, polltype, old_100_percent_base=None
): ):
""" """
Returns None, if the 100-%-base must not be changed, otherwise the correct 100-%-base. Returns None, if the 100-%-base must not be changed, otherwise the correct 100-%-base.
@ -140,7 +140,9 @@ class AssignmentPollSerializer(BasePollSerializer):
AssignmentPoll.PERCENT_BASE_YNA, AssignmentPoll.PERCENT_BASE_YNA,
): ):
return AssignmentPoll.PERCENT_BASE_Y return AssignmentPoll.PERCENT_BASE_Y
return None return super().norm_100_percent_base(
onehundred_percent_base, pollmethod, polltype, old_100_percent_base
)
class AssignmentSerializer(ModelSerializer): class AssignmentSerializer(ModelSerializer):

View File

@ -260,15 +260,17 @@ class MotionPollSerializer(BasePollSerializer):
validated_data.pop("motion", None) validated_data.pop("motion", None)
return super().update(instance, validated_data) return super().update(instance, validated_data)
def norm_100_percent_base_to_pollmethod( def norm_100_percent_base(
self, onehundred_percent_base, pollmethod, old_100_percent_base=None self, onehundred_percent_base, pollmethod, polltype, old_100_percent_base=None
): ):
if ( if (
pollmethod == MotionPoll.POLLMETHOD_YN pollmethod == MotionPoll.POLLMETHOD_YN
and onehundred_percent_base == MotionPoll.PERCENT_BASE_YNA and onehundred_percent_base == MotionPoll.PERCENT_BASE_YNA
): ):
return MotionPoll.PERCENT_BASE_YN return MotionPoll.PERCENT_BASE_YN
return None return super().norm_100_percent_base(
onehundred_percent_base, pollmethod, polltype, old_100_percent_base
)
class MotionChangeRecommendationSerializer(ModelSerializer): class MotionChangeRecommendationSerializer(ModelSerializer):

View File

@ -77,12 +77,14 @@ class BasePollSerializer(ModelSerializer):
def create(self, validated_data): def create(self, validated_data):
""" """
Match the 100 percent base to the pollmethod. Change the base, if it does not Match the 100 percent base to the pollmethod and type. Change the base, if it does not
fit to the pollmethod. fit to the pollmethod or type.
Set is_pseudoanonymized if type is pseudoanonymous. Set is_pseudoanonymized if type is pseudoanonymous.
""" """
new_100_percent_base = self.norm_100_percent_base_to_pollmethod( new_100_percent_base = self.norm_100_percent_base(
validated_data["onehundred_percent_base"], validated_data["pollmethod"] validated_data["onehundred_percent_base"],
validated_data["pollmethod"],
validated_data["type"],
) )
if new_100_percent_base is not None: if new_100_percent_base is not None:
validated_data["onehundred_percent_base"] = new_100_percent_base validated_data["onehundred_percent_base"] = new_100_percent_base
@ -92,7 +94,7 @@ class BasePollSerializer(ModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
""" """
Adjusts the 100%-base to the pollmethod. This might be needed, Adjusts the 100%-base to the pollmethod and type. This might be needed,
if at least one of them was changed. Wrong combinations should be if at least one of them was changed. Wrong combinations should be
also handled by the client, but here we make it sure aswell! also handled by the client, but here we make it sure aswell!
@ -109,8 +111,11 @@ class BasePollSerializer(ModelSerializer):
validated_data["is_pseudoanonymized"] = False validated_data["is_pseudoanonymized"] = False
instance = super().update(instance, validated_data) instance = super().update(instance, validated_data)
new_100_percent_base = self.norm_100_percent_base_to_pollmethod( new_100_percent_base = self.norm_100_percent_base(
instance.onehundred_percent_base, instance.pollmethod, old_100_percent_base instance.onehundred_percent_base,
instance.pollmethod,
instance.type,
old_100_percent_base,
) )
if new_100_percent_base is not None: if new_100_percent_base is not None:
instance.onehundred_percent_base = new_100_percent_base instance.onehundred_percent_base = new_100_percent_base
@ -136,7 +141,17 @@ class BasePollSerializer(ModelSerializer):
) )
return data return data
def norm_100_percent_base_to_pollmethod( def norm_100_percent_base(
self, onehundred_percent_base, pollmethod, old_100_percent_base=None self, onehundred_percent_base, pollmethod, polltype, old_100_percent_base=None
): ):
raise NotImplementedError() if (
polltype == BasePoll.TYPE_ANALOG
and onehundred_percent_base == BasePoll.PERCENT_BASE_ENTITLED
):
if (
old_100_percent_base
and old_100_percent_base != BasePoll.PERCENT_BASE_ENTITLED
):
return old_100_percent_base
return BasePoll.PERCENT_BASE_CAST
return None

View File

@ -322,6 +322,22 @@ class CreateMotionPoll(TestCase):
self.assertFalse(MotionPoll.objects.exists()) self.assertFalse(MotionPoll.objects.exists())
self.assertFalse(MotionVote.objects.exists()) self.assertFalse(MotionVote.objects.exists())
def test_create_with_invalid_percent_base(self):
response = self.client.post(
reverse("motionpoll-list"),
{
"title": "test_title_PgvqRIvuKuVImEpQJAMZ",
"pollmethod": MotionPoll.POLLMETHOD_YN,
"type": MotionPoll.TYPE_ANALOG,
"motion_id": self.motion.id,
"onehundred_percent_base": MotionPoll.PERCENT_BASE_ENTITLED,
"majority_method": MotionPoll.MAJORITY_SIMPLE,
},
)
self.assertHttpStatusVerbose(response, status.HTTP_201_CREATED)
poll = MotionPoll.objects.get()
self.assertEqual(poll.onehundred_percent_base, MotionPoll.PERCENT_BASE_CAST)
class UpdateMotionPoll(TestCase): class UpdateMotionPoll(TestCase):
""" """