added chart projection for polls
This commit is contained in:
parent
6ba0d0c5e6
commit
b48ca8c434
@ -23,7 +23,7 @@ interface ChartEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* One single collection in an arry.
|
||||
* One single collection in an array.
|
||||
*/
|
||||
export interface ChartDate {
|
||||
data: number[];
|
||||
@ -213,6 +213,7 @@ export class ChartsComponent extends BaseViewComponent {
|
||||
/**
|
||||
* Chart option for pie and doughnut
|
||||
*/
|
||||
@Input()
|
||||
public pieChartOptions: ChartOptions = {
|
||||
aspectRatio: 1
|
||||
};
|
||||
|
@ -59,7 +59,7 @@ export class SlideContainerComponent extends BaseComponent {
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -1,8 +1,24 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { AssignmentPollService } from 'app/site/assignments/services/assignment-poll.service';
|
||||
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
||||
import { PollPercentBasePipe } from './poll-percent-base.pipe';
|
||||
|
||||
describe('PollPercentBasePipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new PollPercentBasePipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
fdescribe('PollPercentBasePipe', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
});
|
||||
TestBed.compileComponents();
|
||||
});
|
||||
|
||||
it('create an instance', inject(
|
||||
[AssignmentPollService, MotionPollService],
|
||||
(assignmentPollService: AssignmentPollService, motionPollService: MotionPollService) => {
|
||||
const pipe = new PollPercentBasePipe(assignmentPollService, motionPollService);
|
||||
expect(pipe).toBeTruthy();
|
||||
}
|
||||
));
|
||||
});
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||
import { AssignmentPollService } from 'app/site/assignments/services/assignment-poll.service';
|
||||
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
||||
import { PollData } from 'app/site/polls/services/poll.service';
|
||||
|
||||
/**
|
||||
* Uses a number and a ViewPoll-object.
|
||||
@ -21,8 +23,18 @@ import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||
export class PollPercentBasePipe implements PipeTransform {
|
||||
private decimalPlaces = 3;
|
||||
|
||||
public transform(value: number, viewPoll: ViewBasePoll): string | null {
|
||||
const totalByBase = viewPoll.getPercentBase();
|
||||
public constructor(
|
||||
private assignmentPollService: AssignmentPollService,
|
||||
private motionPollService: MotionPollService
|
||||
) {}
|
||||
|
||||
public transform(value: number, poll: PollData): string | null {
|
||||
let totalByBase: number;
|
||||
if ((<any>poll).assignment) {
|
||||
totalByBase = this.assignmentPollService.getPercentBase(poll);
|
||||
} else {
|
||||
totalByBase = this.motionPollService.getPercentBase(poll);
|
||||
}
|
||||
|
||||
if (totalByBase) {
|
||||
const percentNumber = (value / totalByBase) * 100;
|
||||
|
@ -14,6 +14,7 @@ import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
import { ChartType } from 'app/shared/components/charts/charts.component';
|
||||
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
|
||||
import { PollService } from 'app/site/polls/services/poll.service';
|
||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
|
||||
@ -60,10 +61,11 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
groupRepo: GroupRepositoryService,
|
||||
prompt: PromptService,
|
||||
pollDialog: AssignmentPollDialogService,
|
||||
pollService: PollService,
|
||||
private operator: OperatorService,
|
||||
private viewport: ViewportService
|
||||
) {
|
||||
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog);
|
||||
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService);
|
||||
}
|
||||
|
||||
public onPollWithOptionsLoaded(): void {
|
||||
@ -129,7 +131,7 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
|
||||
this.setVotesData(Object.values(votes));
|
||||
|
||||
this.candidatesLabels = this.poll.initChartLabels();
|
||||
this.candidatesLabels = this.pollService.getChartLabels(this.poll);
|
||||
|
||||
this.isReady = true;
|
||||
}
|
||||
@ -146,7 +148,7 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
protected initChartData(): void {
|
||||
if (this.isVotedPoll) {
|
||||
this._chartType = 'doughnut';
|
||||
this.chartDataSubject.next(this.poll.generateCircleChartData());
|
||||
this.chartDataSubject.next(this.pollService.generateCircleChartData(this.poll));
|
||||
} else {
|
||||
super.initChartData();
|
||||
}
|
||||
|
@ -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 { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { ChartType } from 'app/shared/components/charts/charts.component';
|
||||
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { PollState } from 'app/shared/models/poll/base-poll';
|
||||
import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
|
||||
import { PollService } from 'app/site/polls/services/poll.service';
|
||||
@ -32,11 +31,8 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
||||
@Input()
|
||||
public set poll(value: ViewAssignmentPoll) {
|
||||
this.initPoll(value);
|
||||
this.candidatesLabels = value.initChartLabels();
|
||||
const chartData =
|
||||
value.pollmethod === AssignmentPollMethods.Votes
|
||||
? value.generateCircleChartData()
|
||||
: value.generateChartData();
|
||||
this.candidatesLabels = this.pollService.getChartLabels(value);
|
||||
const chartData = this.pollService.generateChartData(value);
|
||||
this.chartDataSubject.next(chartData);
|
||||
}
|
||||
|
||||
@ -45,7 +41,7 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
||||
}
|
||||
|
||||
public get chartType(): ChartType {
|
||||
return this.poll && this.poll.pollmethod === AssignmentPollMethods.Votes ? 'doughnut' : 'horizontalBar';
|
||||
return this.pollService.getChartType(this.poll);
|
||||
}
|
||||
|
||||
public candidatesLabels: string[] = [];
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||
import { AssignmentPoll, AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { PercentBase, PollColor, PollState } from 'app/shared/models/poll/base-poll';
|
||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { PollState } from 'app/shared/models/poll/base-poll';
|
||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||
import { PollData, ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||
import { PollTableData, ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||
import { ViewAssignment } from './view-assignment';
|
||||
import { ViewAssignmentOption } from './view-assignment-option';
|
||||
|
||||
@ -48,33 +48,7 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
|
||||
};
|
||||
}
|
||||
|
||||
public initChartLabels(): string[] {
|
||||
return this.options.map(candidate => candidate.user.full_name);
|
||||
}
|
||||
|
||||
public generateChartData(): ChartData {
|
||||
const fields = ['yes', 'no'];
|
||||
if (this.pollmethod === AssignmentPollMethods.YNA) {
|
||||
fields.push('abstain');
|
||||
}
|
||||
const data: ChartData = fields.map(key => ({
|
||||
label: key.toUpperCase(),
|
||||
data: this.options.map(vote => vote[key]),
|
||||
backgroundColor: PollColor[key],
|
||||
hoverBackgroundColor: PollColor[key]
|
||||
}));
|
||||
return data;
|
||||
}
|
||||
|
||||
public generateCircleChartData(): ChartData {
|
||||
const data: ChartData = this.options.map(candidate => ({
|
||||
label: candidate.user.getFullName(),
|
||||
data: [candidate.yes]
|
||||
}));
|
||||
return data;
|
||||
}
|
||||
|
||||
public generateTableData(): PollData[] {
|
||||
public generateTableData(): PollTableData[] {
|
||||
const data = this.options
|
||||
.map(candidate => ({
|
||||
yes: candidate.yes,
|
||||
@ -97,43 +71,6 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
|
||||
}
|
||||
return super.getNextStates();
|
||||
}
|
||||
|
||||
private sumOptionsYN(): number {
|
||||
return this.options.reduce((o, n) => {
|
||||
o += n.yes > 0 ? n.yes : 0;
|
||||
o += n.no > 0 ? n.no : 0;
|
||||
return o;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private sumOptionsYNA(): number {
|
||||
return this.options.reduce((o, n) => {
|
||||
o += n.abstain > 0 ? n.abstain : 0;
|
||||
return o;
|
||||
}, this.sumOptionsYN());
|
||||
}
|
||||
|
||||
public getPercentBase(): number {
|
||||
const base: PercentBase = this.poll.onehundred_percent_base;
|
||||
let totalByBase: number;
|
||||
switch (base) {
|
||||
case PercentBase.YN:
|
||||
totalByBase = this.sumOptionsYN();
|
||||
break;
|
||||
case PercentBase.YNA:
|
||||
totalByBase = this.sumOptionsYNA();
|
||||
break;
|
||||
case PercentBase.Valid:
|
||||
totalByBase = this.poll.votesvalid;
|
||||
break;
|
||||
case PercentBase.Cast:
|
||||
totalByBase = this.poll.votescast;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return totalByBase;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ViewAssignmentPoll extends AssignmentPoll {
|
||||
|
@ -6,7 +6,7 @@ import { HtmlToPdfService } from 'app/core/pdf-services/html-to-pdf.service';
|
||||
import { ParsePollNumberPipe } from 'app/shared/pipes/parse-poll-number.pipe';
|
||||
import { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.pipe';
|
||||
import { PollPercentBasePipe } from 'app/shared/pipes/poll-percent-base.pipe';
|
||||
import { PollData } from 'app/site/polls/models/view-base-poll';
|
||||
import { PollTableData } from 'app/site/polls/models/view-base-poll';
|
||||
import { ViewAssignment } from '../models/view-assignment';
|
||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||
|
||||
@ -215,7 +215,7 @@ export class AssignmentPdfService {
|
||||
/**
|
||||
* Converts pollData to a printable string representation
|
||||
*/
|
||||
private getPollResult(votingResult: PollData, poll: ViewAssignmentPoll): string {
|
||||
private getPollResult(votingResult: PollTableData, poll: ViewAssignmentPoll): string {
|
||||
const resultList = poll.pollmethodFields.map(field => {
|
||||
const votingKey = this.translate.instant(this.pollKeyVerbose.transform(field));
|
||||
const resultValue = this.parsePollNumber.transform(votingResult[field]);
|
||||
|
@ -8,7 +8,7 @@ import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { Collection } from 'app/shared/models/base/collection';
|
||||
import { MajorityMethod, PercentBase } from 'app/shared/models/poll/base-poll';
|
||||
import { PollService } from 'app/site/polls/services/poll.service';
|
||||
import { PollData, PollService } from 'app/site/polls/services/poll.service';
|
||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||
|
||||
@Injectable({
|
||||
@ -53,4 +53,41 @@ export class AssignmentPollService extends PollService {
|
||||
poll.pollmethod = AssignmentPollMethods.YN;
|
||||
poll.assignment_id = poll.assignment_id;
|
||||
}
|
||||
|
||||
private sumOptionsYN(poll: PollData): number {
|
||||
return poll.options.reduce((o, n) => {
|
||||
o += n.yes > 0 ? n.yes : 0;
|
||||
o += n.no > 0 ? n.no : 0;
|
||||
return o;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private sumOptionsYNA(poll: PollData): number {
|
||||
return poll.options.reduce((o, n) => {
|
||||
o += n.abstain > 0 ? n.abstain : 0;
|
||||
return o;
|
||||
}, this.sumOptionsYN(poll));
|
||||
}
|
||||
|
||||
public getPercentBase(poll: PollData): number {
|
||||
const base: PercentBase = poll.onehundred_percent_base;
|
||||
let totalByBase: number;
|
||||
switch (base) {
|
||||
case PercentBase.YN:
|
||||
totalByBase = this.sumOptionsYN(poll);
|
||||
break;
|
||||
case PercentBase.YNA:
|
||||
totalByBase = this.sumOptionsYNA(poll);
|
||||
break;
|
||||
case PercentBase.Valid:
|
||||
totalByBase = poll.votesvalid;
|
||||
break;
|
||||
case PercentBase.Cast:
|
||||
totalByBase = poll.votescast;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return totalByBase;
|
||||
}
|
||||
}
|
||||
|
@ -9,19 +9,6 @@ export class ViewMotionOption extends BaseViewModel<MotionOption> {
|
||||
}
|
||||
public static COLLECTIONSTRING = MotionOption.COLLECTIONSTRING;
|
||||
protected _collectionString = MotionOption.COLLECTIONSTRING;
|
||||
|
||||
public sumYN(): number {
|
||||
let sum = 0;
|
||||
sum += this.yes > 0 ? this.yes : 0;
|
||||
sum += this.no > 0 ? this.no : 0;
|
||||
return sum;
|
||||
}
|
||||
|
||||
public sumYNA(): number {
|
||||
let sum = this.sumYN();
|
||||
sum += this.abstain > 0 ? this.abstain : 0;
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
||||
interface TIMotionOptionRelations {
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||
import { MotionPoll, MotionPollMethods } from 'app/shared/models/motions/motion-poll';
|
||||
import { PercentBase, PollColor, PollState } from 'app/shared/models/poll/base-poll';
|
||||
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
||||
import { PollState } from 'app/shared/models/poll/base-poll';
|
||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||
import { PollData, ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||
import { PollTableData, ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||
import { ViewMotion } from './view-motion';
|
||||
|
||||
export interface MotionPollTitleInformation {
|
||||
@ -76,15 +75,11 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
|
||||
return !!this.result.votes.length;
|
||||
}
|
||||
|
||||
public initChartLabels(): string[] {
|
||||
return ['Votes'];
|
||||
}
|
||||
|
||||
public getContentObject(): BaseViewModel {
|
||||
return this.motion;
|
||||
}
|
||||
|
||||
public generateTableData(): PollData[] {
|
||||
public generateTableData(): PollTableData[] {
|
||||
let tableData = this.options.flatMap(vote =>
|
||||
this.tableKeys.map(key => ({
|
||||
key: key.vote,
|
||||
@ -101,21 +96,6 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
|
||||
return tableData;
|
||||
}
|
||||
|
||||
public generateChartData(): ChartData {
|
||||
const fields = ['yes', 'no'];
|
||||
if (this.pollmethod === MotionPollMethods.YNA) {
|
||||
fields.push('abstain');
|
||||
}
|
||||
const data: ChartData = fields.map(key => ({
|
||||
label: key.toUpperCase(),
|
||||
data: this.options.map(option => option[key]),
|
||||
backgroundColor: PollColor[key],
|
||||
hoverBackgroundColor: PollColor[key]
|
||||
}));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||
return {
|
||||
getBasicProjectorElement: options => ({
|
||||
@ -146,39 +126,6 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
|
||||
}
|
||||
return super.getNextStates();
|
||||
}
|
||||
|
||||
public getPercentBase(): number {
|
||||
const base: PercentBase = this.poll.onehundred_percent_base;
|
||||
|
||||
let totalByBase: number;
|
||||
switch (base) {
|
||||
case PercentBase.YN:
|
||||
if (this.result.yes >= 0 && this.result.no >= 0) {
|
||||
totalByBase = this.result.sumYN();
|
||||
}
|
||||
break;
|
||||
case PercentBase.YNA:
|
||||
if (this.result.yes >= 0 && this.result.no >= 0 && this.result.abstain >= 0) {
|
||||
totalByBase = this.result.sumYNA();
|
||||
}
|
||||
break;
|
||||
case PercentBase.Valid:
|
||||
// auslagern
|
||||
if (this.result.yes >= 0 && this.result.no >= 0 && this.result.abstain >= 0) {
|
||||
totalByBase = this.poll.votesvalid;
|
||||
}
|
||||
break;
|
||||
case PercentBase.Cast:
|
||||
totalByBase = this.poll.votescast;
|
||||
break;
|
||||
case PercentBase.Disabled:
|
||||
break;
|
||||
default:
|
||||
throw new Error('The given poll has no percent base: ' + this);
|
||||
}
|
||||
|
||||
return totalByBase;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ViewMotionPoll extends MotionPoll {
|
||||
|
@ -15,6 +15,7 @@ import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||
import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service';
|
||||
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
|
||||
import { PollService } from 'app/site/polls/services/poll.service';
|
||||
|
||||
@Component({
|
||||
selector: 'os-motion-poll-detail',
|
||||
@ -57,10 +58,11 @@ export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotio
|
||||
groupRepo: GroupRepositoryService,
|
||||
prompt: PromptService,
|
||||
pollDialog: MotionPollDialogService,
|
||||
pollService: PollService,
|
||||
private operator: OperatorService,
|
||||
private router: Router
|
||||
) {
|
||||
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog);
|
||||
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService);
|
||||
}
|
||||
|
||||
protected onPollWithOptionsLoaded(): void {
|
||||
|
@ -58,19 +58,19 @@
|
||||
</os-charts>
|
||||
</div>
|
||||
<div class="vote-legend">
|
||||
<div class="votes-yes" *ngIf="isVoteDocumented(voteYes)">
|
||||
<div class="votes-yes" *ngIf="pollService.isVoteDocumented(voteYes)">
|
||||
<os-icon-container icon="thumb_up" size="large">
|
||||
{{ voteYes | parsePollNumber }}
|
||||
{{ voteYes | pollPercentBase: poll }}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
<div class="votes-no" *ngIf="isVoteDocumented(voteNo)">
|
||||
<div class="votes-no" *ngIf="pollService.isVoteDocumented(voteNo)">
|
||||
<os-icon-container icon="thumb_down" size="large">
|
||||
{{ voteNo | parsePollNumber }}
|
||||
{{ voteNo | pollPercentBase: poll }}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
<div class="votes-abstain" *ngIf="isVoteDocumented(voteAbstain)">
|
||||
<div class="votes-abstain" *ngIf="pollService.isVoteDocumented(voteAbstain)">
|
||||
<os-icon-container icon="trip_origin" size="large">
|
||||
{{ voteAbstain | parsePollNumber }}
|
||||
{{ voteAbstain | pollPercentBase: poll }}
|
||||
|
@ -3,12 +3,10 @@ import { MatDialog, MatSnackBar } from '@angular/material';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||
import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service';
|
||||
import { MotionPollPdfService } from 'app/site/motions/services/motion-poll-pdf.service';
|
||||
@ -32,7 +30,7 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
public set poll(value: ViewMotionPoll) {
|
||||
this.initPoll(value);
|
||||
|
||||
const chartData = this.poll.generateChartData();
|
||||
const chartData = this.pollService.generateChartData(value);
|
||||
for (const data of chartData) {
|
||||
if (data.label === 'YES') {
|
||||
this.voteYes = data.data[0];
|
||||
@ -55,11 +53,6 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
return `/motions/polls/${this.poll.id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subject to holding the data needed for the chart.
|
||||
*/
|
||||
public chartDataSubject: BehaviorSubject<ChartData> = new BehaviorSubject([]);
|
||||
|
||||
/**
|
||||
* Number of votes for `Yes`.
|
||||
*/
|
||||
@ -148,8 +141,4 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
this.repo.delete(this.poll).catch(this.raiseError);
|
||||
}
|
||||
}
|
||||
|
||||
public isVoteDocumented(vote: number): boolean {
|
||||
return vote !== null && vote !== undefined && vote !== -2;
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,13 @@ import { Collection } from 'app/shared/models/base/collection';
|
||||
import { MotionPollMethods } from 'app/shared/models/motions/motion-poll';
|
||||
import { MajorityMethod, PercentBase } from 'app/shared/models/poll/base-poll';
|
||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||
import { PollService } from 'app/site/polls/services/poll.service';
|
||||
import { PollData, PollService } from 'app/site/polls/services/poll.service';
|
||||
|
||||
interface PollResultData {
|
||||
yes?: number;
|
||||
no?: number;
|
||||
abstain?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service class for motion polls.
|
||||
@ -55,4 +61,51 @@ export class MotionPollService extends PollService {
|
||||
poll.pollmethod = MotionPollMethods.YNA;
|
||||
poll.motion_id = poll.motion_id;
|
||||
}
|
||||
|
||||
public getPercentBase(poll: PollData): number {
|
||||
const base: PercentBase = poll.onehundred_percent_base;
|
||||
|
||||
let totalByBase: number;
|
||||
const result = poll.options[0];
|
||||
switch (base) {
|
||||
case PercentBase.YN:
|
||||
if (result.yes >= 0 && result.no >= 0) {
|
||||
totalByBase = this.sumYN(result);
|
||||
}
|
||||
break;
|
||||
case PercentBase.YNA:
|
||||
if (result.yes >= 0 && result.no >= 0 && result.abstain >= 0) {
|
||||
totalByBase = this.sumYNA(result);
|
||||
}
|
||||
break;
|
||||
case PercentBase.Valid:
|
||||
// auslagern
|
||||
if (result.yes >= 0 && result.no >= 0 && result.abstain >= 0) {
|
||||
totalByBase = poll.votesvalid;
|
||||
}
|
||||
break;
|
||||
case PercentBase.Cast:
|
||||
totalByBase = poll.votescast;
|
||||
break;
|
||||
case PercentBase.Disabled:
|
||||
break;
|
||||
default:
|
||||
throw new Error('The given poll has no percent base: ' + this);
|
||||
}
|
||||
|
||||
return totalByBase;
|
||||
}
|
||||
|
||||
private sumYN(result: PollResultData): number {
|
||||
let sum = 0;
|
||||
sum += result.yes > 0 ? result.yes : 0;
|
||||
sum += result.no > 0 ? result.no : 0;
|
||||
return sum;
|
||||
}
|
||||
|
||||
private sumYNA(result: PollResultData): number {
|
||||
let sum = this.sumYN(result);
|
||||
sum += result.abstain > 0 ? result.abstain : 0;
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { BasePollRepositoryService } from '../services/base-poll-repository.service';
|
||||
import { PollService } from '../services/poll.service';
|
||||
import { ViewBasePoll } from '../models/view-base-poll';
|
||||
|
||||
export interface BaseVoteData {
|
||||
@ -103,7 +104,8 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
||||
protected route: ActivatedRoute,
|
||||
protected groupRepo: GroupRepositoryService,
|
||||
protected promptService: PromptService,
|
||||
protected pollDialog: BasePollDialogService<V>
|
||||
protected pollDialog: BasePollDialogService<V>,
|
||||
protected pollService: PollService
|
||||
) {
|
||||
super(title, translate, matSnackbar);
|
||||
}
|
||||
@ -174,7 +176,7 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
||||
* Could be overwritten to implement custom chart data.
|
||||
*/
|
||||
protected initChartData(): void {
|
||||
this.chartDataSubject.next(this.poll.generateChartData());
|
||||
this.chartDataSubject.next(this.pollService.generateChartData(this.poll));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||
import { BasePoll, PollState } 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';
|
||||
@ -16,7 +15,7 @@ export enum PollClassType {
|
||||
/**
|
||||
* Interface describes the possible data for the result-table.
|
||||
*/
|
||||
export interface PollData {
|
||||
export interface PollTableData {
|
||||
key?: string;
|
||||
value?: number;
|
||||
yes?: number;
|
||||
@ -84,9 +83,9 @@ export const PercentBaseVerbose = {
|
||||
};
|
||||
|
||||
export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends BaseProjectableViewModel<M> {
|
||||
private _tableData: PollData[] = [];
|
||||
private _tableData: PollTableData[] = [];
|
||||
|
||||
public get tableData(): PollData[] {
|
||||
public get tableData(): PollTableData[] {
|
||||
if (!this._tableData.length) {
|
||||
this._tableData = this.generateTableData();
|
||||
}
|
||||
@ -154,20 +153,11 @@ export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends Bas
|
||||
|
||||
public abstract getContentObject(): BaseViewModel;
|
||||
|
||||
/**
|
||||
* Initializes labels for a chart.
|
||||
*/
|
||||
public abstract initChartLabels(): string[];
|
||||
|
||||
public abstract generateChartData(): ChartData;
|
||||
|
||||
public abstract generateTableData(): PollData[];
|
||||
|
||||
public abstract getPercentBase(): number;
|
||||
public abstract generateTableData(): PollTableData[];
|
||||
}
|
||||
|
||||
export interface ViewBasePoll<M extends BasePoll<M, any> = any> extends BasePoll<M, any> {
|
||||
voted: ViewUser[];
|
||||
groups: ViewGroup[];
|
||||
options: ViewMotionOption[] | ViewAssignmentOption[]; // TODO find a better solution. but works for the moment
|
||||
options: (ViewMotionOption | ViewAssignmentOption)[]; // TODO find a better solution. but works for the moment
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { _ } from 'app/core/translate/translation-marker';
|
||||
import { ChartData, ChartType } from 'app/shared/components/charts/charts.component';
|
||||
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { Collection } from 'app/shared/models/base/collection';
|
||||
import { MajorityMethod, PercentBase, PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { MotionPollMethods } from 'app/shared/models/motions/motion-poll';
|
||||
import { MajorityMethod, PercentBase, PollColor, PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { AssignmentPollMethodsVerbose } from 'app/site/assignments/models/view-assignment-poll';
|
||||
import {
|
||||
MajorityMethodVerbose,
|
||||
@ -88,6 +91,22 @@ export const PollMajorityMethod: CalculableMajorityMethod[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export interface PollData {
|
||||
pollmethod?: string;
|
||||
onehundred_percent_base: PercentBase;
|
||||
options: {
|
||||
user?: {
|
||||
full_name: string;
|
||||
};
|
||||
yes?: number;
|
||||
no?: number;
|
||||
abstain?: number;
|
||||
}[];
|
||||
votesvalid: number;
|
||||
votesinvalid: number;
|
||||
votescast: number;
|
||||
}
|
||||
|
||||
interface OpenSlidesSettings {
|
||||
ENABLE_ELECTRONIC_VOTING: boolean;
|
||||
}
|
||||
@ -122,10 +141,6 @@ export abstract class PollService {
|
||||
*/
|
||||
public pollValues: CalculablePollKey[] = ['yes', 'no', 'abstain', 'votesvalid', 'votesinvalid', 'votescast'];
|
||||
|
||||
/**
|
||||
* empty constructor
|
||||
*
|
||||
*/
|
||||
public constructor(constants: ConstantsService) {
|
||||
constants
|
||||
.get<OpenSlidesSettings>('Settings')
|
||||
@ -158,4 +173,52 @@ export abstract class PollService {
|
||||
public getVerboseNameForKey(key: string): string {
|
||||
return PollPropertyVerbose[key];
|
||||
}
|
||||
|
||||
public generateChartData(poll: PollData): ChartData {
|
||||
if (poll.pollmethod === AssignmentPollMethods.Votes) {
|
||||
return this.generateCircleChartData(poll);
|
||||
} else {
|
||||
return this.generateBarChartData(poll);
|
||||
}
|
||||
}
|
||||
|
||||
public generateBarChartData(poll: PollData): ChartData {
|
||||
const fields = ['yes', 'no'];
|
||||
// cast is needed because ViewBasePoll doesn't have the field `pollmethod`, no easy fix :(
|
||||
if ((<any>poll).pollmethod === MotionPollMethods.YNA) {
|
||||
fields.push('abstain');
|
||||
}
|
||||
const data: ChartData = fields.map(key => ({
|
||||
label: key.toUpperCase(),
|
||||
data: poll.options.map(option => option[key]),
|
||||
backgroundColor: PollColor[key],
|
||||
hoverBackgroundColor: PollColor[key]
|
||||
}));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public generateCircleChartData(poll: PollData): ChartData {
|
||||
const data: ChartData = poll.options.map(candidate => ({
|
||||
label: candidate.user.full_name,
|
||||
data: [candidate.yes]
|
||||
}));
|
||||
return data;
|
||||
}
|
||||
|
||||
public getChartType(poll: PollData): ChartType {
|
||||
if ((<any>poll).pollmethod === AssignmentPollMethods.Votes) {
|
||||
return 'doughnut';
|
||||
} else {
|
||||
return 'horizontalBar';
|
||||
}
|
||||
}
|
||||
|
||||
public getChartLabels(poll: PollData): string[] {
|
||||
return poll.options.map(candidate => candidate.user.full_name);
|
||||
}
|
||||
|
||||
public isVoteDocumented(vote: number): boolean {
|
||||
return vote !== null && vote !== undefined && vote !== -2;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { AssignmentTitleInformation } from 'app/site/assignments/models/view-assignment';
|
||||
import { BasePollSlideData } from 'app/slides/polls/base-poll-slide-data';
|
||||
|
||||
export interface AssignmentPollSlideData {
|
||||
export interface AssignmentPollSlideData extends BasePollSlideData {
|
||||
assignment: AssignmentTitleInformation;
|
||||
poll: {
|
||||
title: string;
|
||||
@ -11,21 +12,23 @@ export interface AssignmentPollSlideData {
|
||||
votes_amount: number;
|
||||
description: string;
|
||||
state: PollState;
|
||||
onehundered_percent_base: PercentBase;
|
||||
onehundred_percent_base: PercentBase;
|
||||
majority_method: MajorityMethod;
|
||||
|
||||
options: {
|
||||
user: string;
|
||||
yes?: string;
|
||||
no?: string;
|
||||
abstain?: string;
|
||||
user: {
|
||||
full_name: string;
|
||||
};
|
||||
yes?: number;
|
||||
no?: number;
|
||||
abstain?: number;
|
||||
}[];
|
||||
|
||||
// optional for published polls:
|
||||
amount_global_no?: string;
|
||||
amount_global_abstain: string;
|
||||
votesvalid: string;
|
||||
votesinvalid: string;
|
||||
votescast: string;
|
||||
amount_global_no?: number;
|
||||
amount_global_abstain?: number;
|
||||
votesvalid: number;
|
||||
votesinvalid: number;
|
||||
votescast: number;
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,19 @@
|
||||
<div *ngIf="data">
|
||||
|
||||
<pre>{{ verboseData }}</pre>
|
||||
|
||||
</div>
|
||||
<ng-container *ngIf="data && data.data">
|
||||
<div class="slidetitle">
|
||||
<h1 class="assignment-title">{{ data.data.assignment.title }}</h1>
|
||||
<h2 class="poll-title">{{ data.data.poll.title }}</h2>
|
||||
</div>
|
||||
<div class="charts-wrapper" *ngIf="data.data.poll.state === PollState.Published">
|
||||
<os-charts
|
||||
[type]="pollService.getChartType(data.data.poll)"
|
||||
[labels]="pollService.getChartLabels(data.data.poll)"
|
||||
[data]="chartDataSubject"
|
||||
[hasPadding]="false"
|
||||
[pieChartOptions]="options"
|
||||
></os-charts>
|
||||
</div>
|
||||
<div *ngIf="data.data.poll.state !== PollState.Published">
|
||||
<!-- TODO -->
|
||||
{{ "Nothing to see here!" | translate }}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -0,0 +1,11 @@
|
||||
.assignment-title {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.slidetitle {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.charts-wrapper {
|
||||
position: relative;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { BaseSlideComponent } from 'app/slides/base-slide-component';
|
||||
import { PollState } from 'app/shared/models/poll/base-poll';
|
||||
import { BasePollSlideComponent } from 'app/slides/polls/base-poll-slide.component';
|
||||
import { AssignmentPollSlideData } from './assignment-poll-slide-data';
|
||||
|
||||
@Component({
|
||||
@ -8,8 +9,8 @@ import { AssignmentPollSlideData } from './assignment-poll-slide-data';
|
||||
templateUrl: './assignment-poll-slide.component.html',
|
||||
styleUrls: ['./assignment-poll-slide.component.scss']
|
||||
})
|
||||
export class AssignmentPollSlideComponent extends BaseSlideComponent<AssignmentPollSlideData> {
|
||||
public get verboseData(): string {
|
||||
return JSON.stringify(this.data, null, 2);
|
||||
}
|
||||
export class AssignmentPollSlideComponent extends BasePollSlideComponent<AssignmentPollSlideData> {
|
||||
public PollState = PollState;
|
||||
|
||||
public options = { maintainAspectRatio: false, responsive: true, legend: { position: 'right' } };
|
||||
}
|
||||
|
@ -1,26 +1,27 @@
|
||||
import { MotionPollMethods } from 'app/shared/models/motions/motion-poll';
|
||||
import { MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { MotionTitleInformation } from 'app/site/motions/models/view-motion';
|
||||
import { BasePollSlideData } from 'app/slides/polls/base-poll-slide-data';
|
||||
|
||||
export interface MotionPollSlideData {
|
||||
export interface MotionPollSlideData extends BasePollSlideData {
|
||||
motion: MotionTitleInformation;
|
||||
poll: {
|
||||
title: string;
|
||||
type: PollType;
|
||||
pollmethod: MotionPollMethods;
|
||||
state: PollState;
|
||||
onehundered_percent_base: PercentBase;
|
||||
onehundred_percent_base: PercentBase;
|
||||
majority_method: MajorityMethod;
|
||||
|
||||
options: {
|
||||
yes?: string;
|
||||
no?: string;
|
||||
abstain?: string;
|
||||
yes?: number;
|
||||
no?: number;
|
||||
abstain?: number;
|
||||
}[];
|
||||
|
||||
// optional for published polls:
|
||||
votesvalid: string;
|
||||
votesinvalid: string;
|
||||
votescast: string;
|
||||
votesvalid: number;
|
||||
votesinvalid: number;
|
||||
votescast: number;
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,45 @@
|
||||
<div *ngIf="data">
|
||||
|
||||
<pre>{{ verboseData }}</pre>
|
||||
|
||||
</div>
|
||||
<ng-container *ngIf="data && data.data">
|
||||
<div class="slidetitle">
|
||||
<h1 class="motion-title">
|
||||
<span *ngIf="data.data.motion.identifier">{{ data.data.motion.identifier }}:</span>
|
||||
{{ data.data.motion.title }}
|
||||
</h1>
|
||||
<h2 class="poll-title">{{ data.data.poll.title }}</h2>
|
||||
</div>
|
||||
<div class="poll-chart-wrapper" *ngIf="data.data.poll.state === PollState.Published">
|
||||
<div class="doughnut-chart">
|
||||
<os-charts
|
||||
[type]="'doughnut'"
|
||||
[data]="chartDataSubject"
|
||||
[pieChartOptions]="{ maintainAspectRatio: false, responsive: true }"
|
||||
[showLegend]="false"
|
||||
[hasPadding]="false"
|
||||
>
|
||||
</os-charts>
|
||||
</div>
|
||||
<div class="vote-legend">
|
||||
<div class="votes-yes" *ngIf="pollService.isVoteDocumented(voteYes)">
|
||||
<os-icon-container icon="thumb_up" size="large">
|
||||
{{ voteYes | parsePollNumber }}
|
||||
{{ voteYes | pollPercentBase: data.data.poll }}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
<div class="votes-no" *ngIf="pollService.isVoteDocumented(voteNo)">
|
||||
<os-icon-container icon="thumb_down" size="large">
|
||||
{{ voteNo | parsePollNumber }}
|
||||
{{ voteNo | pollPercentBase: data.data.poll }}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
<div class="votes-abstain" *ngIf="pollService.isVoteDocumented(voteAbstain)">
|
||||
<os-icon-container icon="trip_origin" size="large">
|
||||
{{ voteAbstain | parsePollNumber }}
|
||||
{{ voteAbstain | pollPercentBase: data.data.poll }}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="data.data.poll.state !== PollState.Published">
|
||||
<!-- TODO -->
|
||||
{{ "Nothing to see here!" | translate }}
|
||||
</div>
|
||||
</ng-container>
|
@ -0,0 +1,40 @@
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
|
||||
.motion-title {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.poll-chart-wrapper {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-areas: 'chart legend';
|
||||
grid-template-columns: min-content auto;
|
||||
|
||||
.doughnut-chart {
|
||||
grid-area: chart;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.vote-legend {
|
||||
grid-area: legend;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
|
||||
div + div {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.votes-yes {
|
||||
color: $votes-yes-color;
|
||||
}
|
||||
|
||||
.votes-no {
|
||||
color: $votes-no-color;
|
||||
}
|
||||
|
||||
.votes-abstain {
|
||||
color: $votes-abstain-color;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { BaseSlideComponent } from 'app/slides/base-slide-component';
|
||||
import { PollState } from 'app/shared/models/poll/base-poll';
|
||||
import { PollService } from 'app/site/polls/services/poll.service';
|
||||
import { BasePollSlideComponent } from 'app/slides/polls/base-poll-slide.component';
|
||||
import { MotionPollSlideData } from './motion-poll-slide-data';
|
||||
|
||||
@Component({
|
||||
@ -8,8 +10,22 @@ import { MotionPollSlideData } from './motion-poll-slide-data';
|
||||
templateUrl: './motion-poll-slide.component.html',
|
||||
styleUrls: ['./motion-poll-slide.component.scss']
|
||||
})
|
||||
export class MotionPollSlideComponent extends BaseSlideComponent<MotionPollSlideData> {
|
||||
public get verboseData(): string {
|
||||
return JSON.stringify(this.data, null, 2);
|
||||
export class MotionPollSlideComponent extends BasePollSlideComponent<MotionPollSlideData> {
|
||||
public PollState = PollState;
|
||||
|
||||
public voteYes: number;
|
||||
public voteNo: number;
|
||||
public voteAbstain: number;
|
||||
|
||||
public constructor(pollService: PollService) {
|
||||
super(pollService);
|
||||
this.chartDataSubject.subscribe(() => {
|
||||
if (this.data && this.data.data) {
|
||||
const result = this.data.data.poll.options[0];
|
||||
this.voteYes = result.yes;
|
||||
this.voteNo = result.no;
|
||||
this.voteAbstain = result.abstain;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
22
client/src/app/slides/polls/base-poll-slide-data.ts
Normal file
22
client/src/app/slides/polls/base-poll-slide-data.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
|
||||
|
||||
export interface BasePollSlideData {
|
||||
poll: {
|
||||
title: string;
|
||||
type: PollType;
|
||||
state: PollState;
|
||||
onehundred_percent_base: PercentBase;
|
||||
majority_method: MajorityMethod;
|
||||
pollmethod: string;
|
||||
|
||||
options: {
|
||||
yes?: number;
|
||||
no?: number;
|
||||
abstain?: number;
|
||||
}[];
|
||||
|
||||
votesvalid: number;
|
||||
votesinvalid: number;
|
||||
votescast: number;
|
||||
};
|
||||
}
|
36
client/src/app/slides/polls/base-poll-slide.component.ts
Normal file
36
client/src/app/slides/polls/base-poll-slide.component.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { forwardRef, Inject, Input } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { SlideData } from 'app/core/core-services/projector-data.service';
|
||||
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||
import { PollState } from 'app/shared/models/poll/base-poll';
|
||||
import { PollService } from 'app/site/polls/services/poll.service';
|
||||
import { BasePollSlideData } from './base-poll-slide-data';
|
||||
import { BaseSlideComponent } from '../base-slide-component';
|
||||
|
||||
export class BasePollSlideComponent<T extends BasePollSlideData> extends BaseSlideComponent<T> {
|
||||
public chartDataSubject: BehaviorSubject<ChartData> = new BehaviorSubject([]);
|
||||
|
||||
@Input()
|
||||
public set data(value: SlideData<T>) {
|
||||
this._data = value;
|
||||
if (value.data.poll.state === PollState.Published) {
|
||||
const chartData = this.pollService.generateChartData(value.data.poll);
|
||||
this.chartDataSubject.next(chartData);
|
||||
}
|
||||
}
|
||||
|
||||
public get data(): SlideData<T> {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
private _data: SlideData<T>;
|
||||
|
||||
public constructor(
|
||||
@Inject(forwardRef(() => PollService))
|
||||
public pollService: PollService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
@ -87,6 +87,7 @@ class AssignmentManager(BaseManager):
|
||||
"tags",
|
||||
"attachments",
|
||||
"polls",
|
||||
"polls__options",
|
||||
)
|
||||
)
|
||||
|
||||
@ -274,8 +275,27 @@ class AssignmentVote(RESTModelMixin, BaseVote):
|
||||
default_permissions = ()
|
||||
|
||||
|
||||
class AssignmentOptionManager(BaseManager):
|
||||
"""
|
||||
Customized model manager to support our get_prefetched_queryset method.
|
||||
"""
|
||||
|
||||
def get_prefetched_queryset(self, *args, **kwargs):
|
||||
"""
|
||||
Returns the normal queryset with all voted users. In the background we
|
||||
join and prefetch all related models.
|
||||
"""
|
||||
return (
|
||||
super()
|
||||
.get_prefetched_queryset(*args, **kwargs)
|
||||
.select_related("user", "poll")
|
||||
.prefetch_related("voted", "votes")
|
||||
)
|
||||
|
||||
|
||||
class AssignmentOption(RESTModelMixin, BaseOption):
|
||||
access_permissions = AssignmentOptionAccessPermissions()
|
||||
objects = AssignmentOptionManager()
|
||||
vote_class = AssignmentVote
|
||||
|
||||
poll = models.ForeignKey(
|
||||
@ -307,7 +327,9 @@ class AssignmentPollManager(BaseManager):
|
||||
super()
|
||||
.get_prefetched_queryset(*args, **kwargs)
|
||||
.select_related("assignment")
|
||||
.prefetch_related("options", "options__user", "options__votes", "groups")
|
||||
.prefetch_related(
|
||||
"options", "options__user", "options__votes", "options__voted", "groups"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -379,15 +401,16 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
||||
abstain_sum += option.abstain
|
||||
return abstain_sum
|
||||
|
||||
def create_options(self):
|
||||
def create_options(self, skip_autoupdate=False):
|
||||
related_users = AssignmentRelatedUser.objects.filter(
|
||||
assignment__id=self.assignment.id
|
||||
).exclude(elected=True)
|
||||
|
||||
for related_user in related_users:
|
||||
AssignmentOption.objects.create(
|
||||
option = AssignmentOption(
|
||||
user=related_user.user, weight=related_user.weight, poll=self
|
||||
)
|
||||
option.save(skip_autoupdate=skip_autoupdate)
|
||||
|
||||
# Add all candidates to list of speakers of related agenda item
|
||||
if config["assignment_poll_add_candidates_to_list_of_speakers"]:
|
||||
@ -401,4 +424,5 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
||||
except OpenSlidesError:
|
||||
# The Speaker is already on the list. Do nothing.
|
||||
pass
|
||||
if not skip_autoupdate:
|
||||
inform_changed_data(self.assignment.list_of_speakers)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from ..users.projector import get_user_name
|
||||
from ..utils.projector import AllData, get_model, register_projector_slide
|
||||
from ..utils.projector import AllData, get_model, get_models, register_projector_slide
|
||||
from .models import AssignmentPoll
|
||||
|
||||
|
||||
@ -62,20 +62,27 @@ async def assignment_poll_slide(
|
||||
|
||||
# Add options:
|
||||
poll_data["options"] = []
|
||||
for option in sorted(poll["options"], key=lambda option: option["weight"]):
|
||||
option_data = {"user": await get_user_name(all_data, option["user_id"])}
|
||||
options = get_models(all_data, "assignments/assignment-option", poll["options_id"])
|
||||
for option in sorted(options, key=lambda option: option["weight"]):
|
||||
option_data: Dict[str, Any] = {
|
||||
"user": {"full_name": await get_user_name(all_data, option["user_id"])}
|
||||
}
|
||||
if poll["state"] == AssignmentPoll.STATE_PUBLISHED:
|
||||
option_data["yes"] = option["yes"]
|
||||
option_data["no"] = option["no"]
|
||||
option_data["abstain"] = option["abstain"]
|
||||
option_data["yes"] = float(option["yes"])
|
||||
option_data["no"] = float(option["no"])
|
||||
option_data["abstain"] = float(option["abstain"])
|
||||
poll_data["options"].append(option_data)
|
||||
|
||||
if poll["state"] == AssignmentPoll.STATE_PUBLISHED:
|
||||
poll_data["amount_global_no"] = poll["amount_global_no"]
|
||||
poll_data["amount_global_abstain"] = poll["amount_global_abstain"]
|
||||
poll_data["votesvalid"] = poll["votesvalid"]
|
||||
poll_data["votesinvalid"] = poll["votesinvalid"]
|
||||
poll_data["votescast"] = poll["votescast"]
|
||||
poll_data["amount_global_no"] = (
|
||||
float(poll["amount_global_no"]) if poll["amount_global_no"] else None
|
||||
)
|
||||
poll_data["amount_global_abstain"] = (
|
||||
float(poll["amount_global_abstain"]) if poll["amount_global_no"] else None
|
||||
)
|
||||
poll_data["votesvalid"] = float(poll["votesvalid"])
|
||||
poll_data["votesinvalid"] = float(poll["votesinvalid"])
|
||||
poll_data["votescast"] = float(poll["votescast"])
|
||||
|
||||
return {
|
||||
"assignment": {"title": assignment["title"]},
|
||||
|
@ -380,7 +380,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
vote_obj.save()
|
||||
poll.save()
|
||||
|
||||
def validate_vote_data(self, data, poll):
|
||||
def validate_vote_data(self, data, poll, user):
|
||||
"""
|
||||
Request data:
|
||||
analog:
|
||||
@ -511,6 +511,9 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
|
||||
options_data = data
|
||||
|
||||
db_option_ids = set(option.id for option in poll.get_options())
|
||||
data_option_ids = set(int(option_id) for option_id in options_data.keys())
|
||||
|
||||
# Just for named/pseudoanonymous with YN/YNA skip the all-options-given check
|
||||
if poll.type not in (
|
||||
AssignmentPoll.TYPE_NAMED,
|
||||
@ -520,12 +523,20 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
AssignmentPoll.POLLMETHOD_YNA,
|
||||
):
|
||||
# Check if all options were given
|
||||
db_option_ids = set(option.id for option in poll.get_options())
|
||||
data_option_ids = set(int(option_id) for option_id in options_data.keys())
|
||||
if data_option_ids != db_option_ids:
|
||||
raise ValidationError(
|
||||
{"error": "You have to provide values for all options"}
|
||||
)
|
||||
else:
|
||||
if not data_option_ids.issubset(db_option_ids):
|
||||
raise ValidationError(
|
||||
{
|
||||
"error": "You gave the following invalid option ids: "
|
||||
+ ", ".join(
|
||||
str(id) for id in data_option_ids.difference(db_option_ids)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
def create_votes_type_votes(self, data, poll, user):
|
||||
"""
|
||||
|
@ -882,8 +882,27 @@ class MotionVote(RESTModelMixin, BaseVote):
|
||||
default_permissions = ()
|
||||
|
||||
|
||||
class MotionOptionManager(BaseManager):
|
||||
"""
|
||||
Customized model manager to support our get_prefetched_queryset method.
|
||||
"""
|
||||
|
||||
def get_prefetched_queryset(self, *args, **kwargs):
|
||||
"""
|
||||
Returns the normal queryset with all voted users. In the background we
|
||||
join and prefetch all related models.
|
||||
"""
|
||||
return (
|
||||
super()
|
||||
.get_prefetched_queryset(*args, **kwargs)
|
||||
.select_related("poll")
|
||||
.prefetch_related("voted", "votes")
|
||||
)
|
||||
|
||||
|
||||
class MotionOption(RESTModelMixin, BaseOption):
|
||||
access_permissions = MotionOptionAccessPermissions()
|
||||
objects = MotionOptionManager()
|
||||
vote_class = MotionVote
|
||||
|
||||
poll = models.ForeignKey(
|
||||
@ -911,7 +930,7 @@ class MotionPollManager(BaseManager):
|
||||
super()
|
||||
.get_prefetched_queryset(*args, **kwargs)
|
||||
.select_related("motion")
|
||||
.prefetch_related("options", "options__votes", "groups")
|
||||
.prefetch_related("options", "options__votes", "options__voted", "groups")
|
||||
)
|
||||
|
||||
|
||||
|
@ -363,7 +363,16 @@ async def motion_poll_slide(
|
||||
}
|
||||
|
||||
if poll["state"] == MotionPoll.STATE_PUBLISHED:
|
||||
poll_data["options"] = poll["options"]
|
||||
option = get_model(
|
||||
all_data, "motions/motion-option", poll["options_id"][0]
|
||||
) # there can only be exactly one option
|
||||
poll_data["options"] = [
|
||||
{
|
||||
"yes": float(option["yes"]),
|
||||
"no": float(option["no"]),
|
||||
"abstain": float(option["abstain"]),
|
||||
}
|
||||
]
|
||||
poll_data["votesvalid"] = poll["votesvalid"]
|
||||
poll_data["votesinvalid"] = poll["votesinvalid"]
|
||||
poll_data["votescast"] = poll["votescast"]
|
||||
|
@ -1196,7 +1196,7 @@ class MotionPollViewSet(BasePollViewSet):
|
||||
|
||||
poll.save()
|
||||
|
||||
def validate_vote_data(self, data, poll):
|
||||
def validate_vote_data(self, data, poll, user):
|
||||
"""
|
||||
Request data for analog:
|
||||
{ "Y": <amount>, "N": <amount>, ["A": <amount>],
|
||||
@ -1223,23 +1223,28 @@ class MotionPollViewSet(BasePollViewSet):
|
||||
elif poll.pollmethod == MotionPoll.POLLMETHOD_YN and data not in ("Y", "N"):
|
||||
raise ValidationError("Data must be Y or N")
|
||||
|
||||
if poll.type == MotionPoll.TYPE_PSEUDOANONYMOUS:
|
||||
if user in poll.options.get().voted.all():
|
||||
raise ValidationError("You already voted on this poll")
|
||||
|
||||
def handle_named_vote(self, data, poll, user):
|
||||
option = poll.options.get()
|
||||
vote, _ = MotionVote.objects.get_or_create(user=user, option=option)
|
||||
self.set_vote_data(data, vote, poll)
|
||||
inform_changed_data(option)
|
||||
self.handle_named_or_pseudoanonymous_vote(data, poll, user, False)
|
||||
|
||||
def handle_pseudoanonymous_vote(self, data, poll):
|
||||
option = poll.options.get()
|
||||
vote = MotionVote.objects.create(option=option)
|
||||
self.set_vote_data(data, vote, poll)
|
||||
inform_changed_data(option)
|
||||
def handle_pseudoanonymous_vote(self, data, poll, user):
|
||||
self.handle_named_or_pseudoanonymous_vote(data, poll, user, True)
|
||||
|
||||
def set_vote_data(self, data, vote, poll):
|
||||
def handle_named_or_pseudoanonymous_vote(self, data, poll, user, pseudoanonymous):
|
||||
option = poll.options.get()
|
||||
vote, _ = MotionVote.objects.get_or_create(
|
||||
user=None if pseudoanonymous else user, option=option
|
||||
)
|
||||
vote.value = data
|
||||
vote.weight = Decimal("1")
|
||||
vote.save(no_delete_on_restriction=True)
|
||||
|
||||
option.voted.add(user)
|
||||
option.save()
|
||||
|
||||
|
||||
class MotionOptionViewSet(BaseOptionViewSet):
|
||||
queryset = MotionOption.objects.all()
|
||||
|
@ -222,19 +222,21 @@ class BasePoll(models.Model):
|
||||
votescast = property(get_votescast, set_votescast)
|
||||
|
||||
def get_user_ids_with_valid_votes(self):
|
||||
initial_option = self.get_options().first()
|
||||
if self.get_options().count():
|
||||
initial_option = self.get_options()[0]
|
||||
user_ids = set(map(lambda u: u.id, initial_option.voted.all()))
|
||||
for option in self.get_options():
|
||||
user_ids = user_ids.intersection(
|
||||
set(map(lambda u: u.id, option.voted.all()))
|
||||
)
|
||||
return list(user_ids)
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_all_voted_user_ids(self):
|
||||
# TODO: This might be faster with only one DB query using distinct.
|
||||
user_ids: Set[int] = set()
|
||||
for option in self.get_options():
|
||||
user_ids.update(option.voted.all().values_list("pk", flat=True))
|
||||
user_ids.update(map(lambda u: u.id, option.voted.all()))
|
||||
return list(user_ids)
|
||||
|
||||
def amount_valid_votes(self):
|
||||
@ -262,7 +264,7 @@ class BasePoll(models.Model):
|
||||
"""
|
||||
Returns the option objects for the poll.
|
||||
"""
|
||||
return self.get_option_class().objects.filter(poll=self)
|
||||
return self.options.all()
|
||||
|
||||
@classmethod
|
||||
def get_vote_class(cls):
|
||||
|
@ -103,7 +103,7 @@ class BasePollViewSet(ModelViewSet):
|
||||
# convert user ids to option ids
|
||||
self.convert_option_data(poll, vote_data)
|
||||
|
||||
self.validate_vote_data(vote_data, poll)
|
||||
self.validate_vote_data(vote_data, poll, request.user)
|
||||
self.handle_analog_vote(vote_data, poll, request.user)
|
||||
|
||||
if request.data.get("publish_immediately"):
|
||||
@ -198,7 +198,7 @@ class BasePollViewSet(ModelViewSet):
|
||||
self.assert_can_vote(poll, request)
|
||||
|
||||
data = request.data
|
||||
self.validate_vote_data(data, poll)
|
||||
self.validate_vote_data(data, poll, request.user)
|
||||
|
||||
if poll.type == BasePoll.TYPE_ANALOG:
|
||||
self.handle_analog_vote(data, poll, request.user)
|
||||
@ -258,7 +258,7 @@ class BasePollViewSet(ModelViewSet):
|
||||
"""
|
||||
pass
|
||||
|
||||
def validate_vote_data(self, data, poll):
|
||||
def validate_vote_data(self, data, poll, user):
|
||||
"""
|
||||
To be implemented by subclass. Validates the data according to poll type and method and fields by validated versions.
|
||||
Raises ValidationError on failure
|
||||
|
@ -115,3 +115,12 @@ def get_model(all_data: AllData, collection: str, id: Any) -> Dict[str, Any]:
|
||||
except KeyError:
|
||||
raise ProjectorElementException(f"{collection} with id {id} does not exist")
|
||||
return model
|
||||
|
||||
|
||||
def get_models(
|
||||
all_data: AllData, collection: str, ids: List[Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Tries to fetch all given models. Models are required to be all of the collection `collection`.
|
||||
"""
|
||||
return [get_model(all_data, collection, id) for id in ids]
|
||||
|
@ -51,19 +51,23 @@ def test_assignment_vote_db_queries():
|
||||
@pytest.mark.django_db(transaction=False)
|
||||
def test_assignment_option_db_queries():
|
||||
"""
|
||||
Tests that only 1 query is done when fetching AssignmentOptions
|
||||
Tests that only the following db queries are done:
|
||||
* 1 request to get the options,
|
||||
* 1 request to get all users that voted on the options,
|
||||
* 1 request to get all votes for all options,
|
||||
= 3 queries
|
||||
"""
|
||||
create_assignment_polls()
|
||||
assert count_queries(AssignmentOption.get_elements)() == 1
|
||||
assert count_queries(AssignmentOption.get_elements)() == 3
|
||||
|
||||
|
||||
def create_assignment_polls():
|
||||
"""
|
||||
Creates 1 assignment with 3 candidates which has 5 polls in which each candidate got a random amount of votes between 0 and 10 from 3 users
|
||||
"""
|
||||
assignment = Assignment.objects.create(
|
||||
title="test_assignment_ohneivoh9caiB8Yiungo", open_posts=1
|
||||
)
|
||||
assignment = Assignment(title="test_assignment_ohneivoh9caiB8Yiungo", open_posts=1)
|
||||
assignment.save(skip_autoupdate=True)
|
||||
|
||||
group1 = get_group_model().objects.get(pk=1)
|
||||
group2 = get_group_model().objects.get(pk=2)
|
||||
for i in range(3):
|
||||
@ -73,13 +77,14 @@ def create_assignment_polls():
|
||||
assignment.add_candidate(user)
|
||||
|
||||
for i in range(5):
|
||||
poll = AssignmentPoll.objects.create(
|
||||
poll = AssignmentPoll(
|
||||
assignment=assignment,
|
||||
title="test_title_UnMiGzEHmwqplmVBPNEZ",
|
||||
pollmethod=AssignmentPoll.POLLMETHOD_YN,
|
||||
type=AssignmentPoll.TYPE_NAMED,
|
||||
)
|
||||
poll.create_options()
|
||||
poll.save(skip_autoupdate=True)
|
||||
poll.create_options(skip_autoupdate=True)
|
||||
poll.groups.add(group1)
|
||||
poll.groups.add(group2)
|
||||
|
||||
@ -94,7 +99,7 @@ def create_assignment_polls():
|
||||
AssignmentVote.objects.create(
|
||||
user=user, option=option, value="Y", weight=Decimal(weight)
|
||||
)
|
||||
poll.voted.add(user)
|
||||
option.voted.add(user)
|
||||
|
||||
|
||||
class CreateAssignmentPoll(TestCase):
|
||||
@ -105,7 +110,7 @@ class CreateAssignmentPoll(TestCase):
|
||||
self.assignment.add_candidate(self.admin)
|
||||
|
||||
def test_simple(self):
|
||||
with self.assertNumQueries(41):
|
||||
with self.assertNumQueries(50):
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-list"),
|
||||
{
|
||||
@ -1018,7 +1023,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_too_few_options(self):
|
||||
def test_partial_vote(self):
|
||||
self.add_candidate()
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
@ -1026,8 +1031,8 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
{"1": "Y"},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertTrue(AssignmentPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_wrong_options(self):
|
||||
self.add_candidate()
|
||||
@ -1082,7 +1087,9 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
def test_missing_data(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertHttpStatusVerbose(
|
||||
response, status.HTTP_200_OK
|
||||
) # new "feature" because of partial requests: empty requests work!
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
def test_wrong_data_format(self):
|
||||
@ -1469,7 +1476,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
{"1": "N"},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
option1 = poll.options.get(pk=1)
|
||||
self.assertEqual(option1.yes, Decimal("1"))
|
||||
@ -1486,7 +1493,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_too_few_options(self):
|
||||
def test_partial_vote(self):
|
||||
self.add_candidate()
|
||||
self.start_poll()
|
||||
response = self.client.post(
|
||||
@ -1494,8 +1501,8 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
{"1": "Y"},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertFalse(AssignmentPoll.objects.get().get_votes().exists())
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertTrue(AssignmentPoll.objects.get().get_votes().exists())
|
||||
|
||||
def test_wrong_options(self):
|
||||
self.add_candidate()
|
||||
@ -1550,7 +1557,9 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
def test_missing_data(self):
|
||||
self.start_poll()
|
||||
response = self.client.post(reverse("assignmentpoll-vote", args=[self.poll.pk]))
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertHttpStatusVerbose(
|
||||
response, status.HTTP_200_OK
|
||||
) # new "feature" because of partial requests: empty requests work!
|
||||
self.assertFalse(AssignmentVote.objects.exists())
|
||||
|
||||
def test_wrong_data_format(self):
|
||||
@ -1658,7 +1667,7 @@ class VoteAssignmentPollPseudoanonymousVotes(VoteAssignmentPollBaseTestClass):
|
||||
{"1": 0, "2": 1},
|
||||
format="json",
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
option1 = poll.options.get(pk=1)
|
||||
option2 = poll.options.get(pk=2)
|
||||
@ -1896,13 +1905,23 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"type": AssignmentPoll.TYPE_NAMED,
|
||||
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
|
||||
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
"votes_amount": 1,
|
||||
"votescast": "1.000000",
|
||||
"votesinvalid": "0.000000",
|
||||
"votesvalid": "1.000000",
|
||||
},
|
||||
"assignments/assignment-option:1": {
|
||||
"abstain": "1.000000",
|
||||
"id": 1,
|
||||
"no": "0.000000",
|
||||
"poll_id": 1,
|
||||
"pollstate": AssignmentPoll.STATE_STARTED,
|
||||
"yes": "0.000000",
|
||||
"user_id": 1,
|
||||
"weight": 1,
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"assignments/assignment-vote:1": {
|
||||
"id": 1,
|
||||
"option_id": 1,
|
||||
@ -1951,7 +1970,6 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"groups_id": [GROUP_DELEGATE_PK],
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
"votes_amount": 1,
|
||||
},
|
||||
)
|
||||
@ -1966,7 +1984,7 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
vote.value = "A"
|
||||
vote.weight = Decimal("1")
|
||||
vote.save(no_delete_on_restriction=True, skip_autoupdate=True)
|
||||
self.poll.voted.add(self.user.id)
|
||||
option.voted.add(self.user.id)
|
||||
self.poll.state = AssignmentPoll.STATE_FINISHED
|
||||
self.poll.save(skip_autoupdate=True)
|
||||
response = self.client.post(
|
||||
@ -2004,8 +2022,6 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"state": 4,
|
||||
"title": self.poll.title,
|
||||
"type": "named",
|
||||
"user_has_voted": user == self.user,
|
||||
"voted_id": [self.user.id],
|
||||
"votes_amount": 1,
|
||||
"votescast": "1.000000",
|
||||
"votesinvalid": "0.000000",
|
||||
@ -2028,6 +2044,8 @@ class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass)
|
||||
"yes": "0.000000",
|
||||
"user_id": 1,
|
||||
"weight": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -2067,13 +2085,23 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
|
||||
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
|
||||
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
"votes_amount": 1,
|
||||
"votescast": "1.000000",
|
||||
"votesinvalid": "0.000000",
|
||||
"votesvalid": "1.000000",
|
||||
},
|
||||
"assignments/assignment-option:1": {
|
||||
"abstain": "1.000000",
|
||||
"id": 1,
|
||||
"no": "0.000000",
|
||||
"poll_id": 1,
|
||||
"pollstate": AssignmentPoll.STATE_STARTED,
|
||||
"yes": "0.000000",
|
||||
"user_id": 1,
|
||||
"weight": 1,
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
"assignments/assignment-vote:1": {
|
||||
"id": 1,
|
||||
"option_id": 1,
|
||||
@ -2108,7 +2136,6 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"groups_id": [GROUP_DELEGATE_PK],
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
"votes_amount": 1,
|
||||
},
|
||||
)
|
||||
@ -2122,7 +2149,7 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
vote.value = "A"
|
||||
vote.weight = Decimal("1")
|
||||
vote.save(no_delete_on_restriction=True, skip_autoupdate=True)
|
||||
self.poll.voted.add(self.user.id)
|
||||
option.voted.add(self.user.id)
|
||||
self.poll.state = AssignmentPoll.STATE_FINISHED
|
||||
self.poll.save(skip_autoupdate=True)
|
||||
response = self.client.post(
|
||||
@ -2160,8 +2187,6 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"state": 4,
|
||||
"title": self.poll.title,
|
||||
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
|
||||
"user_has_voted": user == self.user,
|
||||
"voted_id": [self.user.id],
|
||||
"votes_amount": 1,
|
||||
"votescast": "1.000000",
|
||||
"votesinvalid": "0.000000",
|
||||
@ -2184,6 +2209,8 @@ class VoteAssignmentPollPseudoanonymousAutoupdates(
|
||||
"yes": "0.000000",
|
||||
"user_id": 1,
|
||||
"weight": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -24,6 +24,7 @@ def test_assignment_db_queries():
|
||||
* 1 request to get the tags,
|
||||
* 1 request to get the attachments and
|
||||
* 1 Request to get the polls of the assignment
|
||||
* 1 Request to get the options of these polls
|
||||
"""
|
||||
for index in range(10):
|
||||
assignment = Assignment.objects.create(title=f"assignment{index}", open_posts=1)
|
||||
@ -35,7 +36,7 @@ def test_assignment_db_queries():
|
||||
type=AssignmentPoll.TYPE_NAMED,
|
||||
)
|
||||
|
||||
assert count_queries(Assignment.get_elements)() == 7
|
||||
assert count_queries(Assignment.get_elements)() == 8
|
||||
|
||||
|
||||
class CreateAssignment(TestCase):
|
||||
|
@ -109,7 +109,7 @@ class CreateMotion(TestCase):
|
||||
The created motion should have an identifier and the admin user should
|
||||
be the submitter.
|
||||
"""
|
||||
with self.assertNumQueries(51, verbose=True):
|
||||
with self.assertNumQueries(51):
|
||||
response = self.client.post(
|
||||
reverse("motion-list"),
|
||||
{
|
||||
|
@ -44,10 +44,14 @@ def test_motion_vote_db_queries():
|
||||
@pytest.mark.django_db(transaction=False)
|
||||
def test_motion_option_db_queries():
|
||||
"""
|
||||
Tests that only 1 query is done when fetching MotionOptions
|
||||
Tests that only the following db queries are done:
|
||||
* 1 request to get the options,
|
||||
* 1 request to get all votes for all options,
|
||||
* 1 request to get all users that voted on the options
|
||||
= 5 queries
|
||||
"""
|
||||
create_motion_polls()
|
||||
assert count_queries(MotionOption.get_elements)() == 1
|
||||
assert count_queries(MotionOption.get_elements)() == 3
|
||||
|
||||
|
||||
def create_motion_polls():
|
||||
@ -79,7 +83,7 @@ def create_motion_polls():
|
||||
value=("Y" if k == 0 else "N"),
|
||||
weight=Decimal(1),
|
||||
)
|
||||
poll.voted.add(user)
|
||||
option.voted.add(user)
|
||||
|
||||
|
||||
class CreateMotionPoll(TestCase):
|
||||
@ -157,12 +161,10 @@ class CreateMotionPoll(TestCase):
|
||||
"onehundred_percent_base": MotionPoll.PERCENT_BASE_YN,
|
||||
"majority_method": MotionPoll.MAJORITY_SIMPLE,
|
||||
"groups_id": [],
|
||||
"user_has_voted": False,
|
||||
"votesvalid": "0.000000",
|
||||
"votesinvalid": "0.000000",
|
||||
"votescast": "0.000000",
|
||||
"options_id": [1],
|
||||
"voted_id": [],
|
||||
"id": 1,
|
||||
},
|
||||
)
|
||||
@ -754,7 +756,6 @@ class VoteMotionPollNamed(TestCase):
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
self.assertEqual(poll.get_votes().count(), 1)
|
||||
self.assertEqual(poll.count_users_voted(), 1)
|
||||
option = poll.options.get()
|
||||
self.assertEqual(option.yes, Decimal("0"))
|
||||
self.assertEqual(option.no, Decimal("1"))
|
||||
@ -779,7 +780,6 @@ class VoteMotionPollNamed(TestCase):
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
self.assertEqual(poll.get_votes().count(), 1)
|
||||
self.assertEqual(poll.count_users_voted(), 1)
|
||||
option = poll.options.get()
|
||||
self.assertEqual(option.yes, Decimal("0"))
|
||||
self.assertEqual(option.no, Decimal("0"))
|
||||
@ -905,12 +905,10 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
"onehundred_percent_base": "YN",
|
||||
"majority_method": "simple",
|
||||
"groups_id": [GROUP_DELEGATE_PK],
|
||||
"user_has_voted": False,
|
||||
"votesvalid": "1.000000",
|
||||
"votesinvalid": "0.000000",
|
||||
"votescast": "1.000000",
|
||||
"options_id": [1],
|
||||
"voted_id": [self.user.id],
|
||||
"id": 1,
|
||||
},
|
||||
"motions/motion-vote:1": {
|
||||
@ -928,6 +926,8 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
"poll_id": 1,
|
||||
"pollstate": 2,
|
||||
"yes": "0.000000",
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -948,7 +948,7 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
)
|
||||
self.assertEqual(
|
||||
autoupdate[0]["motions/motion-option:1"],
|
||||
{"id": 1, "poll_id": 1, "pollstate": 2},
|
||||
{"id": 1, "poll_id": 1, "pollstate": 2, "user_has_voted": True},
|
||||
)
|
||||
self.assertEqual(autoupdate[1], [])
|
||||
|
||||
@ -969,12 +969,16 @@ class VoteMotionPollNamedAutoupdates(TestCase):
|
||||
"groups_id": [GROUP_DELEGATE_PK],
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
autoupdate[0]["motions/motion-option:1"],
|
||||
{"id": 1, "poll_id": 1, "pollstate": 2},
|
||||
{
|
||||
"id": 1,
|
||||
"poll_id": 1,
|
||||
"pollstate": 2,
|
||||
"user_has_voted": user == self.user,
|
||||
},
|
||||
)
|
||||
|
||||
# Other users should not get a vote autoupdate
|
||||
@ -1040,12 +1044,10 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
||||
"onehundred_percent_base": "YN",
|
||||
"majority_method": "simple",
|
||||
"groups_id": [GROUP_DELEGATE_PK],
|
||||
"user_has_voted": False,
|
||||
"votesvalid": "1.000000",
|
||||
"votesinvalid": "0.000000",
|
||||
"votescast": "1.000000",
|
||||
"options_id": [1],
|
||||
"voted_id": [self.user.id],
|
||||
"id": 1,
|
||||
},
|
||||
"motions/motion-vote:1": {
|
||||
@ -1063,6 +1065,8 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
||||
"poll_id": 1,
|
||||
"pollstate": 2,
|
||||
"yes": "0.000000",
|
||||
"user_has_voted": False,
|
||||
"voted_id": [self.user.id],
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -1086,7 +1090,6 @@ class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
||||
"groups_id": [GROUP_DELEGATE_PK],
|
||||
"options_id": [1],
|
||||
"id": 1,
|
||||
"user_has_voted": user == self.user,
|
||||
},
|
||||
)
|
||||
|
||||
@ -1150,12 +1153,12 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
self.assertEqual(poll.get_votes().count(), 1)
|
||||
self.assertEqual(poll.count_users_voted(), 1)
|
||||
self.assertTrue(self.admin in poll.voted.all())
|
||||
self.assertEqual(poll.amount_valid_votes(), 1)
|
||||
option = poll.options.get()
|
||||
self.assertEqual(option.yes, Decimal("0"))
|
||||
self.assertEqual(option.no, Decimal("1"))
|
||||
self.assertEqual(option.abstain, Decimal("0"))
|
||||
self.assertTrue(self.admin in option.voted.all())
|
||||
vote = option.votes.get()
|
||||
self.assertEqual(vote.user, None)
|
||||
|
||||
@ -1170,7 +1173,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
||||
response = self.client.post(
|
||||
reverse("motionpoll-vote", args=[self.poll.pk]), "A"
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_403_FORBIDDEN)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
option = MotionPoll.objects.get().options.get()
|
||||
self.assertEqual(option.yes, Decimal("0"))
|
||||
self.assertEqual(option.no, Decimal("1"))
|
||||
@ -1303,12 +1306,10 @@ class PublishMotionPoll(TestCase):
|
||||
"onehundred_percent_base": "YN",
|
||||
"majority_method": "simple",
|
||||
"groups_id": [],
|
||||
"user_has_voted": False,
|
||||
"votesvalid": "0.000000",
|
||||
"votesinvalid": "0.000000",
|
||||
"votescast": "0.000000",
|
||||
"options_id": [1],
|
||||
"voted_id": [],
|
||||
"id": 1,
|
||||
},
|
||||
"motions/motion-vote:1": {
|
||||
@ -1326,6 +1327,8 @@ class PublishMotionPoll(TestCase):
|
||||
"poll_id": 1,
|
||||
"pollstate": 4,
|
||||
"yes": "0.000000",
|
||||
"user_has_voted": False,
|
||||
"voted_id": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -1359,12 +1362,12 @@ class PseudoanonymizeMotionPoll(TestCase):
|
||||
self.vote1 = MotionVote.objects.create(
|
||||
user=self.user1, option=self.option, value="Y", weight=Decimal(1)
|
||||
)
|
||||
self.poll.voted.add(self.user1)
|
||||
self.option.voted.add(self.user1)
|
||||
self.user2, _ = self.create_user()
|
||||
self.vote2 = MotionVote.objects.create(
|
||||
user=self.user2, option=self.option, value="N", weight=Decimal(1)
|
||||
)
|
||||
self.poll.voted.add(self.user2)
|
||||
self.option.voted.add(self.user2)
|
||||
|
||||
def test_pseudoanonymize_poll(self):
|
||||
response = self.client.post(
|
||||
@ -1373,16 +1376,16 @@ class PseudoanonymizeMotionPoll(TestCase):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
self.assertEqual(poll.get_votes().count(), 2)
|
||||
self.assertEqual(poll.count_users_voted(), 2)
|
||||
self.assertEqual(poll.amount_valid_votes(), 2)
|
||||
self.assertEqual(poll.votesvalid, Decimal("2"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("2"))
|
||||
self.assertTrue(self.user1 in poll.voted.all())
|
||||
self.assertTrue(self.user2 in poll.voted.all())
|
||||
option = poll.options.get()
|
||||
self.assertEqual(option.yes, Decimal("1"))
|
||||
self.assertEqual(option.no, Decimal("1"))
|
||||
self.assertEqual(option.abstain, Decimal("0"))
|
||||
self.assertTrue(self.user1 in option.voted.all())
|
||||
self.assertTrue(self.user2 in option.voted.all())
|
||||
for vote in poll.get_votes().all():
|
||||
self.assertTrue(vote.user is None)
|
||||
|
||||
@ -1429,19 +1432,19 @@ class ResetMotionPoll(TestCase):
|
||||
self.vote1 = MotionVote.objects.create(
|
||||
user=self.user1, option=self.option, value="Y", weight=Decimal(1)
|
||||
)
|
||||
self.poll.voted.add(self.user1)
|
||||
self.option.voted.add(self.user1)
|
||||
self.user2, _ = self.create_user()
|
||||
self.vote2 = MotionVote.objects.create(
|
||||
user=self.user2, option=self.option, value="N", weight=Decimal(1)
|
||||
)
|
||||
self.poll.voted.add(self.user2)
|
||||
self.option.voted.add(self.user2)
|
||||
|
||||
def test_reset_poll(self):
|
||||
response = self.client.post(reverse("motionpoll-reset", args=[self.poll.pk]))
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
self.assertEqual(poll.get_votes().count(), 0)
|
||||
self.assertEqual(poll.count_users_voted(), 0)
|
||||
self.assertEqual(poll.amount_valid_votes(), 0)
|
||||
self.assertEqual(poll.votesvalid, None)
|
||||
self.assertEqual(poll.votesinvalid, None)
|
||||
self.assertEqual(poll.votescast, None)
|
||||
@ -1468,4 +1471,4 @@ class ResetMotionPoll(TestCase):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
poll = MotionPoll.objects.get()
|
||||
self.assertTrue(poll.get_votes().exists())
|
||||
self.assertEqual(poll.count_users_voted(), 2)
|
||||
self.assertEqual(poll.amount_valid_votes(), 2)
|
||||
|
Loading…
Reference in New Issue
Block a user