table update on pseudoanonymize, view base classes for votes and

options, renaming for assignment percent bases
This commit is contained in:
Joshua Sangmeister 2020-02-25 10:44:39 +01:00 committed by FinnStutzenstein
parent 82c8ade0ba
commit 72678770bb
38 changed files with 298 additions and 202 deletions

View File

@ -2,24 +2,36 @@ import { CalculablePollKey } from 'app/site/polls/services/poll.service';
import { AssignmentOption } from './assignment-option'; import { AssignmentOption } from './assignment-option';
import { BasePoll } from '../poll/base-poll'; import { BasePoll } from '../poll/base-poll';
export enum AssignmentPollMethods { export enum AssignmentPollMethod {
YN = 'YN', YN = 'YN',
YNA = 'YNA', YNA = 'YNA',
Votes = 'votes' Votes = 'votes'
} }
export enum AssignmentPollPercentBase {
YN = 'YN',
YNA = 'YNA',
Votes = 'votes',
Valid = 'valid',
Cast = 'cast',
Disabled = 'disabled'
}
/** /**
* Content of the 'polls' property of assignments * Class representing a poll for an assignment.
* @ignore
*/ */
export class AssignmentPoll extends BasePoll<AssignmentPoll, AssignmentOption> { export class AssignmentPoll extends BasePoll<
AssignmentPoll,
AssignmentOption,
AssignmentPollMethod,
AssignmentPollPercentBase
> {
public static COLLECTIONSTRING = 'assignments/assignment-poll'; public static COLLECTIONSTRING = 'assignments/assignment-poll';
public static defaultGroupsConfig = 'assignment_poll_default_groups'; public static defaultGroupsConfig = 'assignment_poll_default_groups';
public static defaultPollMethodConfig = 'assignment_poll_method'; public static defaultPollMethodConfig = 'assignment_poll_method';
public id: number; public id: number;
public assignment_id: number; public assignment_id: number;
public pollmethod: AssignmentPollMethods;
public votes_amount: number; public votes_amount: number;
public allow_multiple_votes_per_candidate: boolean; public allow_multiple_votes_per_candidate: boolean;
public global_no: boolean; public global_no: boolean;
@ -27,11 +39,11 @@ export class AssignmentPoll extends BasePoll<AssignmentPoll, AssignmentOption> {
public description: string; public description: string;
public get pollmethodFields(): CalculablePollKey[] { public get pollmethodFields(): CalculablePollKey[] {
if (this.pollmethod === AssignmentPollMethods.YN) { if (this.pollmethod === AssignmentPollMethod.YN) {
return ['yes', 'no']; return ['yes', 'no'];
} else if (this.pollmethod === AssignmentPollMethods.YNA) { } else if (this.pollmethod === AssignmentPollMethod.YNA) {
return ['yes', 'no', 'abstain']; return ['yes', 'no', 'abstain'];
} else if (this.pollmethod === AssignmentPollMethods.Votes) { } else if (this.pollmethod === AssignmentPollMethod.Votes) {
return ['yes']; return ['yes'];
} }
} }

View File

@ -1,8 +1,8 @@
import { CalculablePollKey } from 'app/site/polls/services/poll.service'; import { CalculablePollKey } from 'app/site/polls/services/poll.service';
import { BasePoll } from '../poll/base-poll'; import { BasePoll, PercentBase } from '../poll/base-poll';
import { MotionOption } from './motion-option'; import { MotionOption } from './motion-option';
export enum MotionPollMethods { export enum MotionPollMethod {
YN = 'YN', YN = 'YN',
YNA = 'YNA' YNA = 'YNA'
} }
@ -10,19 +10,18 @@ export enum MotionPollMethods {
/** /**
* Class representing a poll for a motion. * Class representing a poll for a motion.
*/ */
export class MotionPoll extends BasePoll<MotionPoll, MotionOption> { export class MotionPoll extends BasePoll<MotionPoll, MotionOption, MotionPollMethod, PercentBase> {
public static COLLECTIONSTRING = 'motions/motion-poll'; public static COLLECTIONSTRING = 'motions/motion-poll';
public static defaultGroupsConfig = 'motion_poll_default_groups'; public static defaultGroupsConfig = 'motion_poll_default_groups';
public id: number; public id: number;
public motion_id: number; public motion_id: number;
public pollmethod: MotionPollMethods;
public get pollmethodFields(): CalculablePollKey[] { public get pollmethodFields(): CalculablePollKey[] {
const ynField: CalculablePollKey[] = ['yes', 'no']; const ynField: CalculablePollKey[] = ['yes', 'no'];
if (this.pollmethod === MotionPollMethods.YN) { if (this.pollmethod === MotionPollMethod.YN) {
return ynField; return ynField;
} else if (this.pollmethod === MotionPollMethods.YNA) { } else if (this.pollmethod === MotionPollMethod.YNA) {
return ynField.concat(['abstain']); return ynField.concat(['abstain']);
} }
} }

View File

@ -23,6 +23,13 @@ export enum PollType {
Pseudoanonymous = 'pseudoanonymous' Pseudoanonymous = 'pseudoanonymous'
} }
export enum MajorityMethod {
Simple = 'simple',
TwoThirds = 'two_thirds',
ThreeQuarters = 'three_quarters',
Disabled = 'disabled'
}
export enum PercentBase { export enum PercentBase {
YN = 'YN', YN = 'YN',
YNA = 'YNA', YNA = 'YNA',
@ -31,14 +38,12 @@ export enum PercentBase {
Disabled = 'disabled' Disabled = 'disabled'
} }
export enum MajorityMethod { export abstract class BasePoll<
Simple = 'simple', T = any,
TwoThirds = 'two_thirds', O extends BaseOption<any> = any,
ThreeQuarters = 'three_quarters', PM extends string = string,
Disabled = 'disabled' PB extends string = string
} > extends BaseDecimalModel<T> {
export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends BaseDecimalModel<T> {
public state: PollState; public state: PollState;
public type: PollType; public type: PollType;
public title: string; public title: string;
@ -47,7 +52,9 @@ export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends
public votescast: number; public votescast: number;
public groups_id: number[]; public groups_id: number[];
public majority_method: MajorityMethod; public majority_method: MajorityMethod;
public onehundred_percent_base: PercentBase;
public pollmethod: PM;
public onehundred_percent_base: PB;
public get isCreated(): boolean { public get isCreated(): boolean {
return this.state === PollState.Created; return this.state === PollState.Created;

View File

@ -16,7 +16,7 @@ export const GeneralValueVerbose = {
votesabstain: 'Votes abstain' votesabstain: 'Votes abstain'
}; };
export abstract class BaseVote<T> extends BaseDecimalModel<T> { export abstract class BaseVote<T = any> extends BaseDecimalModel<T> {
public weight: number; public weight: number;
public value: VoteValue; public value: VoteValue;
public option_id: number; public option_id: number;

View File

@ -103,7 +103,7 @@
<b *ngIf="!vote.user">{{ 'Anonymous' | translate }}</b> <b *ngIf="!vote.user">{{ 'Anonymous' | translate }}</b>
</div> </div>
<!-- Y/N/(A) --> <!-- Y/N/(A) -->
<ng-container *ngIf="poll.pollmethod !== AssignmentPollMethods.Votes"> <ng-container *ngIf="poll.pollmethod !== AssignmentPollMethod.Votes">
<ng-container *ngFor="let option of poll.options"> <ng-container *ngFor="let option of poll.options">
<div <div
*pblNgridCellDef="'votes-' + option.user_id; row as vote" *pblNgridCellDef="'votes-' + option.user_id; row as vote"
@ -116,7 +116,7 @@
</ng-container> </ng-container>
</ng-container> </ng-container>
<!-- Votes method --> <!-- Votes method -->
<ng-container *ngIf="poll.pollmethod === AssignmentPollMethods.Votes"> <ng-container *ngIf="poll.pollmethod === AssignmentPollMethod.Votes">
<div *pblNgridCellDef="'votes'; row as vote"> <div *pblNgridCellDef="'votes'; row as vote">
<div *ngFor="let candidate of vote.votes">{{ candidate }}</div> <div *ngFor="let candidate of vote.votes">{{ candidate }}</div>
</div> </div>

View File

@ -8,11 +8,12 @@ import { PblColumnDefinition } from '@pebula/ngrid';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service'; import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewportService } from 'app/core/ui-services/viewport.service'; import { ViewportService } from 'app/core/ui-services/viewport.service';
import { ChartType } from 'app/shared/components/charts/charts.component'; import { ChartType } from 'app/shared/components/charts/charts.component';
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll'; import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component'; import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
import { VotingResult } from 'app/site/polls/models/view-base-poll'; import { VotingResult } from 'app/site/polls/models/view-base-poll';
import { PollService } from 'app/site/polls/services/poll.service'; import { PollService } from 'app/site/polls/services/poll.service';
@ -26,7 +27,7 @@ import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewAssignmentPoll> { export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewAssignmentPoll> {
public AssignmentPollMethods = AssignmentPollMethods; public AssignmentPollMethod = AssignmentPollMethod;
public columnDefinitionSingleVotes: PblColumnDefinition[]; public columnDefinitionSingleVotes: PblColumnDefinition[];
@ -41,7 +42,7 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
} }
public get isVotedPoll(): boolean { public get isVotedPoll(): boolean {
return this.poll.pollmethod === AssignmentPollMethods.Votes; return this.poll.pollmethod === AssignmentPollMethod.Votes;
} }
private _chartType: ChartType = 'horizontalBar'; private _chartType: ChartType = 'horizontalBar';
@ -56,17 +57,18 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
prompt: PromptService, prompt: PromptService,
pollDialog: AssignmentPollDialogService, pollDialog: AssignmentPollDialogService,
pollService: PollService, pollService: PollService,
votesRepo: AssignmentVoteRepositoryService,
private operator: OperatorService, private operator: OperatorService,
private viewport: ViewportService private viewport: ViewportService
) { ) {
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService); super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService, votesRepo);
} }
public onPollWithOptionsLoaded(): void { protected createVotesData(): void {
const votes = {}; const votes = {};
let i = -1; let i = -1;
this.columnDefinitionSingleVotes = [ const definitions: PblColumnDefinition[] = [
{ {
prop: 'user', prop: 'user',
label: 'Participant', label: 'Participant',
@ -75,7 +77,7 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
} }
]; ];
if (this.isVotedPoll) { if (this.isVotedPoll) {
this.columnDefinitionSingleVotes.push(this.getVoteColumnDefinition('votes', 'Votes')); definitions.push(this.getVoteColumnDefinition('votes', 'Votes'));
} }
/** /**
@ -90,9 +92,7 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
*/ */
for (const option of this.poll.options) { for (const option of this.poll.options) {
if (!this.isVotedPoll) { if (!this.isVotedPoll) {
this.columnDefinitionSingleVotes.push( definitions.push(this.getVoteColumnDefinition('votes-' + option.user_id, option.user.getFullName()));
this.getVoteColumnDefinition('votes-' + option.user_id, option.user.getFullName())
);
} }
for (const vote of option.votes) { for (const vote of option.votes) {
@ -125,6 +125,7 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
this.setVotesData(Object.values(votes)); this.setVotesData(Object.values(votes));
this.candidatesLabels = this.pollService.getChartLabels(this.poll); this.candidatesLabels = this.pollService.getChartLabels(this.poll);
this.columnDefinitionSingleVotes = definitions;
this.isReady = true; this.isReady = true;
} }
@ -151,11 +152,11 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
} }
public voteFitsMethod(result: VotingResult): boolean { public voteFitsMethod(result: VotingResult): boolean {
if (this.poll.pollmethod === AssignmentPollMethods.Votes) { if (this.poll.pollmethod === AssignmentPollMethod.Votes) {
if (result.vote === 'abstain' || result.vote === 'no') { if (result.vote === 'abstain' || result.vote === 'no') {
return false; return false;
} }
} else if (this.poll.pollmethod === AssignmentPollMethods.YN) { } else if (this.poll.pollmethod === AssignmentPollMethod.YN) {
if (result.vote === 'abstain') { if (result.vote === 'abstain') {
return false; return false;
} }

View File

@ -1,4 +1,4 @@
<os-poll-form [data]="pollData" [pollMethods]="assignmentPollMethods" #pollForm></os-poll-form> <os-poll-form [data]="pollData" [pollMethods]="AssignmentPollMethodVerbose" [percentBases]="AssignmentPollPercentBaseVerbose" #pollForm></os-poll-form>
<!-- Analog voting --> <!-- Analog voting -->
<ng-container *ngIf="pollForm.contentForm.get('type').value === 'analog'"> <ng-container *ngIf="pollForm.contentForm.get('type').value === 'analog'">

View File

@ -6,9 +6,12 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll'; import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
import { GeneralValueVerbose, VoteValue, VoteValueVerbose } from 'app/shared/models/poll/base-vote'; import { GeneralValueVerbose, VoteValue, VoteValueVerbose } from 'app/shared/models/poll/base-vote';
import { AssignmentPollMethodsVerbose } from 'app/site/assignments/models/view-assignment-poll'; import {
AssignmentPollMethodVerbose,
AssignmentPollPercentBaseVerbose
} from 'app/site/assignments/models/view-assignment-poll';
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component'; import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component'; import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
@ -50,7 +53,8 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
public voteValueVerbose = VoteValueVerbose; public voteValueVerbose = VoteValueVerbose;
public generalValueVerbose = GeneralValueVerbose; public generalValueVerbose = GeneralValueVerbose;
public assignmentPollMethods = AssignmentPollMethodsVerbose; public AssignmentPollMethodVerbose = AssignmentPollMethodVerbose;
public AssignmentPollPercentBaseVerbose = AssignmentPollPercentBaseVerbose;
public options: OptionsObject; public options: OptionsObject;
@ -92,10 +96,10 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
private setAnalogPollValues(): void { private setAnalogPollValues(): void {
const pollmethod = this.pollForm.contentForm.get('pollmethod').value; const pollmethod = this.pollForm.contentForm.get('pollmethod').value;
this.analogPollValues = ['Y']; this.analogPollValues = ['Y'];
if (pollmethod !== AssignmentPollMethods.Votes) { if (pollmethod !== AssignmentPollMethod.Votes) {
this.analogPollValues.push('N'); this.analogPollValues.push('N');
} }
if (pollmethod === AssignmentPollMethods.YNA) { if (pollmethod === AssignmentPollMethod.YNA) {
this.analogPollValues.push('A'); this.analogPollValues.push('A');
} }
} }
@ -110,10 +114,10 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent<ViewA
for (const option of data.options) { for (const option of data.options) {
const votes: any = {}; const votes: any = {};
votes.Y = option.yes; votes.Y = option.yes;
if (data.pollmethod !== AssignmentPollMethods.Votes) { if (data.pollmethod !== AssignmentPollMethod.Votes) {
votes.N = option.no; votes.N = option.no;
} }
if (data.pollmethod === AssignmentPollMethods.YNA) { if (data.pollmethod === AssignmentPollMethod.YNA) {
votes.A = option.abstain; votes.A = option.abstain;
} }
update.options[option.user_id] = votes; update.options[option.user_id] = votes;

View File

@ -7,7 +7,7 @@
<span *ngIf="poll.user_has_not_voted">You have not give any voting here!</span> <span *ngIf="poll.user_has_not_voted">You have not give any voting here!</span>
<!-- Leftover votes --> <!-- Leftover votes -->
<h4 *ngIf="poll.pollmethod === pollMethods.Votes && poll.votes_amount > 1 && !currentVotes.global"> <h4 *ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1 && !currentVotes.global">
{{ 'Votes for this poll' | translate }}: {{ getVotesCount() }}/{{ poll.votes_amount }} {{ 'Votes for this poll' | translate }}: {{ getVotesCount() }}/{{ poll.votes_amount }}
</h4> </h4>
@ -16,9 +16,9 @@
<div *ngIf="poll.type !== PollType.Pseudoanonymous || !option.user_has_voted"> <div *ngIf="poll.type !== PollType.Pseudoanonymous || !option.user_has_voted">
<div <div
[ngClass]="{ [ngClass]="{
'yna-grid': poll.pollmethod === pollMethods.YNA, 'yna-grid': poll.pollmethod === AssignmentPollMethod.YNA,
'yn-grid': poll.pollmethod === pollMethods.YN, 'yn-grid': poll.pollmethod === AssignmentPollMethod.YN,
'single-vote-grid': poll.pollmethod === pollMethods.Votes 'single-vote-grid': poll.pollmethod === AssignmentPollMethod.Votes
}" }"
> >
<div class="vote-candidate-name"> <div class="vote-candidate-name">
@ -34,7 +34,7 @@
> >
<mat-icon> {{ action.icon }}</mat-icon> <mat-icon> {{ action.icon }}</mat-icon>
</button> </button>
<span *ngIf="poll.pollmethod !== pollMethods.Votes" class="vote-label"> <span *ngIf="poll.pollmethod !== AssignmentPollMethod.Votes" class="vote-label">
{{ action.label | translate }} {{ action.label | translate }}
</span> </span>
</div> </div>
@ -44,7 +44,7 @@
</div> </div>
<!-- global no/abstain --> <!-- global no/abstain -->
<ng-container *ngIf="poll.pollmethod === pollMethods.Votes && (poll.global_no || poll.global_abstain)"> <ng-container *ngIf="poll.pollmethod === AssignmentPollMethod.Votes && (poll.global_no || poll.global_abstain)">
<mat-divider></mat-divider> <mat-divider></mat-divider>
<div class="global-option-grid"> <div class="global-option-grid">
<div *ngIf="poll.global_no"> <div *ngIf="poll.global_no">

View File

@ -8,7 +8,7 @@ import { OperatorService } from 'app/core/core-services/operator.service';
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service'; import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service'; import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service';
import { VotingService } from 'app/core/ui-services/voting.service'; import { VotingService } from 'app/core/ui-services/voting.service';
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll'; import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
import { PollType } from 'app/shared/models/poll/base-poll'; import { PollType } from 'app/shared/models/poll/base-poll';
import { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component'; import { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component';
import { ViewAssignmentPoll } from '../../models/view-assignment-poll'; import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
@ -28,7 +28,7 @@ interface VoteActions {
styleUrls: ['./assignment-poll-vote.component.scss'] styleUrls: ['./assignment-poll-vote.component.scss']
}) })
export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssignmentPoll> implements OnInit { export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssignmentPoll> implements OnInit {
public pollMethods = AssignmentPollMethods; public AssignmentPollMethod = AssignmentPollMethod;
public PollType = PollType; public PollType = PollType;
public voteActions: VoteActions[] = []; public voteActions: VoteActions[] = [];
@ -70,7 +70,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
label: 'Yes' label: 'Yes'
}); });
if (this.poll.pollmethod !== AssignmentPollMethods.Votes) { if (this.poll.pollmethod !== AssignmentPollMethod.Votes) {
this.voteActions.push({ this.voteActions.push({
vote: 'N', vote: 'N',
css: 'voted-no', css: 'voted-no',
@ -79,7 +79,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
}); });
} }
if (this.poll.pollmethod === AssignmentPollMethods.YNA) { if (this.poll.pollmethod === AssignmentPollMethod.YNA) {
this.voteActions.push({ this.voteActions.push({
vote: 'A', vote: 'A',
css: 'voted-abstain', css: 'voted-abstain',
@ -101,7 +101,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
for (const option of this.poll.options) { for (const option of this.poll.options) {
let curr_vote = filtered.find(vote => vote.option.id === option.id); let curr_vote = filtered.find(vote => vote.option.id === option.id);
if (this.poll.pollmethod === AssignmentPollMethods.Votes && curr_vote) { if (this.poll.pollmethod === AssignmentPollMethod.Votes && curr_vote) {
if (curr_vote.value !== 'Y') { if (curr_vote.value !== 'Y') {
this.currentVotes.global = curr_vote.valueVerbose; this.currentVotes.global = curr_vote.valueVerbose;
curr_vote = null; curr_vote = null;
@ -120,7 +120,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
public saveSingleVote(optionId: number, vote: 'Y' | 'N' | 'A'): void { public saveSingleVote(optionId: number, vote: 'Y' | 'N' | 'A'): void {
let requestData; let requestData;
if (this.poll.pollmethod === AssignmentPollMethods.Votes) { if (this.poll.pollmethod === AssignmentPollMethod.Votes) {
const pollOptionIds = this.getPollOptionIds(); const pollOptionIds = this.getPollOptionIds();
requestData = pollOptionIds.reduce((o, n) => { requestData = pollOptionIds.reduce((o, n) => {
if ((n === optionId && vote === 'Y') !== (this.currentVotes[n] === 'Yes')) { if ((n === optionId && vote === 'Y') !== (this.currentVotes[n] === 'Yes')) {

View File

@ -45,7 +45,7 @@
</div> </div>
</div> </div>
<div *ngIf="hasVotes"> <div *ngIf="canSeeVotes">
<os-charts <os-charts
[class]="chartType === 'doughnut' ? 'doughnut-chart' : 'bar-chart'" [class]="chartType === 'doughnut' ? 'doughnut-chart' : 'bar-chart'"
[type]="chartType" [type]="chartType"

View File

@ -10,7 +10,6 @@ import { OperatorService } from 'app/core/core-services/operator.service';
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service'; import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { ChartType } from 'app/shared/components/charts/charts.component'; import { ChartType } from 'app/shared/components/charts/charts.component';
import { PollState } from 'app/shared/models/poll/base-poll';
import { BasePollComponent } from 'app/site/polls/components/base-poll.component'; import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
import { PollService } from 'app/site/polls/services/poll.service'; import { PollService } from 'app/site/polls/services/poll.service';
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service'; import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
@ -61,8 +60,8 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
return this.operator.hasPerms('assignments.can_see'); return this.operator.hasPerms('assignments.can_see');
} }
public get hasVotes(): boolean { public get canSeeVotes(): boolean {
return (this.canManage && this.poll.state === PollState.Finished) || this.poll.state === PollState.Published; return (this.canManage && this.poll.isFinished) || this.poll.isPublished;
} }
/** /**

View File

@ -1,21 +1,12 @@
import { AssignmentOption } from 'app/shared/models/assignments/assignment-option'; import { AssignmentOption } from 'app/shared/models/assignments/assignment-option';
import { ViewBaseOption } from 'app/site/polls/models/view-base-option';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { BaseViewModel } from '../../base/base-view-model';
import { ViewAssignmentPoll } from './view-assignment-poll';
import { ViewAssignmentVote } from './view-assignment-vote';
export class ViewAssignmentOption extends BaseViewModel<AssignmentOption> { export class ViewAssignmentOption extends ViewBaseOption<AssignmentOption> {
public get option(): AssignmentOption {
return this._model;
}
public static COLLECTIONSTRING = AssignmentOption.COLLECTIONSTRING; public static COLLECTIONSTRING = AssignmentOption.COLLECTIONSTRING;
protected _collectionString = AssignmentOption.COLLECTIONSTRING; protected _collectionString = AssignmentOption.COLLECTIONSTRING;
} }
interface TIAssignmentOptionRelations { export interface ViewAssignmentOption extends AssignmentOption {
votes: ViewAssignmentVote[];
user: ViewUser; user: ViewUser;
poll: ViewAssignmentPoll;
} }
export interface ViewAssignmentOption extends AssignmentOption, TIAssignmentOptionRelations {}

View File

@ -1,10 +1,14 @@
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { ChartData } from 'app/shared/components/charts/charts.component'; import { ChartData } from 'app/shared/components/charts/charts.component';
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll'; import {
AssignmentPoll,
AssignmentPollMethod,
AssignmentPollPercentBase
} from 'app/shared/models/assignments/assignment-poll';
import { BaseViewModel } from 'app/site/base/base-view-model'; import { BaseViewModel } from 'app/site/base/base-view-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { PollTableData, ViewBasePoll, VotingResult } from 'app/site/polls/models/view-base-poll'; import { PollClassType, PollTableData, ViewBasePoll, VotingResult } from 'app/site/polls/models/view-base-poll';
import { ViewAssignment } from './view-assignment'; import { ViewAssignment } from './view-assignment';
import { ViewAssignmentOption } from './view-assignment-option'; import { ViewAssignmentOption } from './view-assignment-option';
@ -12,21 +16,35 @@ export interface AssignmentPollTitleInformation {
title: string; title: string;
} }
export const AssignmentPollMethodsVerbose = { export const AssignmentPollMethodVerbose = {
votes: 'Yes per candidate', votes: 'Yes per candidate',
YN: 'Yes/No per candidate', YN: 'Yes/No per candidate',
YNA: 'Yes/No/Abstain per candidate' YNA: 'Yes/No/Abstain per candidate'
}; };
export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements AssignmentPollTitleInformation { export const AssignmentPollPercentBaseVerbose = {
YN: 'Yes/No per candidate',
YNA: 'Yes/No/Abstain per candidate',
votes: 'Sum of votes inclusive global ones',
valid: 'All valid ballots',
cast: 'All casted ballots',
disabled: 'Disabled (no percents)'
};
export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll, AssignmentPollMethod, AssignmentPollPercentBase>
implements AssignmentPollTitleInformation {
public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING; public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING;
protected _collectionString = AssignmentPoll.COLLECTIONSTRING; protected _collectionString = AssignmentPoll.COLLECTIONSTRING;
public readonly tableChartData: Map<string, BehaviorSubject<ChartData>> = new Map(); public readonly tableChartData: Map<string, BehaviorSubject<ChartData>> = new Map();
public readonly pollClassType: 'assignment' | 'motion' = 'assignment'; public readonly pollClassType = PollClassType.Assignment;
public get pollmethodVerbose(): string { public get pollmethodVerbose(): string {
return AssignmentPollMethodsVerbose[this.pollmethod]; return AssignmentPollMethodVerbose[this.pollmethod];
}
public get percentBaseVerbose(): string {
return AssignmentPollPercentBaseVerbose[this.onehundred_percent_base];
} }
public getContentObject(): BaseViewModel { public getContentObject(): BaseViewModel {

View File

@ -1,19 +1,9 @@
import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote'; import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewBaseVote } from 'app/site/polls/models/view-base-vote';
import { BaseViewModel } from '../../base/base-view-model';
import { ViewAssignmentOption } from './view-assignment-option';
export class ViewAssignmentVote extends BaseViewModel<AssignmentVote> { export class ViewAssignmentVote extends ViewBaseVote<AssignmentVote> {
public get vote(): AssignmentVote {
return this._model;
}
public static COLLECTIONSTRING = AssignmentVote.COLLECTIONSTRING; public static COLLECTIONSTRING = AssignmentVote.COLLECTIONSTRING;
protected _collectionString = AssignmentVote.COLLECTIONSTRING; protected _collectionString = AssignmentVote.COLLECTIONSTRING;
} }
interface TIAssignmentVoteRelations { export interface ViewAssignmentVote extends AssignmentVote {}
user: ViewUser;
option: ViewAssignmentOption;
}
export interface ViewAssignmentVote extends AssignmentVote, TIAssignmentVoteRelations {}

View File

@ -7,7 +7,7 @@ import { PdfDocumentService } from 'app/core/pdf-services/pdf-document.service';
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service'; import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll'; import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
import { ViewAssignmentPoll } from '../models/view-assignment-poll'; import { ViewAssignmentPoll } from '../models/view-assignment-poll';
/** /**
@ -162,7 +162,7 @@ export class AssignmentPollPdfService extends PollPdfService {
return resultObject; return resultObject;
} }
private createYNBallotEntry(option: string, method: AssignmentPollMethods): object { private createYNBallotEntry(option: string, method: AssignmentPollMethod): object {
const choices = method === 'YNA' ? ['Yes', 'No', 'Abstain'] : ['Yes', 'No']; const choices = method === 'YNA' ? ['Yes', 'No', 'Abstain'] : ['Yes', 'No'];
const columnstack = choices.map(choice => { const columnstack = choices.map(choice => {
return { return {

View File

@ -5,8 +5,12 @@ import { TranslateService } from '@ngx-translate/core';
import { ConstantsService } from 'app/core/core-services/constants.service'; import { ConstantsService } from 'app/core/core-services/constants.service';
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service'; import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { AssignmentPoll, AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll'; import {
import { MajorityMethod, PercentBase } from 'app/shared/models/poll/base-poll'; AssignmentPoll,
AssignmentPollMethod,
AssignmentPollPercentBase
} from 'app/shared/models/assignments/assignment-poll';
import { MajorityMethod } from 'app/shared/models/poll/base-poll';
import { PollData, PollService } from 'app/site/polls/services/poll.service'; import { PollData, PollService } from 'app/site/polls/services/poll.service';
@Injectable({ @Injectable({
@ -16,7 +20,7 @@ export class AssignmentPollService extends PollService {
/** /**
* The default percentage base * The default percentage base
*/ */
public defaultPercentBase: PercentBase; public defaultPercentBase: AssignmentPollPercentBase;
/** /**
* The default majority method * The default majority method
@ -25,7 +29,7 @@ export class AssignmentPollService extends PollService {
public defaultGroupIds: number[]; public defaultGroupIds: number[];
public defaultPollMethod: AssignmentPollMethods; public defaultPollMethod: AssignmentPollMethod;
/** /**
* Constructor. Subscribes to the configuration values needed * Constructor. Subscribes to the configuration values needed
@ -39,14 +43,14 @@ export class AssignmentPollService extends PollService {
) { ) {
super(constants); super(constants);
config config
.get<PercentBase>('motion_poll_default_100_percent_base') .get<AssignmentPollPercentBase>('assignment_poll_default_100_percent_base')
.subscribe(base => (this.defaultPercentBase = base)); .subscribe(base => (this.defaultPercentBase = base));
config config
.get<MajorityMethod>('motion_poll_default_majority_method') .get<MajorityMethod>('assignment_poll_default_majority_method')
.subscribe(method => (this.defaultMajorityMethod = method)); .subscribe(method => (this.defaultMajorityMethod = method));
config.get<number[]>(AssignmentPoll.defaultGroupsConfig).subscribe(ids => (this.defaultGroupIds = ids)); config.get<number[]>(AssignmentPoll.defaultGroupsConfig).subscribe(ids => (this.defaultGroupIds = ids));
config config
.get<AssignmentPollMethods>(AssignmentPoll.defaultPollMethodConfig) .get<AssignmentPollMethod>(AssignmentPoll.defaultPollMethodConfig)
.subscribe(method => (this.defaultPollMethod = method)); .subscribe(method => (this.defaultPollMethod = method));
} }
@ -77,19 +81,22 @@ export class AssignmentPollService extends PollService {
} }
public getPercentBase(poll: PollData): number { public getPercentBase(poll: PollData): number {
const base: PercentBase = poll.onehundred_percent_base; const base: AssignmentPollPercentBase = poll.onehundred_percent_base as AssignmentPollPercentBase;
let totalByBase: number; let totalByBase: number;
switch (base) { switch (base) {
case PercentBase.YN: case AssignmentPollPercentBase.YN:
totalByBase = this.sumOptionsYN(poll); totalByBase = this.sumOptionsYN(poll);
break; break;
case PercentBase.YNA: case AssignmentPollPercentBase.YNA:
totalByBase = this.sumOptionsYNA(poll); totalByBase = this.sumOptionsYNA(poll);
break; break;
case PercentBase.Valid: case AssignmentPollPercentBase.Votes:
totalByBase = this.sumOptionsYNA(poll);
break;
case AssignmentPollPercentBase.Valid:
totalByBase = poll.votesvalid; totalByBase = poll.votesvalid;
break; break;
case PercentBase.Cast: case AssignmentPollPercentBase.Cast:
totalByBase = poll.votescast; totalByBase = poll.votescast;
break; break;
default: default:

View File

@ -1,19 +1,8 @@
import { MotionOption } from 'app/shared/models/motions/motion-option'; import { MotionOption } from 'app/shared/models/motions/motion-option';
import { BaseViewModel } from '../../base/base-view-model'; import { ViewBaseOption } from 'app/site/polls/models/view-base-option';
import { ViewMotionPoll } from './view-motion-poll';
import { ViewMotionVote } from './view-motion-vote';
export class ViewMotionOption extends BaseViewModel<MotionOption> { export class ViewMotionOption extends ViewBaseOption<MotionOption> {
public get option(): MotionOption {
return this._model;
}
public static COLLECTIONSTRING = MotionOption.COLLECTIONSTRING; public static COLLECTIONSTRING = MotionOption.COLLECTIONSTRING;
protected _collectionString = MotionOption.COLLECTIONSTRING; protected _collectionString = MotionOption.COLLECTIONSTRING;
} }
export interface ViewMotionOption extends MotionOption {}
interface TIMotionOptionRelations {
votes: ViewMotionVote[];
poll: ViewMotionPoll;
}
export interface ViewMotionOption extends MotionOption, TIMotionOptionRelations {}

View File

@ -1,31 +1,41 @@
import { MotionPoll } from 'app/shared/models/motions/motion-poll'; import { MotionPoll, MotionPollMethod } from 'app/shared/models/motions/motion-poll';
import { PercentBase } from 'app/shared/models/poll/base-poll';
import { BaseViewModel } from 'app/site/base/base-view-model'; import { BaseViewModel } from 'app/site/base/base-view-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option'; import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
import { PollTableData, ViewBasePoll, VotingResult } from 'app/site/polls/models/view-base-poll'; import { PollClassType, PollTableData, ViewBasePoll, VotingResult } from 'app/site/polls/models/view-base-poll';
import { ViewMotion } from './view-motion'; import { ViewMotion } from './view-motion';
export interface MotionPollTitleInformation { export interface MotionPollTitleInformation {
title: string; title: string;
} }
export const MotionPollMethodsVerbose = { export const MotionPollMethodVerbose = {
YN: 'Yes/No', YN: 'Yes/No',
YNA: 'Yes/No/Abstain' YNA: 'Yes/No/Abstain'
}; };
export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPollTitleInformation { export const MotionPollPercentBaseVerbose = {
YN: 'Yes/No',
YNA: 'Yes/No/Abstain',
valid: 'All valid ballots',
cast: 'All casted ballots',
disabled: 'Disabled (no percents)'
};
export class ViewMotionPoll extends ViewBasePoll<MotionPoll, MotionPollMethod, PercentBase>
implements MotionPollTitleInformation {
public static COLLECTIONSTRING = MotionPoll.COLLECTIONSTRING; public static COLLECTIONSTRING = MotionPoll.COLLECTIONSTRING;
protected _collectionString = MotionPoll.COLLECTIONSTRING; protected _collectionString = MotionPoll.COLLECTIONSTRING;
public readonly pollClassType: 'assignment' | 'motion' = 'motion'; public readonly pollClassType = PollClassType.Motion;
public get result(): ViewMotionOption { public get result(): ViewMotionOption {
return this.options[0]; return this.options[0];
} }
public get hasVotes(): boolean { public get hasVotes(): boolean {
return !!this.result.votes.length; return this.result && !!this.result.votes.length;
} }
public getContentObject(): BaseViewModel { public getContentObject(): BaseViewModel {
@ -71,7 +81,11 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
} }
public get pollmethodVerbose(): string { public get pollmethodVerbose(): string {
return MotionPollMethodsVerbose[this.pollmethod]; return MotionPollMethodVerbose[this.pollmethod];
}
public get percentBaseVerbose(): string {
return MotionPollPercentBaseVerbose[this.onehundred_percent_base];
} }
public anySpecialVotes(): boolean { public anySpecialVotes(): boolean {

View File

@ -1,19 +1,9 @@
import { MotionVote } from 'app/shared/models/motions/motion-vote'; import { MotionVote } from 'app/shared/models/motions/motion-vote';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewBaseVote } from 'app/site/polls/models/view-base-vote';
import { BaseViewModel } from '../../base/base-view-model';
import { ViewMotionOption } from './view-motion-option';
export class ViewMotionVote extends BaseViewModel<MotionVote> { export class ViewMotionVote extends ViewBaseVote<MotionVote> {
public get vote(): MotionVote {
return this._model;
}
public static COLLECTIONSTRING = MotionVote.COLLECTIONSTRING; public static COLLECTIONSTRING = MotionVote.COLLECTIONSTRING;
protected _collectionString = MotionVote.COLLECTIONSTRING; protected _collectionString = MotionVote.COLLECTIONSTRING;
} }
interface TIMotionVoteRelations { export interface ViewMotionVote extends MotionVote {}
user?: ViewUser;
option: ViewMotionOption;
}
export interface ViewMotionVote extends MotionVote, TIMotionVoteRelations {}

View File

@ -82,6 +82,7 @@
<div class="named-result-table" *ngIf="poll.type === 'named'"> <div class="named-result-table" *ngIf="poll.type === 'named'">
<h2>{{ 'Single votes' | translate }}</h2> <h2>{{ 'Single votes' | translate }}</h2>
<os-list-view-table <os-list-view-table
*ngIf="votesDataObservable"
[listObservable]="votesDataObservable" [listObservable]="votesDataObservable"
[columns]="columnDefinition" [columns]="columnDefinition"
[filterProps]="filterProps" [filterProps]="filterProps"
@ -107,12 +108,10 @@
</div> </div>
<div>{{ vote.valueVerbose | translate }}</div> <div>{{ vote.valueVerbose | translate }}</div>
</div> </div>
<!-- No Results -->
<div *pblNgridNoDataRef class="pbl-ngrid-no-data">
{{ 'The individual votes were made anonymous.' | translate }}
</div>
</os-list-view-table> </os-list-view-table>
<div *ngIf="!votesDataObservable">
{{ 'The individual votes were anonymized.' | translate }}
</div>
</div> </div>
</div> </div>

View File

@ -8,6 +8,7 @@ import { PblColumnDefinition } from '@pebula/ngrid';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service'; import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
import { MotionVoteRepositoryService } from 'app/core/repositories/motions/motion-vote-repository.service';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { ChartType } from 'app/shared/components/charts/charts.component'; import { ChartType } from 'app/shared/components/charts/charts.component';
@ -60,12 +61,13 @@ export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotio
pollDialog: MotionPollDialogService, pollDialog: MotionPollDialogService,
pollService: PollService, pollService: PollService,
private operator: OperatorService, private operator: OperatorService,
private router: Router private router: Router,
votesRepo: MotionVoteRepositoryService
) { ) {
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService); super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService, votesRepo);
} }
protected onPollWithOptionsLoaded(): void { protected createVotesData(): void {
this.setVotesData(this.poll.options[0].votes); this.setVotesData(this.poll.options[0].votes);
} }

View File

@ -1,4 +1,4 @@
<os-poll-form [data]="pollData" #pollForm></os-poll-form> <os-poll-form [data]="pollData" [percentBases]="PercentBaseVerbose" #pollForm></os-poll-form>
<ng-container *ngIf="pollForm.contentForm.get('type').value === 'analog'"> <ng-container *ngIf="pollForm.contentForm.get('type').value === 'analog'">
<div class="os-form-card-mobile" mat-dialog-content> <div class="os-form-card-mobile" mat-dialog-content>
<form [formGroup]="dialogVoteForm"> <form [formGroup]="dialogVoteForm">

View File

@ -6,9 +6,9 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll'; import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { MotionPollMethodsVerbose } from 'app/site/motions/models/view-motion-poll';
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component'; import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component'; import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form.component';
import { PercentBaseVerbose } from 'app/site/polls/models/view-base-poll';
@Component({ @Component({
selector: 'os-motion-poll-dialog', selector: 'os-motion-poll-dialog',
@ -16,7 +16,7 @@ import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form
styleUrls: ['./motion-poll-dialog.component.scss'] styleUrls: ['./motion-poll-dialog.component.scss']
}) })
export class MotionPollDialogComponent extends BasePollDialogComponent<ViewMotionPoll> { export class MotionPollDialogComponent extends BasePollDialogComponent<ViewMotionPoll> {
public motionPollMethods = { YNA: MotionPollMethodsVerbose.YNA }; public PercentBaseVerbose = PercentBaseVerbose;
@ViewChild('pollForm', { static: false }) @ViewChild('pollForm', { static: false })
protected pollForm: PollFormComponent<ViewMotionPoll>; protected pollForm: PollFormComponent<ViewMotionPoll>;

View File

@ -8,7 +8,7 @@ import { OperatorService } from 'app/core/core-services/operator.service';
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service'; import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
import { MotionVoteRepositoryService } from 'app/core/repositories/motions/motion-vote-repository.service'; import { MotionVoteRepositoryService } from 'app/core/repositories/motions/motion-vote-repository.service';
import { VotingService } from 'app/core/ui-services/voting.service'; import { VotingService } from 'app/core/ui-services/voting.service';
import { MotionPollMethods } from 'app/shared/models/motions/motion-poll'; import { MotionPollMethod } from 'app/shared/models/motions/motion-poll';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll'; import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote'; import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
import { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component'; import { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component';
@ -34,7 +34,7 @@ export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPol
*/ */
public currentVote: ViewMotionVote; public currentVote: ViewMotionVote;
public pollMethods = MotionPollMethods; public MotionPollMethod = MotionPollMethod;
private votes: ViewMotionVote[]; private votes: ViewMotionVote[];

View File

@ -5,7 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
import { ConstantsService } from 'app/core/core-services/constants.service'; import { ConstantsService } from 'app/core/core-services/constants.service';
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service'; import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { MotionPoll, MotionPollMethods } from 'app/shared/models/motions/motion-poll'; import { MotionPoll, MotionPollMethod } from 'app/shared/models/motions/motion-poll';
import { MajorityMethod, PercentBase } from 'app/shared/models/poll/base-poll'; import { MajorityMethod, PercentBase } from 'app/shared/models/poll/base-poll';
import { PollData, PollService } from 'app/site/polls/services/poll.service'; import { PollData, PollService } from 'app/site/polls/services/poll.service';
@ -60,13 +60,13 @@ export class MotionPollService extends PollService {
const length = this.pollRepo.getViewModelList().filter(item => item.motion_id === poll.motion_id).length; const length = this.pollRepo.getViewModelList().filter(item => item.motion_id === poll.motion_id).length;
poll.title = !length ? this.translate.instant('Vote') : `${this.translate.instant('Vote')} (${length + 1})`; poll.title = !length ? this.translate.instant('Vote') : `${this.translate.instant('Vote')} (${length + 1})`;
poll.pollmethod = MotionPollMethods.YNA; poll.pollmethod = MotionPollMethod.YNA;
return poll; return poll;
} }
public getPercentBase(poll: PollData): number { public getPercentBase(poll: PollData): number {
const base: PercentBase = poll.onehundred_percent_base; const base: PercentBase = poll.onehundred_percent_base as PercentBase;
let totalByBase: number; let totalByBase: number;
const result = poll.options[0]; const result = poll.options[0];

View File

@ -6,17 +6,21 @@ import { ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Label } from 'ng2-charts'; import { Label } from 'ng2-charts';
import { BehaviorSubject, from, Observable } from 'rxjs'; import { BehaviorSubject, from, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { BaseRepository } from 'app/core/repositories/base-repository';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service'; import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { ChartData, ChartType } from 'app/shared/components/charts/charts.component'; import { ChartData, ChartType } from 'app/shared/components/charts/charts.component';
import { BaseVote } from 'app/shared/models/poll/base-vote';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseViewComponent } from 'app/site/base/base-view';
import { ViewGroup } from 'app/site/users/models/view-group'; import { ViewGroup } from 'app/site/users/models/view-group';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { BasePollRepositoryService } from '../services/base-poll-repository.service'; import { BasePollRepositoryService } from '../services/base-poll-repository.service';
import { PollService } from '../services/poll.service'; import { PollService } from '../services/poll.service';
import { ViewBasePoll } from '../models/view-base-poll'; import { ViewBasePoll } from '../models/view-base-poll';
import { ViewBaseVote } from '../models/view-base-vote';
export interface BaseVoteData { export interface BaseVoteData {
user?: ViewUser; user?: ViewUser;
@ -74,6 +78,8 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
// The observable for the votes-per-user table // The observable for the votes-per-user table
public votesDataObservable: Observable<BaseVoteData[]>; public votesDataObservable: Observable<BaseVoteData[]>;
protected optionsLoaded = false;
/** /**
* Constructor * Constructor
* *
@ -98,9 +104,23 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
protected groupRepo: GroupRepositoryService, protected groupRepo: GroupRepositoryService,
protected promptService: PromptService, protected promptService: PromptService,
protected pollDialog: BasePollDialogService<V>, protected pollDialog: BasePollDialogService<V>,
protected pollService: PollService protected pollService: PollService,
protected votesRepo: BaseRepository<ViewBaseVote, BaseVote, object>
) { ) {
super(title, translate, matSnackbar); super(title, translate, matSnackbar);
votesRepo
.getViewModelListObservable()
.pipe(
filter(() => this.poll && this.canSeeVotes), // filter first for valid poll state to avoid unneccessary iteration of potentially thousands of votes
map(votes => votes.filter(vote => vote.option.poll_id === this.poll.id)),
filter(votes => !!votes.length)
)
.subscribe(() => {
// votes data can only be created when options are ready
if (this.optionsLoaded) {
this.createVotesData();
}
});
} }
/** /**
@ -143,12 +163,18 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
*/ */
protected onPollLoaded(): void {} protected onPollLoaded(): void {}
protected onPollWithOptionsLoaded(): void {} protected onPollWithOptionsLoaded(): void {
this.createVotesData();
}
protected onStateChanged(): void {} protected onStateChanged(): void {}
protected abstract hasPerms(): boolean; protected abstract hasPerms(): boolean;
protected get canSeeVotes(): boolean {
return (this.hasPerms && this.poll.isFinished) || this.poll.isPublished;
}
/** /**
* sets the votes data only if the poll wasn't pseudoanonymized * sets the votes data only if the poll wasn't pseudoanonymized
*/ */
@ -160,6 +186,11 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
} }
} }
/**
* Is called when the underlying vote data changes. Is supposed to call setVotesData
*/
protected abstract createVotesData(): void;
/** /**
* Initializes data for the shown chart. * Initializes data for the shown chart.
* Could be overwritten to implement custom chart data. * Could be overwritten to implement custom chart data.
@ -169,10 +200,10 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
} }
/** /**
* This checks, if the poll has votes. * This checks if the poll has votes.
*/ */
private checkData(): void { private checkData(): void {
if (this.poll.state === 3 || this.poll.state === 4) { if (this.poll.stateHasVotes) {
setTimeout(() => this.initChartData()); setTimeout(() => this.initChartData());
} }
} }
@ -203,6 +234,7 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
if (!this.poll.options || !this.poll.options.length) { if (!this.poll.options || !this.poll.options.length) {
setTimeout(() => this.waitForOptions(), 1); setTimeout(() => this.waitForOptions(), 1);
} else { } else {
this.optionsLoaded = true;
this.onPollWithOptionsLoaded(); this.onPollWithOptionsLoaded();
} }
} }

View File

@ -65,7 +65,7 @@
formControlName="onehundred_percent_base" formControlName="onehundred_percent_base"
required required
> >
<ng-container *ngFor="let option of percentBases | keyvalue"> <ng-container *ngFor="let option of percentBases | keyvalue: keepEntryOrder">
<mat-option [value]="option.key">{{ option.value | translate }}</mat-option> <mat-option [value]="option.key">{{ option.value | translate }}</mat-option>
</ng-container> </ng-container>
</mat-select> </mat-select>

View File

@ -44,6 +44,12 @@ export class PollFormComponent<T extends ViewBasePoll> extends BaseViewComponent
@Input() @Input()
public pollMethods: { [key: string]: string }; public pollMethods: { [key: string]: string };
/**
* The different percent bases for this poll.
*/
@Input()
public percentBases: { [key: string]: string };
@Input() @Input()
public data: Partial<T>; public data: Partial<T>;
@ -52,11 +58,6 @@ export class PollFormComponent<T extends ViewBasePoll> extends BaseViewComponent
*/ */
public pollTypes = PollTypeVerbose; public pollTypes = PollTypeVerbose;
/**
* The percent base for the poll.
*/
public percentBases: { [key: string]: string } = PercentBaseVerbose;
/** /**
* The majority methods for the poll. * The majority methods for the poll.
*/ */
@ -233,4 +234,11 @@ export class PollFormComponent<T extends ViewBasePoll> extends BaseViewComponent
global_abstain: [false] global_abstain: [false]
}); });
} }
/**
* compare function used with the KeyValuePipe to display the percent bases in original order
*/
public keepEntryOrder(): number {
return 0;
}
} }

View File

@ -0,0 +1,15 @@
import { BaseOption } from 'app/shared/models/poll/base-option';
import { BaseViewModel } from '../../base/base-view-model';
import { ViewBasePoll } from './view-base-poll';
import { ViewBaseVote } from './view-base-vote';
export class ViewBaseOption<M extends BaseOption<M> = any> extends BaseViewModel<M> {
public get option(): M {
return this._model;
}
}
export interface ViewBaseOption<M extends BaseOption<M> = any> extends BaseOption<M> {
votes: ViewBaseVote[];
poll: ViewBasePoll;
}

View File

@ -1,11 +1,10 @@
import { BasePoll, PercentBase, PollType } from 'app/shared/models/poll/base-poll'; import { BasePoll, PercentBase, PollType } from 'app/shared/models/poll/base-poll';
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
import { BaseViewModel } from 'app/site/base/base-view-model'; import { BaseViewModel } from 'app/site/base/base-view-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
import { ViewGroup } from 'app/site/users/models/view-group'; import { ViewGroup } from 'app/site/users/models/view-group';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { ViewBaseOption } from './view-base-option';
export enum PollClassType { export enum PollClassType {
Motion = 'motion', Motion = 'motion',
@ -73,9 +72,6 @@ export const MajorityMethodVerbose = {
disabled: 'Disabled' disabled: 'Disabled'
}; };
/**
* TODO: These need to be in order
*/
export const PercentBaseVerbose = { export const PercentBaseVerbose = {
YN: 'Yes/No', YN: 'Yes/No',
YNA: 'Yes/No/Abstain', YNA: 'Yes/No/Abstain',
@ -84,7 +80,11 @@ export const PercentBaseVerbose = {
disabled: 'Disabled' disabled: 'Disabled'
}; };
export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends BaseProjectableViewModel<M> { export abstract class ViewBasePoll<
M extends BasePoll<M, any, PM, PB> = any,
PM extends string = string,
PB extends string = string
> extends BaseProjectableViewModel<M> {
private _tableData: PollTableData[] = []; private _tableData: PollTableData[] = [];
protected voteTableKeys: VotingResult[] = [ protected voteTableKeys: VotingResult[] = [
@ -157,15 +157,15 @@ export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends Bas
return MajorityMethodVerbose[this.majority_method]; return MajorityMethodVerbose[this.majority_method];
} }
public get percentBaseVerbose(): string { public abstract get pollmethodVerbose(): string;
return PercentBaseVerbose[this.onehundred_percent_base];
} public abstract get percentBaseVerbose(): string;
public get showAbstainPercent(): boolean { public get showAbstainPercent(): boolean {
return this.onehundred_percent_base === PercentBase.YNA; return this.onehundred_percent_base === PercentBase.YNA;
} }
public abstract readonly pollClassType: 'motion' | 'assignment'; public abstract readonly pollClassType: PollClassType;
public canBeVotedFor: () => boolean; public canBeVotedFor: () => boolean;
@ -188,8 +188,12 @@ export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends Bas
public abstract generateTableData(): PollTableData[]; public abstract generateTableData(): PollTableData[];
} }
export interface ViewBasePoll<M extends BasePoll<M, any> = any> extends BasePoll<M, any> { export interface ViewBasePoll<
M extends BasePoll<M, any, PM, PB> = any,
PM extends string = string,
PB extends string = string
> extends BasePoll<M, any, PM, PB> {
voted: ViewUser[]; voted: ViewUser[];
groups: ViewGroup[]; groups: ViewGroup[];
options: (ViewMotionOption | ViewAssignmentOption)[]; // TODO find a better solution. but works for the moment options: ViewBaseOption[];
} }

View File

@ -0,0 +1,15 @@
import { BaseVote } from 'app/shared/models/poll/base-vote';
import { ViewUser } from 'app/site/users/models/view-user';
import { BaseViewModel } from '../../base/base-view-model';
import { ViewBaseOption } from './view-base-option';
export class ViewBaseVote<M extends BaseVote<M> = any> extends BaseViewModel<M> {
public get vote(): M {
return this._model;
}
}
export interface ViewBaseVote<M extends BaseVote<M> = any> extends BaseVote<M> {
user?: ViewUser;
option: ViewBaseOption;
}

View File

@ -9,7 +9,7 @@ import { MotionPollRepositoryService } from 'app/core/repositories/motions/motio
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll'; import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
import { BaseViewModel } from 'app/site/base/base-view-model'; import { BaseViewModel } from 'app/site/base/base-view-model';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll'; import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { ViewBasePoll } from '../models/view-base-poll'; import { PollClassType, ViewBasePoll } from '../models/view-base-poll';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -28,13 +28,13 @@ export class PollListObservableService implements HasViewModelListObservable<Vie
) { ) {
motionPollRepo motionPollRepo
.getViewModelListObservable() .getViewModelListObservable()
.subscribe(polls => this.adjustViewModelListObservable(polls, 'motion')); .subscribe(polls => this.adjustViewModelListObservable(polls, PollClassType.Motion));
assignmentPollRepo assignmentPollRepo
.getViewModelListObservable() .getViewModelListObservable()
.subscribe(polls => this.adjustViewModelListObservable(polls, 'assignment')); .subscribe(polls => this.adjustViewModelListObservable(polls, PollClassType.Assignment));
} }
private adjustViewModelListObservable(polls: ViewBasePoll[], mode: 'motion' | 'assignment'): void { private adjustViewModelListObservable(polls: ViewBasePoll[], mode: PollClassType): void {
this[mode + 'Polls'] = polls; this[mode + 'Polls'] = polls;
const allPolls = (this.motionPolls as ViewBasePoll[]).concat(this.assignmentPolls); const allPolls = (this.motionPolls as ViewBasePoll[]).concat(this.assignmentPolls);

View File

@ -2,10 +2,10 @@ import { Injectable } from '@angular/core';
import { _ } from 'app/core/translate/translation-marker'; import { _ } from 'app/core/translate/translation-marker';
import { ChartData, ChartType } from 'app/shared/components/charts/charts.component'; import { ChartData, ChartType } from 'app/shared/components/charts/charts.component';
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll'; import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
import { MotionPollMethods } from 'app/shared/models/motions/motion-poll'; import { MotionPollMethod } from 'app/shared/models/motions/motion-poll';
import { BasePoll, MajorityMethod, PercentBase, PollColor, PollType } from 'app/shared/models/poll/base-poll'; import { BasePoll, MajorityMethod, PollColor, PollType } from 'app/shared/models/poll/base-poll';
import { AssignmentPollMethodsVerbose } from 'app/site/assignments/models/view-assignment-poll'; import { AssignmentPollMethodVerbose } from 'app/site/assignments/models/view-assignment-poll';
import { import {
MajorityMethodVerbose, MajorityMethodVerbose,
PercentBaseVerbose, PercentBaseVerbose,
@ -90,8 +90,8 @@ export const PollMajorityMethod: CalculableMajorityMethod[] = [
]; ];
export interface PollData { export interface PollData {
pollmethod?: string; pollmethod: string;
onehundred_percent_base: PercentBase; onehundred_percent_base: string;
options: { options: {
user?: { user?: {
full_name: string; full_name: string;
@ -120,7 +120,7 @@ export abstract class PollService {
/** /**
* The default percentage base * The default percentage base
*/ */
public abstract defaultPercentBase: PercentBase; public abstract defaultPercentBase: string;
/** /**
* The default majority method * The default majority method
@ -170,7 +170,7 @@ export abstract class PollService {
case 'onehundred_percent_base': case 'onehundred_percent_base':
return PercentBaseVerbose[value]; return PercentBaseVerbose[value];
case 'pollmethod': case 'pollmethod':
return AssignmentPollMethodsVerbose[value]; return AssignmentPollMethodVerbose[value];
case 'type': case 'type':
return PollTypeVerbose[value]; return PollTypeVerbose[value];
} }
@ -181,7 +181,7 @@ export abstract class PollService {
} }
public generateChartData(poll: PollData): ChartData { public generateChartData(poll: PollData): ChartData {
if (poll.pollmethod === AssignmentPollMethods.Votes) { if (poll.pollmethod === AssignmentPollMethod.Votes) {
return this.generateCircleChartData(poll); return this.generateCircleChartData(poll);
} else { } else {
return this.generateBarChartData(poll); return this.generateBarChartData(poll);
@ -191,7 +191,7 @@ export abstract class PollService {
public generateBarChartData(poll: PollData): ChartData { public generateBarChartData(poll: PollData): ChartData {
const fields = ['yes', 'no']; const fields = ['yes', 'no'];
// cast is needed because ViewBasePoll doesn't have the field `pollmethod`, no easy fix :( // cast is needed because ViewBasePoll doesn't have the field `pollmethod`, no easy fix :(
if ((<any>poll).pollmethod === MotionPollMethods.YNA) { if ((<any>poll).pollmethod === MotionPollMethod.YNA) {
fields.push('abstain'); fields.push('abstain');
} }
const data: ChartData = fields.map(key => ({ const data: ChartData = fields.map(key => ({
@ -213,7 +213,7 @@ export abstract class PollService {
} }
public getChartType(poll: PollData): ChartType { public getChartType(poll: PollData): ChartType {
if ((<any>poll).pollmethod === AssignmentPollMethods.Votes) { if ((<any>poll).pollmethod === AssignmentPollMethod.Votes) {
return 'doughnut'; return 'doughnut';
} else { } else {
return 'horizontalBar'; return 'horizontalBar';

View File

@ -1,4 +1,4 @@
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll'; import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
import { MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll'; import { MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
import { AssignmentTitleInformation } from 'app/site/assignments/models/view-assignment'; import { AssignmentTitleInformation } from 'app/site/assignments/models/view-assignment';
import { BasePollSlideData } from 'app/slides/polls/base-poll-slide-data'; import { BasePollSlideData } from 'app/slides/polls/base-poll-slide-data';
@ -8,7 +8,7 @@ export interface AssignmentPollSlideData extends BasePollSlideData {
poll: { poll: {
title: string; title: string;
type: PollType; type: PollType;
pollmethod: AssignmentPollMethods; pollmethod: AssignmentPollMethod;
votes_amount: number; votes_amount: number;
description: string; description: string;
state: PollState; state: PollState;

View File

@ -1,4 +1,4 @@
import { MotionPollMethods } from 'app/shared/models/motions/motion-poll'; import { MotionPollMethod } from 'app/shared/models/motions/motion-poll';
import { MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll'; import { MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
import { MotionTitleInformation } from 'app/site/motions/models/view-motion'; import { MotionTitleInformation } from 'app/site/motions/models/view-motion';
import { BasePollSlideData } from 'app/slides/polls/base-poll-slide-data'; import { BasePollSlideData } from 'app/slides/polls/base-poll-slide-data';
@ -8,7 +8,7 @@ export interface MotionPollSlideData extends BasePollSlideData {
poll: { poll: {
title: string; title: string;
type: PollType; type: PollType;
pollmethod: MotionPollMethods; pollmethod: MotionPollMethod;
state: PollState; state: PollState;
onehundred_percent_base: PercentBase; onehundred_percent_base: PercentBase;
majority_method: MajorityMethod; majority_method: MajorityMethod;

View File

@ -96,8 +96,8 @@ class Migration(migrations.Migration):
name="onehundred_percent_base", name="onehundred_percent_base",
field=models.CharField( field=models.CharField(
choices=[ choices=[
("YN", "Yes/No per candidate"), ("YN", "Yes/No"),
("YNA", "Yes/No/Abstain per candidate"), ("YNA", "Yes/No/Abstain"),
("valid", "All valid ballots"), ("valid", "All valid ballots"),
("cast", "All casted ballots"), ("cast", "All casted ballots"),
("disabled", "Disabled (no percents)"), ("disabled", "Disabled (no percents)"),

View File

@ -155,8 +155,8 @@ class BasePoll(models.Model):
PERCENT_BASE_CAST = "cast" PERCENT_BASE_CAST = "cast"
PERCENT_BASE_DISABLED = "disabled" PERCENT_BASE_DISABLED = "disabled"
PERCENT_BASES: Iterable[Tuple[str, str]] = ( PERCENT_BASES: Iterable[Tuple[str, str]] = (
(PERCENT_BASE_YN, "Yes/No per candidate"), (PERCENT_BASE_YN, "Yes/No"),
(PERCENT_BASE_YNA, "Yes/No/Abstain per candidate"), (PERCENT_BASE_YNA, "Yes/No/Abstain"),
(PERCENT_BASE_VALID, "All valid ballots"), (PERCENT_BASE_VALID, "All valid ballots"),
(PERCENT_BASE_CAST, "All casted ballots"), (PERCENT_BASE_CAST, "All casted ballots"),
(PERCENT_BASE_DISABLED, "Disabled (no percents)"), (PERCENT_BASE_DISABLED, "Disabled (no percents)"),