Merge pull request #5978 from jsangmeister/voting-changes
Implement voting changes
This commit is contained in:
commit
787390c899
9
Makefile
9
Makefile
@ -4,15 +4,20 @@ build-dev:
|
|||||||
docker-compose -f docker/docker-compose.dev.yml build
|
docker-compose -f docker/docker-compose.dev.yml build
|
||||||
|
|
||||||
run-dev: | build-dev
|
run-dev: | build-dev
|
||||||
UID=$$(id -u $${USER}) GID=$$(id -g $${USER}) docker-compose -f docker/docker-compose.dev.yml up
|
USER_ID=$$(id -u $${USER}) GROUP_ID=$$(id -g $${USER}) docker-compose -f docker/docker-compose.dev.yml up
|
||||||
|
|
||||||
stop-dev:
|
stop-dev:
|
||||||
docker-compose -f docker/docker-compose.dev.yml down
|
docker-compose -f docker/docker-compose.dev.yml down
|
||||||
|
|
||||||
server-shell:
|
server-shell:
|
||||||
docker-compose -f docker/docker-compose.dev.yml run --entrypoint="" server docker/wait-for-dev-dependencies.sh
|
docker-compose -f docker/docker-compose.dev.yml run --entrypoint="" server docker/wait-for-dev-dependencies.sh
|
||||||
UID=$$(id -u $${USER}) GID=$$(id -g $${USER}) docker-compose -f docker/docker-compose.dev.yml run --entrypoint="" server bash
|
USER_ID=$$(id -u $${USER}) GROUP_ID=$$(id -g $${USER}) docker-compose -f docker/docker-compose.dev.yml run --entrypoint="" server bash
|
||||||
docker-compose -f docker/docker-compose.dev.yml down
|
docker-compose -f docker/docker-compose.dev.yml down
|
||||||
|
|
||||||
reload-proxy:
|
reload-proxy:
|
||||||
docker-compose -f docker/docker-compose.dev.yml exec -w /etc/caddy proxy caddy reload
|
docker-compose -f docker/docker-compose.dev.yml exec -w /etc/caddy proxy caddy reload
|
||||||
|
|
||||||
|
clear-cache:
|
||||||
|
docker-compose -f docker/docker-compose.dev.yml exec redis redis-cli flushall
|
||||||
|
docker-compose -f docker/docker-compose.dev.yml restart autoupdate
|
||||||
|
docker-compose -f docker/docker-compose.dev.yml restart server
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 0a2eb0fce664bdb76eb47beadf0f7c383e40e709
|
Subproject commit c1211219d81965b10780ecfa5a1de31f9e30d31e
|
@ -49,6 +49,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="isPercentBaseEntitled" class="entitled-users-row">
|
||||||
|
<td>{{ 'Entitled users' | translate }}</td>
|
||||||
|
<td class="result">
|
||||||
|
<div class="single-result">
|
||||||
|
<span>
|
||||||
|
{{ poll.entitled_users_at_stop.length | pollPercentBase: poll:'assignment' }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ poll.entitled_users_at_stop.length }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -46,4 +46,9 @@
|
|||||||
.single-result {
|
.single-result {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entitled-users-row {
|
||||||
|
border-bottom: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { BaseComponent } from 'app/base.component';
|
import { BaseComponent } from 'app/base.component';
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { PollState } from 'app/shared/models/poll/base-poll';
|
import { PercentBase, PollState } from 'app/shared/models/poll/base-poll';
|
||||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
import { AssignmentPollService } from 'app/site/assignments/modules/assignment-poll/services/assignment-poll.service';
|
import { AssignmentPollService } from 'app/site/assignments/modules/assignment-poll/services/assignment-poll.service';
|
||||||
import { PollData, PollTableData, VotingResult } from 'app/site/polls/services/poll.service';
|
import { PollData, PollTableData, VotingResult } from 'app/site/polls/services/poll.service';
|
||||||
@ -72,6 +72,10 @@ export class AssignmentPollDetailContentComponent extends BaseComponent {
|
|||||||
return this.operator.hasPerms(this.permission.assignmentsCanManage) || this.isPublished;
|
return this.operator.hasPerms(this.permission.assignmentsCanManage) || this.isPublished;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get isPercentBaseEntitled(): boolean {
|
||||||
|
return this.poll.onehundred_percent_base === PercentBase.Entitled;
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
titleService: Title,
|
titleService: Title,
|
||||||
translateService: TranslateService,
|
translateService: TranslateService,
|
||||||
|
@ -30,6 +30,15 @@
|
|||||||
{{ row.value[0].amount | parsePollNumber }}
|
{{ row.value[0].amount | parsePollNumber }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr *ngIf="isPercentBaseEntitled" class="entitled-users-row">
|
||||||
|
<td>{{ 'Entitled users' | translate }}</td>
|
||||||
|
<td class="result-cell-definition">
|
||||||
|
{{ poll.entitled_users_at_stop.length | pollPercentBase: poll:'motion' }}
|
||||||
|
</td>
|
||||||
|
<td class="result-cell-definition">
|
||||||
|
{{ poll.entitled_users_at_stop.length }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
|
|
||||||
import { BaseComponent } from 'app/base.component';
|
import { BaseComponent } from 'app/base.component';
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { PollState } from 'app/shared/models/poll/base-poll';
|
import { PercentBase, PollState } from 'app/shared/models/poll/base-poll';
|
||||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
||||||
import { PollData, PollTableData } from 'app/site/polls/services/poll.service';
|
import { PollData, PollTableData } from 'app/site/polls/services/poll.service';
|
||||||
@ -58,6 +58,10 @@ export class MotionPollDetailContentComponent extends BaseComponent {
|
|||||||
return this.operator.hasPerms(this.permission.motionsCanManagePolls) || this.isPublished;
|
return this.operator.hasPerms(this.permission.motionsCanManagePolls) || this.isPublished;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get isPercentBaseEntitled(): boolean {
|
||||||
|
return this.poll.onehundred_percent_base === PercentBase.Entitled;
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
titleService: Title,
|
titleService: Title,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
|
@ -15,6 +15,7 @@ export enum AssignmentPollPercentBase {
|
|||||||
YNA = 'YNA',
|
YNA = 'YNA',
|
||||||
Valid = 'valid',
|
Valid = 'valid',
|
||||||
Cast = 'cast',
|
Cast = 'cast',
|
||||||
|
Entitled = 'entitled',
|
||||||
Disabled = 'disabled'
|
Disabled = 'disabled'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,9 +35,16 @@ export enum PercentBase {
|
|||||||
YNA = 'YNA',
|
YNA = 'YNA',
|
||||||
Valid = 'valid',
|
Valid = 'valid',
|
||||||
Cast = 'cast',
|
Cast = 'cast',
|
||||||
|
Entitled = 'entitled',
|
||||||
Disabled = 'disabled'
|
Disabled = 'disabled'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EntitledUsersEntry {
|
||||||
|
user_id: number;
|
||||||
|
voted: boolean;
|
||||||
|
vote_delegated_to_id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const VOTE_MAJORITY = -1;
|
export const VOTE_MAJORITY = -1;
|
||||||
export const VOTE_UNDOCUMENTED = -2;
|
export const VOTE_UNDOCUMENTED = -2;
|
||||||
export const LOWEST_VOTE_VALUE = VOTE_UNDOCUMENTED;
|
export const LOWEST_VOTE_VALUE = VOTE_UNDOCUMENTED;
|
||||||
@ -61,6 +68,8 @@ export abstract class BasePoll<
|
|||||||
public user_has_voted_for_delegations: number[];
|
public user_has_voted_for_delegations: number[];
|
||||||
public pollmethod: PM;
|
public pollmethod: PM;
|
||||||
public onehundred_percent_base: PB;
|
public onehundred_percent_base: PB;
|
||||||
|
public is_pseudoanonymized: boolean;
|
||||||
|
public entitled_users_at_stop: EntitledUsersEntry[];
|
||||||
|
|
||||||
public get isCreated(): boolean {
|
public get isCreated(): boolean {
|
||||||
return this.state === PollState.Created;
|
return this.state === PollState.Created;
|
||||||
|
@ -28,6 +28,7 @@ export abstract class BaseVote<T = any> extends BaseDecimalModel<T> {
|
|||||||
public value: VoteValue;
|
public value: VoteValue;
|
||||||
public option_id: number;
|
public option_id: number;
|
||||||
public user_id?: number;
|
public user_id?: number;
|
||||||
|
public user_token: string;
|
||||||
|
|
||||||
public get valueVerbose(): string {
|
public get valueVerbose(): string {
|
||||||
return VoteValueVerbose[this.value];
|
return VoteValueVerbose[this.value];
|
||||||
|
@ -115,6 +115,7 @@ import { ChartsComponent } from './components/charts/charts.component';
|
|||||||
import { CheckInputComponent } from './components/check-input/check-input.component';
|
import { CheckInputComponent } from './components/check-input/check-input.component';
|
||||||
import { BannerComponent } from './components/banner/banner.component';
|
import { BannerComponent } from './components/banner/banner.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 { EntitledUsersTableComponent } from 'app/site/polls/components/entitled-users-table/entitled-users-table.component';
|
||||||
import { MotionPollDialogComponent } from 'app/site/motions/modules/motion-poll/motion-poll-dialog/motion-poll-dialog.component';
|
import { MotionPollDialogComponent } from 'app/site/motions/modules/motion-poll/motion-poll-dialog/motion-poll-dialog.component';
|
||||||
import { ParsePollNumberPipe } from './pipes/parse-poll-number.pipe';
|
import { ParsePollNumberPipe } from './pipes/parse-poll-number.pipe';
|
||||||
import { ReversePipe } from './pipes/reverse.pipe';
|
import { ReversePipe } from './pipes/reverse.pipe';
|
||||||
@ -293,6 +294,7 @@ import { ApplauseParticleDisplayComponent } from './components/applause-particle
|
|||||||
CheckInputComponent,
|
CheckInputComponent,
|
||||||
BannerComponent,
|
BannerComponent,
|
||||||
PollFormComponent,
|
PollFormComponent,
|
||||||
|
EntitledUsersTableComponent,
|
||||||
MotionPollDialogComponent,
|
MotionPollDialogComponent,
|
||||||
ParsePollNumberPipe,
|
ParsePollNumberPipe,
|
||||||
ReversePipe,
|
ReversePipe,
|
||||||
@ -356,6 +358,7 @@ import { ApplauseParticleDisplayComponent } from './components/applause-particle
|
|||||||
CheckInputComponent,
|
CheckInputComponent,
|
||||||
BannerComponent,
|
BannerComponent,
|
||||||
PollFormComponent,
|
PollFormComponent,
|
||||||
|
EntitledUsersTableComponent,
|
||||||
MotionPollDialogComponent,
|
MotionPollDialogComponent,
|
||||||
ParsePollNumberPipe,
|
ParsePollNumberPipe,
|
||||||
ReversePipe,
|
ReversePipe,
|
||||||
|
@ -30,6 +30,7 @@ export const AssignmentPollPercentBaseVerbose = {
|
|||||||
YNA: _('Yes/No/Abstain per candidate'),
|
YNA: _('Yes/No/Abstain per candidate'),
|
||||||
valid: _('All valid ballots'),
|
valid: _('All valid ballots'),
|
||||||
cast: _('All casted ballots'),
|
cast: _('All casted ballots'),
|
||||||
|
entitled: _('All entitled users'),
|
||||||
disabled: _('Disabled (no percents)')
|
disabled: _('Disabled (no percents)')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,9 +37,10 @@
|
|||||||
<os-charts class="assignment-result-chart" [labels]="candidatesLabels" [data]="chartData"></os-charts>
|
<os-charts class="assignment-result-chart" [labels]="candidatesLabels" [data]="chartData"></os-charts>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<mat-tab-group *ngIf="showResults && poll.stateHasVotes && poll.isEVoting">
|
||||||
|
<mat-tab label="{{ 'Single votes' | translate }}">
|
||||||
<!-- Single Votes Table -->
|
<!-- Single Votes Table -->
|
||||||
<div class="named-result-table" *ngIf="showResults && poll.stateHasVotes && poll.type === 'named'">
|
<div class="named-result-table">
|
||||||
<h3>{{ 'Single votes' | translate }}</h3>
|
|
||||||
<os-list-view-table
|
<os-list-view-table
|
||||||
class="single-votes-table"
|
class="single-votes-table"
|
||||||
*ngIf="votesDataObservable"
|
*ngIf="votesDataObservable"
|
||||||
@ -86,7 +87,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!vote.user">
|
<div *ngIf="!vote.user">
|
||||||
{{ 'Anonymous' | translate }}
|
<i *ngIf="poll.is_pseudoanonymized">{{ "Anonymous" | translate }}</i>
|
||||||
|
<i *ngIf="!poll.is_pseudoanonymized">{{ "Deleted user" | translate }}</i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -94,10 +96,15 @@
|
|||||||
<div class="single-vote-result" *ngFor="let candidate of vote.votes">{{ candidate }}</div>
|
<div class="single-vote-result" *ngFor="let candidate of vote.votes">{{ candidate }}</div>
|
||||||
</div>
|
</div>
|
||||||
</os-list-view-table>
|
</os-list-view-table>
|
||||||
<div *ngIf="!votesDataObservable">
|
|
||||||
{{ 'The individual votes were anonymized.' | translate }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
<mat-tab label="{{ 'Entitled users' | translate }}">
|
||||||
|
<os-entitled-users-table
|
||||||
|
[entitledUsersObservable]="entitledUsersObservable"
|
||||||
|
[listStorageKey]="assignment-poll-entitled-users"
|
||||||
|
></os-entitled-users-table>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
|||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { PblColumnDefinition } from '@pebula/ngrid';
|
import { PblColumnDefinition } from '@pebula/ngrid';
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ import { OperatorService, Permission } from 'app/core/core-services/operator.ser
|
|||||||
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 { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
import { GroupRepositoryService } from 'app/core/repositories/users/group-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 { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { ChartData } from 'app/shared/components/charts/charts.component';
|
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||||
@ -62,7 +64,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponentDirect
|
|||||||
votesRepo: AssignmentVoteRepositoryService,
|
votesRepo: AssignmentVoteRepositoryService,
|
||||||
protected operator: OperatorService,
|
protected operator: OperatorService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
protected cd: ChangeDetectorRef
|
protected cd: ChangeDetectorRef,
|
||||||
|
protected userRepo: UserRepositoryService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
title,
|
title,
|
||||||
@ -76,7 +79,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponentDirect
|
|||||||
pollService,
|
pollService,
|
||||||
votesRepo,
|
votesRepo,
|
||||||
operator,
|
operator,
|
||||||
cd
|
cd,
|
||||||
|
userRepo
|
||||||
);
|
);
|
||||||
configService
|
configService
|
||||||
.get<boolean>('users_activate_vote_weight')
|
.get<boolean>('users_activate_vote_weight')
|
||||||
@ -100,14 +104,14 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponentDirect
|
|||||||
];
|
];
|
||||||
|
|
||||||
const votes = {};
|
const votes = {};
|
||||||
let isPseudoanonymized = true;
|
|
||||||
for (const option of this.poll.options) {
|
for (const option of this.poll.options) {
|
||||||
for (const vote of option.votes) {
|
for (const vote of option.votes) {
|
||||||
const userId = vote.user_id;
|
const token = vote.user_token;
|
||||||
if (userId) {
|
if (!token) {
|
||||||
isPseudoanonymized = false;
|
throw new Error(`assignment_vote/${vote.id} does not contain a user_token`);
|
||||||
if (!votes[userId]) {
|
}
|
||||||
votes[userId] = {
|
if (!votes[token]) {
|
||||||
|
votes[token] = {
|
||||||
user: vote.user,
|
user: vote.user,
|
||||||
votes: []
|
votes: []
|
||||||
};
|
};
|
||||||
@ -116,31 +120,17 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponentDirect
|
|||||||
if (vote.weight > 0) {
|
if (vote.weight > 0) {
|
||||||
if (this.poll.isMethodY) {
|
if (this.poll.isMethodY) {
|
||||||
if (vote.value === 'Y') {
|
if (vote.value === 'Y') {
|
||||||
votes[userId].votes.push(option.user.getFullName());
|
votes[token].votes.push(option.user.getFullName());
|
||||||
} else {
|
} else {
|
||||||
votes[userId].votes.push(this.voteValueToLabel(vote.value));
|
votes[token].votes.push(this.voteValueToLabel(vote.value));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
votes[userId].votes.push(
|
const candidate_name = option.user?.getShortName() ?? this.translate.instant('Deleted user');
|
||||||
`${option.user.getShortName()}: ${this.voteValueToLabel(vote.value)}`
|
votes[token].votes.push(`${candidate_name}: ${this.voteValueToLabel(vote.value)}`);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// if the poll was not pseudoanonymized, add all other users as empty votes
|
|
||||||
if (!isPseudoanonymized) {
|
|
||||||
for (const user of this.poll.voted) {
|
|
||||||
if (!votes[user.id]) {
|
|
||||||
votes[user.id] = {
|
|
||||||
user: user,
|
|
||||||
votes: [this.translate.instant('empty vote')]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.columnDefinitionSingleVotes = definitions;
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
AssignmentPollMethod,
|
AssignmentPollMethod,
|
||||||
AssignmentPollPercentBase
|
AssignmentPollPercentBase
|
||||||
} from 'app/shared/models/assignments/assignment-poll';
|
} from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { MajorityMethod, PollType, VOTE_UNDOCUMENTED } from 'app/shared/models/poll/base-poll';
|
import { MajorityMethod, PercentBase, PollType, VOTE_UNDOCUMENTED } from 'app/shared/models/poll/base-poll';
|
||||||
import { ParsePollNumberPipe } from 'app/shared/pipes/parse-poll-number.pipe';
|
import { ParsePollNumberPipe } from 'app/shared/pipes/parse-poll-number.pipe';
|
||||||
import { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.pipe';
|
import { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.pipe';
|
||||||
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||||
@ -227,6 +227,9 @@ export class AssignmentPollService extends PollService {
|
|||||||
case AssignmentPollPercentBase.Valid:
|
case AssignmentPollPercentBase.Valid:
|
||||||
totalByBase = poll.votesvalid;
|
totalByBase = poll.votesvalid;
|
||||||
break;
|
break;
|
||||||
|
case AssignmentPollPercentBase.Entitled:
|
||||||
|
totalByBase = poll.entitled_users_at_stop.length;
|
||||||
|
break;
|
||||||
case AssignmentPollPercentBase.Cast:
|
case AssignmentPollPercentBase.Cast:
|
||||||
totalByBase = poll.votescast;
|
totalByBase = poll.votescast;
|
||||||
break;
|
break;
|
||||||
|
@ -20,6 +20,7 @@ export const MotionPollPercentBaseVerbose = {
|
|||||||
YNA: 'Yes/No/Abstain',
|
YNA: 'Yes/No/Abstain',
|
||||||
valid: 'All valid ballots',
|
valid: 'All valid ballots',
|
||||||
cast: 'All casted ballots',
|
cast: 'All casted ballots',
|
||||||
|
entitled: 'All entitled users',
|
||||||
disabled: 'Disabled (no percents)'
|
disabled: 'Disabled (no percents)'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ export class MotionDetailOriginalChangeRecommendationsComponent implements OnIni
|
|||||||
@Output()
|
@Output()
|
||||||
public gotoChangeRecommendation: EventEmitter<ViewMotionChangeRecommendation> = new EventEmitter<
|
public gotoChangeRecommendation: EventEmitter<ViewMotionChangeRecommendation> = new EventEmitter<
|
||||||
ViewMotionChangeRecommendation
|
ViewMotionChangeRecommendation
|
||||||
>();
|
>(); // prettier-ignore
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
public html: string;
|
public html: string;
|
||||||
|
@ -32,14 +32,14 @@
|
|||||||
<os-motion-poll-detail-content [poll]="poll"></os-motion-poll-detail-content>
|
<os-motion-poll-detail-content [poll]="poll"></os-motion-poll-detail-content>
|
||||||
|
|
||||||
<!-- Named table: only show if votes are present -->
|
<!-- Named table: only show if votes are present -->
|
||||||
<div class="named-result-table" *ngIf="showResults && poll.stateHasVotes && poll.type === 'named'">
|
<mat-tab-group *ngIf="showResults && poll.stateHasVotes && poll.isEVoting">
|
||||||
<h2>{{ 'Single votes' | translate }}</h2>
|
<mat-tab label="{{ 'Single votes' | translate }}">
|
||||||
|
<div class="named-result-table">
|
||||||
<os-list-view-table
|
<os-list-view-table
|
||||||
*ngIf="votesDataObservable"
|
|
||||||
class="single-votes-table"
|
class="single-votes-table"
|
||||||
[listObservable]="votesDataObservable"
|
[listObservable]="votesDataObservable"
|
||||||
[columns]="columnDefinition"
|
[columns]="columnDefinitionSingleVotesTable"
|
||||||
[filterProps]="filterProps"
|
[filterProps]="filterPropsSingleVotesTable"
|
||||||
[allowProjector]="false"
|
[allowProjector]="false"
|
||||||
[fullScreen]="true"
|
[fullScreen]="true"
|
||||||
[vScrollFixed]="-1"
|
[vScrollFixed]="-1"
|
||||||
@ -75,7 +75,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!vote.user">{{ 'Anonymous' | translate }}</div>
|
<div *ngIf="!vote.user">
|
||||||
|
<i *ngIf="poll.is_pseudoanonymized">{{ "Anonymous" | translate }}</i>
|
||||||
|
<i *ngIf="!poll.is_pseudoanonymized">{{ "Deleted user" | translate }}</i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *pblNgridCellDef="'vote'; row as vote" class="vote-cell">
|
<div *pblNgridCellDef="'vote'; row as vote" class="vote-cell">
|
||||||
<div class="vote-cell-icon-container" [ngClass]="voteOptionStyle[vote.value].css">
|
<div class="vote-cell-icon-container" [ngClass]="voteOptionStyle[vote.value].css">
|
||||||
@ -84,10 +87,15 @@
|
|||||||
<div>{{ vote.valueVerbose | translate }}</div>
|
<div>{{ vote.valueVerbose | translate }}</div>
|
||||||
</div>
|
</div>
|
||||||
</os-list-view-table>
|
</os-list-view-table>
|
||||||
<div *ngIf="!votesDataObservable">
|
|
||||||
{{ 'The individual votes were anonymized.' | translate }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
<mat-tab label="{{ 'Entitled users' | translate }}">
|
||||||
|
<os-entitled-users-table
|
||||||
|
[entitledUsersObservable]="entitledUsersObservable"
|
||||||
|
[listStorageKey]="motion-poll-entitled-users"
|
||||||
|
></os-entitled-users-table>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
||||||
|
|
||||||
<div class="poll-content">
|
<div class="poll-content">
|
||||||
<small *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
<small *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||||
|
@ -10,6 +10,7 @@ import { OperatorService, Permission } from 'app/core/core-services/operator.ser
|
|||||||
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 { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
import { GroupRepositoryService } from 'app/core/repositories/users/group-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 { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||||
@ -27,7 +28,7 @@ import { BasePollDetailComponentDirective } from 'app/site/polls/components/base
|
|||||||
})
|
})
|
||||||
export class MotionPollDetailComponent extends BasePollDetailComponentDirective<ViewMotionPoll, MotionPollService> {
|
export class MotionPollDetailComponent extends BasePollDetailComponentDirective<ViewMotionPoll, MotionPollService> {
|
||||||
public motion: ViewMotion;
|
public motion: ViewMotion;
|
||||||
public columnDefinition: PblColumnDefinition[] = [
|
public columnDefinitionSingleVotesTable: PblColumnDefinition[] = [
|
||||||
{
|
{
|
||||||
prop: 'user',
|
prop: 'user',
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
@ -40,7 +41,7 @@ export class MotionPollDetailComponent extends BasePollDetailComponentDirective<
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
public filterProps = ['user.getFullName', 'valueVerbose'];
|
public filterPropsSingleVotesTable = ['user.getFullName', 'valueVerbose'];
|
||||||
|
|
||||||
public isVoteWeightActive: boolean;
|
public isVoteWeightActive: boolean;
|
||||||
|
|
||||||
@ -62,7 +63,8 @@ export class MotionPollDetailComponent extends BasePollDetailComponentDirective<
|
|||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
protected operator: OperatorService,
|
protected operator: OperatorService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
protected cd: ChangeDetectorRef
|
protected cd: ChangeDetectorRef,
|
||||||
|
protected userRepo: UserRepositoryService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
title,
|
title,
|
||||||
@ -76,7 +78,8 @@ export class MotionPollDetailComponent extends BasePollDetailComponentDirective<
|
|||||||
pollService,
|
pollService,
|
||||||
votesRepo,
|
votesRepo,
|
||||||
operator,
|
operator,
|
||||||
cd
|
cd,
|
||||||
|
userRepo
|
||||||
);
|
);
|
||||||
configService
|
configService
|
||||||
.get<boolean>('users_activate_vote_weight')
|
.get<boolean>('users_activate_vote_weight')
|
||||||
|
@ -137,6 +137,9 @@ export class MotionPollService extends PollService {
|
|||||||
case PercentBase.Cast:
|
case PercentBase.Cast:
|
||||||
totalByBase = poll.votescast;
|
totalByBase = poll.votescast;
|
||||||
break;
|
break;
|
||||||
|
case PercentBase.Entitled:
|
||||||
|
totalByBase = poll.entitled_users_at_stop.length;
|
||||||
|
break;
|
||||||
case PercentBase.Disabled:
|
case PercentBase.Disabled:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -1,25 +1,29 @@
|
|||||||
import { ChangeDetectorRef, Directive, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Directive, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { PblColumnDefinition } from '@pebula/ngrid';
|
||||||
import { Label } from 'ng2-charts';
|
import { Label } from 'ng2-charts';
|
||||||
import { BehaviorSubject, from, Observable } from 'rxjs';
|
import { BehaviorSubject, from, Observable, Subscription } from 'rxjs';
|
||||||
import { filter, map } from 'rxjs/operators';
|
import { filter, map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { Deferred } from 'app/core/promises/deferred';
|
import { Deferred } from 'app/core/promises/deferred';
|
||||||
import { BaseRepository } from 'app/core/repositories/base-repository';
|
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 { UserRepositoryService } from 'app/core/repositories/users/user-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 } from 'app/shared/components/charts/charts.component';
|
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||||
|
import { EntitledUsersEntry } from 'app/shared/models/poll/base-poll';
|
||||||
import { BaseVote } from 'app/shared/models/poll/base-vote';
|
import { BaseVote } from 'app/shared/models/poll/base-vote';
|
||||||
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
import { BaseViewComponentDirective } 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 { EntitledUsersTableEntry } from './entitled-users-table/entitled-users-table.component';
|
||||||
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';
|
import { ViewBaseVote } from '../models/view-base-vote';
|
||||||
@ -31,7 +35,7 @@ export interface BaseVoteData {
|
|||||||
@Directive()
|
@Directive()
|
||||||
export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S extends PollService>
|
export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S extends PollService>
|
||||||
extends BaseViewComponentDirective
|
extends BaseViewComponentDirective
|
||||||
implements OnInit {
|
implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* All the groups of users.
|
* All the groups of users.
|
||||||
*/
|
*/
|
||||||
@ -73,8 +77,13 @@ export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S
|
|||||||
// The observable for the votes-per-user table
|
// The observable for the votes-per-user table
|
||||||
public votesDataObservable: Observable<BaseVoteData[]>;
|
public votesDataObservable: Observable<BaseVoteData[]>;
|
||||||
|
|
||||||
|
// The observable for the entitled-users-table
|
||||||
|
public entitledUsersObservable: Observable<EntitledUsersTableEntry[]>;
|
||||||
|
|
||||||
protected optionsLoaded = new Deferred();
|
protected optionsLoaded = new Deferred();
|
||||||
|
|
||||||
|
private entitledUsersSubscription: Subscription;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
@ -102,7 +111,8 @@ export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S
|
|||||||
protected pollService: S,
|
protected pollService: S,
|
||||||
protected votesRepo: BaseRepository<ViewBaseVote, BaseVote, object>,
|
protected votesRepo: BaseRepository<ViewBaseVote, BaseVote, object>,
|
||||||
protected operator: OperatorService,
|
protected operator: OperatorService,
|
||||||
protected cd: ChangeDetectorRef
|
protected cd: ChangeDetectorRef,
|
||||||
|
protected userRepo: UserRepositoryService
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackbar);
|
super(title, translate, matSnackbar);
|
||||||
this.setup();
|
this.setup();
|
||||||
@ -168,15 +178,11 @@ export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* sets the votes data only if the poll wasn't pseudoanonymized
|
* Set the votes data.
|
||||||
*/
|
*/
|
||||||
protected setVotesData(data: BaseVoteData[]): void {
|
protected setVotesData(data: BaseVoteData[]): void {
|
||||||
if (data.every(voteDate => !voteDate.user)) {
|
|
||||||
this.votesDataObservable = null;
|
|
||||||
} else {
|
|
||||||
this.votesDataObservable = from([data]);
|
this.votesDataObservable = from([data]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is called when the underlying vote data changes. Is supposed to call setVotesData
|
* Is called when the underlying vote data changes. Is supposed to call setVotesData
|
||||||
@ -196,12 +202,46 @@ export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S
|
|||||||
this.createVotesData();
|
this.createVotesData();
|
||||||
this.optionsLoaded.resolve();
|
this.optionsLoaded.resolve();
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
|
this.setEntitledUsersData();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setEntitledUsersData(): void {
|
||||||
|
if (this.entitledUsersSubscription) {
|
||||||
|
this.entitledUsersSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
const userIds = new Set<number>();
|
||||||
|
for (const entry of this.poll.entitled_users_at_stop) {
|
||||||
|
userIds.add(entry.user_id);
|
||||||
|
if (entry.vote_delegated_to_id) {
|
||||||
|
userIds.add(entry.vote_delegated_to_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.entitledUsersSubscription = this.userRepo
|
||||||
|
.getViewModelListObservable()
|
||||||
|
.pipe(
|
||||||
|
filter(users => !!users.length),
|
||||||
|
map(users => users.filter(user => userIds.has(user.id)))
|
||||||
|
)
|
||||||
|
.subscribe(users => {
|
||||||
|
const entries = [];
|
||||||
|
for (const entry of this.poll.entitled_users_at_stop) {
|
||||||
|
entries.push({
|
||||||
|
...entry,
|
||||||
|
user: users.find(user => user.id === entry.user_id),
|
||||||
|
voted_verbose: `voted:${entry.voted}`,
|
||||||
|
vote_delegated_to: entry.vote_delegated_to_id
|
||||||
|
? users.find(user => user.id === entry.vote_delegated_to_id)
|
||||||
|
: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.entitledUsersObservable = from([entries]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected userHasVoteDelegation(user: ViewUser): boolean {
|
protected userHasVoteDelegation(user: ViewUser): boolean {
|
||||||
/**
|
/**
|
||||||
* This will be false if the operator does not have "can_see_extra_data"
|
* This will be false if the operator does not have "can_see_extra_data"
|
||||||
@ -227,4 +267,10 @@ export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S
|
|||||||
return this.operator.viewUser;
|
return this.operator.viewUser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
this.entitledUsersSubscription.unsubscribe();
|
||||||
|
this.entitledUsersSubscription = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
<div *ngIf="canSeeUsers">
|
||||||
|
<os-list-view-table
|
||||||
|
[listObservable]="entitledUsersObservable"
|
||||||
|
[columns]="columnDefinitionEntitledUsersTable"
|
||||||
|
[filterProps]="filterPropsEntitledUsersTable"
|
||||||
|
[allowProjector]="false"
|
||||||
|
[fullScreen]="false"
|
||||||
|
[vScrollFixed]="-1"
|
||||||
|
[listStorageKey]="list-storage-keys"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div *pblNgridHeaderCellDef="'*'; col as col">
|
||||||
|
{{ col.label | translate }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div *pblNgridCellDef="'user_id'; row as entry">
|
||||||
|
<span *ngIf="entry.user">{{ entry.user.getFullName() }}</span>
|
||||||
|
<i *ngIf="!entry.user">{{ 'Deleted user' | translate }}</i>
|
||||||
|
</div>
|
||||||
|
<div *pblNgridCellDef="'voted'; row as entry">
|
||||||
|
<mat-icon *ngIf="entry.voted">check_box</mat-icon>
|
||||||
|
</div>
|
||||||
|
<div *pblNgridCellDef="'delegation'; row as entry">
|
||||||
|
<div *ngIf="entry.vote_delegated_to_id">
|
||||||
|
<span class="repr-prefix">represented by</span>
|
||||||
|
<span *ngIf="entry.vote_delegated_to">{{ entry.vote_delegated_to.getFullName() }}</span>
|
||||||
|
<i *ngIf="!entry.vote_delegated_to">{{ 'Deleted user' | translate }}</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</os-list-view-table>
|
||||||
|
</div>
|
||||||
|
<div class="no-can-see-users" *ngIf="!canSeeUsers">
|
||||||
|
<i>{{ 'You are not allowed to see all entitled users.' | translate }}</i>
|
||||||
|
</div>
|
@ -0,0 +1,9 @@
|
|||||||
|
.repr-prefix {
|
||||||
|
color: #888;
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-can-see-users {
|
||||||
|
margin: 1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { EntitledUsersTableComponent } from './entitled-users-table.component';
|
||||||
|
|
||||||
|
describe('EntitledUsersTableComponent', () => {
|
||||||
|
let component: EntitledUsersTableComponent;
|
||||||
|
let fixture: ComponentFixture<EntitledUsersTableComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EntitledUsersTableComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Directive,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
ViewEncapsulation
|
||||||
|
} from '@angular/core';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { PblColumnDefinition } from '@pebula/ngrid';
|
||||||
|
import { Label } from 'ng2-charts';
|
||||||
|
import { BehaviorSubject, from, Observable, Subscription } from 'rxjs';
|
||||||
|
import { filter, map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { BaseComponent } from 'app/base.component';
|
||||||
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
|
import { Deferred } from 'app/core/promises/deferred';
|
||||||
|
import { BaseRepository } from 'app/core/repositories/base-repository';
|
||||||
|
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
||||||
|
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||||
|
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
|
||||||
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
|
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||||
|
import { EntitledUsersEntry } from 'app/shared/models/poll/base-poll';
|
||||||
|
import { BaseVote } from 'app/shared/models/poll/base-vote';
|
||||||
|
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||||
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
|
||||||
|
export interface EntitledUsersTableEntry extends EntitledUsersEntry {
|
||||||
|
user_id: number;
|
||||||
|
user?: ViewUser;
|
||||||
|
voted: boolean;
|
||||||
|
voted_verbose: string;
|
||||||
|
vote_delegated_to_id?: number;
|
||||||
|
vote_delegated_to?: ViewUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-entitled-users-table',
|
||||||
|
templateUrl: './entitled-users-table.component.html',
|
||||||
|
styleUrls: ['./entitled-users-table.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
|
})
|
||||||
|
export class EntitledUsersTableComponent extends BaseComponent {
|
||||||
|
@Input()
|
||||||
|
public entitledUsersObservable: Observable<EntitledUsersTableEntry[]>;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
public listStorageKey: string;
|
||||||
|
|
||||||
|
public columnDefinitionEntitledUsersTable: PblColumnDefinition[] = [
|
||||||
|
{
|
||||||
|
prop: 'user_id',
|
||||||
|
width: 'auto',
|
||||||
|
label: 'Participant'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'voted',
|
||||||
|
width: 'auto',
|
||||||
|
label: 'Voted'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'delegation',
|
||||||
|
width: 'auto',
|
||||||
|
label: 'Delegated to'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
public filterPropsEntitledUsersTable = ['user.getFullName', 'vote_delegated_to.getFullName', 'voted_verbose'];
|
||||||
|
|
||||||
|
public get canSeeUsers(): boolean {
|
||||||
|
return this.operator.hasPerms(this.permission.usersCanSeeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(title: Title, translate: TranslateService, private operator: OperatorService) {
|
||||||
|
super(title, translate);
|
||||||
|
}
|
||||||
|
}
|
@ -65,6 +65,7 @@ export const PercentBaseVerbose = {
|
|||||||
YNA: 'Yes/No/Abstain',
|
YNA: 'Yes/No/Abstain',
|
||||||
valid: 'Valid votes',
|
valid: 'Valid votes',
|
||||||
cast: 'Total votes cast',
|
cast: 'Total votes cast',
|
||||||
|
entitled: 'All entitled users',
|
||||||
disabled: 'Disabled'
|
disabled: 'Disabled'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { ChartData, ChartDate } from 'app/shared/components/charts/charts.compon
|
|||||||
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import {
|
import {
|
||||||
BasePoll,
|
BasePoll,
|
||||||
|
EntitledUsersEntry,
|
||||||
MajorityMethod,
|
MajorityMethod,
|
||||||
PercentBase,
|
PercentBase,
|
||||||
PollColor,
|
PollColor,
|
||||||
@ -111,6 +112,7 @@ export interface PollData {
|
|||||||
votesvalid: number;
|
votesvalid: number;
|
||||||
votesinvalid: number;
|
votesinvalid: number;
|
||||||
votescast: number;
|
votescast: number;
|
||||||
|
entitled_users_at_stop: EntitledUsersEntry[];
|
||||||
amount_global_yes?: number;
|
amount_global_yes?: number;
|
||||||
amount_global_no?: number;
|
amount_global_no?: number;
|
||||||
amount_global_abstain?: number;
|
amount_global_abstain?: number;
|
||||||
@ -286,7 +288,11 @@ export abstract class PollService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public showPercentOfValidOrCast(poll: PollData | ViewBasePoll): boolean {
|
public showPercentOfValidOrCast(poll: PollData | ViewBasePoll): boolean {
|
||||||
return poll.onehundred_percent_base === PercentBase.Valid || poll.onehundred_percent_base === PercentBase.Cast;
|
return (
|
||||||
|
poll.onehundred_percent_base === PercentBase.Valid ||
|
||||||
|
poll.onehundred_percent_base === PercentBase.Cast ||
|
||||||
|
poll.onehundred_percent_base === PercentBase.Entitled
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSumTableKeys(poll: PollData | ViewBasePoll): VotingResult[] {
|
public getSumTableKeys(poll: PollData | ViewBasePoll): VotingResult[] {
|
||||||
|
@ -3,6 +3,7 @@ import { Component, Input } from '@angular/core';
|
|||||||
import { BaseSlideComponentDirective } from 'app/slides/base-slide-component';
|
import { BaseSlideComponentDirective } from 'app/slides/base-slide-component';
|
||||||
import { CommonListOfSpeakersSlideData, SlideSpeaker } from '../common/common-list-of-speakers-slide-data';
|
import { CommonListOfSpeakersSlideData, SlideSpeaker } from '../common/common-list-of-speakers-slide-data';
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'os-current-list-of-speakers-overlay-slide',
|
selector: 'os-current-list-of-speakers-overlay-slide',
|
||||||
templateUrl: './current-list-of-speakers-overlay-slide.component.html',
|
templateUrl: './current-list-of-speakers-overlay-slide.component.html',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { AssignmentPollMethod } 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 { EntitledUsersEntry, 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';
|
||||||
|
|
||||||
@ -25,6 +25,8 @@ export interface AssignmentPollSlideData extends BasePollSlideData {
|
|||||||
abstain?: number;
|
abstain?: number;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
entitled_users_at_stop: EntitledUsersEntry[];
|
||||||
|
|
||||||
// optional for published polls:
|
// optional for published polls:
|
||||||
amount_global_yes?: number;
|
amount_global_yes?: number;
|
||||||
amount_global_no?: number;
|
amount_global_no?: number;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MotionPollMethod } 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 { EntitledUsersEntry, 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';
|
||||||
|
|
||||||
@ -19,6 +19,8 @@ export interface MotionPollSlideData extends BasePollSlideData {
|
|||||||
abstain?: number;
|
abstain?: number;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
entitled_users_at_stop: EntitledUsersEntry[];
|
||||||
|
|
||||||
// optional for published polls:
|
// optional for published polls:
|
||||||
votesvalid: number;
|
votesvalid: number;
|
||||||
votesinvalid: number;
|
votesinvalid: number;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
|
import { EntitledUsersEntry, MajorityMethod, PercentBase, PollState, PollType } from 'app/shared/models/poll/base-poll';
|
||||||
|
|
||||||
export interface BasePollSlideData {
|
export interface BasePollSlideData {
|
||||||
poll: {
|
poll: {
|
||||||
@ -15,6 +15,8 @@ export interface BasePollSlideData {
|
|||||||
abstain?: number;
|
abstain?: number;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
entitled_users_at_stop: EntitledUsersEntry[];
|
||||||
|
|
||||||
votesvalid: number;
|
votesvalid: number;
|
||||||
votesinvalid: number;
|
votesinvalid: number;
|
||||||
votescast: number;
|
votescast: number;
|
||||||
|
@ -10,7 +10,7 @@ services:
|
|||||||
|
|
||||||
server:
|
server:
|
||||||
image: os3-server-dev
|
image: os3-server-dev
|
||||||
user: $UID:$GID
|
user: $USER_ID:$GROUP_ID
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: docker/Dockerfile.dev
|
dockerfile: docker/Dockerfile.dev
|
||||||
|
@ -19,8 +19,9 @@ RUN rm -rf /var/lib/apt/lists/*
|
|||||||
|
|
||||||
COPY requirements /app/requirements
|
COPY requirements /app/requirements
|
||||||
COPY requirements.txt /app/requirements.txt
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
COPY make/requirements.txt /app/requirements/make_requirements.txt
|
||||||
|
|
||||||
RUN pip install -r requirements.txt -r requirements/saml.txt && \
|
RUN pip install -r requirements.txt -r requirements/saml.txt -r requirements/make_requirements.txt && \
|
||||||
rm -rf /root/.cache/pip
|
rm -rf /root/.cache/pip
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by jsangmeister on 2021-03-18 16:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("assignments", "0018_votes_amount"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentvote",
|
||||||
|
name="user_token",
|
||||||
|
field=models.CharField(null=True, max_length=16),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by jsangmeister on 2021-03-25 10:41
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from ...poll.migrations.vote_migration_helper import set_user_tokens
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("assignments", "0019_assignmentvote_user_token_1"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(set_user_tokens("assignments", "AssignmentVote")),
|
||||||
|
]
|
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by jsangmeister on 2021-03-25 10:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.poll.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("assignments", "0020_assignmentvote_user_token_2"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="assignmentvote",
|
||||||
|
name="user_token",
|
||||||
|
field=models.CharField(
|
||||||
|
null=False,
|
||||||
|
default=openslides.poll.models.generate_user_token,
|
||||||
|
max_length=16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,63 @@
|
|||||||
|
# Generated by jsangmeister on 2021-03-22 12:44
|
||||||
|
|
||||||
|
import jsonfield.encoder
|
||||||
|
import jsonfield.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("assignments", "0021_assignmentvote_user_token_3"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="is_pseudoanonymized",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="entitled_users_at_stop",
|
||||||
|
field=jsonfield.fields.JSONField(
|
||||||
|
dump_kwargs={
|
||||||
|
"cls": jsonfield.encoder.JSONEncoder,
|
||||||
|
"separators": (",", ":"),
|
||||||
|
},
|
||||||
|
load_kwargs={},
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
name="onehundred_percent_base",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("YN", "Yes/No per candidate"),
|
||||||
|
("YNA", "Yes/No/Abstain per candidate"),
|
||||||
|
("Y", "Sum of votes including general No/Abstain"),
|
||||||
|
("valid", "All valid ballots"),
|
||||||
|
("cast", "All casted ballots"),
|
||||||
|
("entitled", "All entitled users"),
|
||||||
|
("disabled", "Disabled (no percents)"),
|
||||||
|
],
|
||||||
|
max_length=8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
old_name="db_votescast",
|
||||||
|
new_name="votescast",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
old_name="db_votesinvalid",
|
||||||
|
new_name="votesinvalid",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="assignmentpoll",
|
||||||
|
old_name="db_votesvalid",
|
||||||
|
new_name="votesvalid",
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,20 @@
|
|||||||
|
# Generated by jsangmeister on 2021-03-22 12:44
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from ...poll.migrations.poll_migration_helper import (
|
||||||
|
calculate_vote_fields,
|
||||||
|
set_is_pseudoanonymized,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("assignments", "0022_assignmentpoll_change_fields_1"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(set_is_pseudoanonymized("assignments", "AssignmentPoll")),
|
||||||
|
migrations.RunPython(calculate_vote_fields("assignments", "AssignmentPoll")),
|
||||||
|
]
|
@ -9,7 +9,7 @@ from openslides.agenda.models import Speaker
|
|||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.core.models import Tag
|
from openslides.core.models import Tag
|
||||||
from openslides.mediafiles.models import Mediafile
|
from openslides.mediafiles.models import Mediafile
|
||||||
from openslides.poll.models import BaseOption, BasePoll, BaseVote
|
from openslides.poll.models import BaseOption, BasePoll, BaseVote, BaseVoteManager
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.manager import BaseManager
|
from openslides.utils.manager import BaseManager
|
||||||
@ -212,7 +212,7 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
|||||||
return {"title": self.title}
|
return {"title": self.title}
|
||||||
|
|
||||||
|
|
||||||
class AssignmentVoteManager(BaseManager):
|
class AssignmentVoteManager(BaseVoteManager):
|
||||||
"""
|
"""
|
||||||
Customized model manager to support our get_prefetched_queryset method.
|
Customized model manager to support our get_prefetched_queryset method.
|
||||||
"""
|
"""
|
||||||
@ -325,6 +325,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
|||||||
PERCENT_BASE_YNA = "YNA"
|
PERCENT_BASE_YNA = "YNA"
|
||||||
PERCENT_BASE_VALID = "valid"
|
PERCENT_BASE_VALID = "valid"
|
||||||
PERCENT_BASE_CAST = "cast"
|
PERCENT_BASE_CAST = "cast"
|
||||||
|
PERCENT_BASE_ENTITLED = "entitled"
|
||||||
PERCENT_BASE_DISABLED = "disabled"
|
PERCENT_BASE_DISABLED = "disabled"
|
||||||
PERCENT_BASES = (
|
PERCENT_BASES = (
|
||||||
(PERCENT_BASE_YN, "Yes/No per candidate"),
|
(PERCENT_BASE_YN, "Yes/No per candidate"),
|
||||||
@ -332,6 +333,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
|||||||
(PERCENT_BASE_Y, "Sum of votes including general No/Abstain"),
|
(PERCENT_BASE_Y, "Sum of votes including general 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_ENTITLED, "All entitled users"),
|
||||||
(PERCENT_BASE_DISABLED, "Disabled (no percents)"),
|
(PERCENT_BASE_DISABLED, "Disabled (no percents)"),
|
||||||
)
|
)
|
||||||
onehundred_percent_base = models.CharField(
|
onehundred_percent_base = models.CharField(
|
||||||
|
@ -522,6 +522,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
"""
|
"""
|
||||||
options = poll.get_options()
|
options = poll.get_options()
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
|
user_token = AssignmentVote.objects.generate_user_token()
|
||||||
for option_id, amount in data.items():
|
for option_id, amount in data.items():
|
||||||
# Add user to the option's voted array
|
# Add user to the option's voted array
|
||||||
option = options.get(pk=option_id)
|
option = options.get(pk=option_id)
|
||||||
@ -540,6 +541,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
delegated_user=request_user,
|
delegated_user=request_user,
|
||||||
weight=weight,
|
weight=weight,
|
||||||
value=value,
|
value=value,
|
||||||
|
user_token=user_token,
|
||||||
)
|
)
|
||||||
inform_changed_data(vote)
|
inform_changed_data(vote)
|
||||||
else: # global_no or global_abstain
|
else: # global_no or global_abstain
|
||||||
@ -566,6 +568,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
request_user is the user who gives the vote, may be a delegate
|
request_user is the user who gives the vote, may be a delegate
|
||||||
"""
|
"""
|
||||||
options = poll.get_options()
|
options = poll.get_options()
|
||||||
|
user_token = AssignmentVote.objects.generate_user_token()
|
||||||
weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1)
|
weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1)
|
||||||
for option_id, result in data.items():
|
for option_id, result in data.items():
|
||||||
option = options.get(pk=option_id)
|
option = options.get(pk=option_id)
|
||||||
@ -575,6 +578,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
delegated_user=request_user,
|
delegated_user=request_user,
|
||||||
value=result,
|
value=result,
|
||||||
weight=weight,
|
weight=weight,
|
||||||
|
user_token=user_token,
|
||||||
)
|
)
|
||||||
inform_changed_data(vote)
|
inform_changed_data(vote)
|
||||||
inform_changed_data(option)
|
inform_changed_data(option)
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by jsangmeister on 2021-03-18 16:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("motions", "0037_motionvote_delegated_user"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motionvote",
|
||||||
|
name="user_token",
|
||||||
|
field=models.CharField(null=True, max_length=16),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by jsangmeister on 2021-03-18 16:27
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from ...poll.migrations.vote_migration_helper import set_user_tokens
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("motions", "0038_motionvote_user_token_1"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(set_user_tokens("motions", "MotionVote")),
|
||||||
|
]
|
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by jsangmeister on 2021-03-18 16:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.poll.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("motions", "0039_motionvote_user_token_2"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motionvote",
|
||||||
|
name="user_token",
|
||||||
|
field=models.CharField(
|
||||||
|
null=False,
|
||||||
|
default=openslides.poll.models.generate_user_token,
|
||||||
|
max_length=16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,62 @@
|
|||||||
|
# Generated by jsangmeister on 2021-03-22 12:44
|
||||||
|
|
||||||
|
import jsonfield.encoder
|
||||||
|
import jsonfield.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("motions", "0040_motionvote_user_token_3"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
name="is_pseudoanonymized",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
name="entitled_users_at_stop",
|
||||||
|
field=jsonfield.fields.JSONField(
|
||||||
|
dump_kwargs={
|
||||||
|
"cls": jsonfield.encoder.JSONEncoder,
|
||||||
|
"separators": (",", ":"),
|
||||||
|
},
|
||||||
|
load_kwargs={},
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
name="onehundred_percent_base",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("YN", "Yes/No"),
|
||||||
|
("YNA", "Yes/No/Abstain"),
|
||||||
|
("valid", "All valid ballots"),
|
||||||
|
("cast", "All casted ballots"),
|
||||||
|
("entitled", "All entitled users"),
|
||||||
|
("disabled", "Disabled (no percents)"),
|
||||||
|
],
|
||||||
|
max_length=8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
old_name="db_votescast",
|
||||||
|
new_name="votescast",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
old_name="db_votesinvalid",
|
||||||
|
new_name="votesinvalid",
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="motionpoll",
|
||||||
|
old_name="db_votesvalid",
|
||||||
|
new_name="votesvalid",
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,20 @@
|
|||||||
|
# Generated by jsangmeister on 2021-03-22 12:44
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from ...poll.migrations.poll_migration_helper import (
|
||||||
|
calculate_vote_fields,
|
||||||
|
set_is_pseudoanonymized,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("motions", "0041_motionpoll_change_fields_1"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(set_is_pseudoanonymized("motions", "MotionPoll")),
|
||||||
|
migrations.RunPython(calculate_vote_fields("motions", "MotionPoll")),
|
||||||
|
]
|
@ -8,7 +8,7 @@ from openslides.agenda.mixins import AgendaItemWithListOfSpeakersMixin
|
|||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.core.models import Tag
|
from openslides.core.models import Tag
|
||||||
from openslides.mediafiles.models import Mediafile
|
from openslides.mediafiles.models import Mediafile
|
||||||
from openslides.poll.models import BaseOption, BasePoll, BaseVote
|
from openslides.poll.models import BaseOption, BasePoll, BaseVote, BaseVoteManager
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.manager import BaseManager
|
from openslides.utils.manager import BaseManager
|
||||||
@ -828,7 +828,7 @@ class MotionBlock(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Mode
|
|||||||
return {"title": self.title}
|
return {"title": self.title}
|
||||||
|
|
||||||
|
|
||||||
class MotionVoteManager(BaseManager):
|
class MotionVoteManager(BaseVoteManager):
|
||||||
"""
|
"""
|
||||||
Customized model manager to support our get_prefetched_queryset method.
|
Customized model manager to support our get_prefetched_queryset method.
|
||||||
"""
|
"""
|
||||||
|
0
server/openslides/poll/migrations/__init__.py
Normal file
0
server/openslides/poll/migrations/__init__.py
Normal file
36
server/openslides/poll/migrations/poll_migration_helper.py
Normal file
36
server/openslides/poll/migrations/poll_migration_helper.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from ..models import BasePoll
|
||||||
|
|
||||||
|
|
||||||
|
def set_is_pseudoanonymized(poll_model_collection, poll_model_name):
|
||||||
|
"""
|
||||||
|
Takes all polls of the given model and updates is_pseudoanonymized, if necessary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _set_is_pseudoanonymized(apps, schema_editor):
|
||||||
|
PollModel = apps.get_model(poll_model_collection, poll_model_name)
|
||||||
|
for poll in PollModel.objects.all():
|
||||||
|
if poll.type == BasePoll.TYPE_PSEUDOANONYMOUS or all(
|
||||||
|
not vote.user_id
|
||||||
|
for option in poll.options.all()
|
||||||
|
for vote in option.votes.all()
|
||||||
|
):
|
||||||
|
poll.is_pseudoanonymized = True
|
||||||
|
poll.save(skip_autoupdate=True)
|
||||||
|
|
||||||
|
return _set_is_pseudoanonymized
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_vote_fields(poll_model_collection, poll_model_name):
|
||||||
|
"""
|
||||||
|
Takes all polls of the given model and updates votes*, if necessary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _calculate_vote_fields(apps, schema_editor):
|
||||||
|
PollModel = apps.get_model(poll_model_collection, poll_model_name)
|
||||||
|
for poll in PollModel.objects.all():
|
||||||
|
if poll.state in (BasePoll.STATE_FINISHED, BasePoll.STATE_PUBLISHED):
|
||||||
|
BasePoll.calculate_votes(poll)
|
||||||
|
BasePoll.calculate_entitled_users(poll)
|
||||||
|
poll.save(skip_autoupdate=True)
|
||||||
|
|
||||||
|
return _calculate_vote_fields
|
24
server/openslides/poll/migrations/vote_migration_helper.py
Normal file
24
server/openslides/poll/migrations/vote_migration_helper.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from ..models import generate_user_token
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_tokens(vote_model_collection, vote_model_name):
|
||||||
|
"""
|
||||||
|
Takes all votes of the given model and checks their tokens. For named polls,
|
||||||
|
multiple votes with the same user_id will get the same token.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _set_user_token(apps, schema_editor):
|
||||||
|
user_token_map = {}
|
||||||
|
VoteModel = apps.get_model(vote_model_collection, vote_model_name)
|
||||||
|
for vote in VoteModel.objects.all():
|
||||||
|
if vote.user is not None:
|
||||||
|
key = (vote.user_id, vote.option.poll_id)
|
||||||
|
if key not in user_token_map:
|
||||||
|
user_token_map[key] = generate_user_token()
|
||||||
|
token = user_token_map[key]
|
||||||
|
else:
|
||||||
|
token = generate_user_token()
|
||||||
|
vote.user_token = token
|
||||||
|
vote.save(skip_autoupdate=True)
|
||||||
|
|
||||||
|
return _set_user_token
|
@ -4,12 +4,21 @@ from typing import Iterable, Optional, Tuple, Type
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
from jsonfield import JSONField
|
||||||
|
|
||||||
|
from openslides.utils.manager import BaseManager
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
|
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
|
||||||
from ..utils.models import SET_NULL_AND_AUTOUPDATE
|
from ..utils.models import SET_NULL_AND_AUTOUPDATE
|
||||||
|
|
||||||
|
|
||||||
|
def generate_user_token():
|
||||||
|
""" Generates a 16 character alphanumeric token. """
|
||||||
|
return get_random_string(16)
|
||||||
|
|
||||||
|
|
||||||
class BaseVote(models.Model):
|
class BaseVote(models.Model):
|
||||||
"""
|
"""
|
||||||
All subclasses must have option attribute with the related name "votes"
|
All subclasses must have option attribute with the related name "votes"
|
||||||
@ -37,11 +46,21 @@ class BaseVote(models.Model):
|
|||||||
on_delete=SET_NULL_AND_AUTOUPDATE,
|
on_delete=SET_NULL_AND_AUTOUPDATE,
|
||||||
related_name="%(class)s_delegated_votes",
|
related_name="%(class)s_delegated_votes",
|
||||||
)
|
)
|
||||||
|
user_token = models.CharField(default=generate_user_token, max_length=16)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class BaseVoteManager(BaseManager):
|
||||||
|
"""
|
||||||
|
Base vote manager that supplies the generate_user_token method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def generate_user_token(self):
|
||||||
|
return generate_user_token()
|
||||||
|
|
||||||
|
|
||||||
class BaseOption(models.Model):
|
class BaseOption(models.Model):
|
||||||
"""
|
"""
|
||||||
All subclasses must have poll attribute with the related name "options"
|
All subclasses must have poll attribute with the related name "options"
|
||||||
@ -134,21 +153,21 @@ class BasePoll(models.Model):
|
|||||||
groups = models.ManyToManyField(settings.AUTH_GROUP_MODEL, blank=True)
|
groups = models.ManyToManyField(settings.AUTH_GROUP_MODEL, blank=True)
|
||||||
voted = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)
|
voted = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)
|
||||||
|
|
||||||
db_votesvalid = models.DecimalField(
|
votesvalid = models.DecimalField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
validators=[MinValueValidator(Decimal("-2"))],
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
max_digits=15,
|
max_digits=15,
|
||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
)
|
)
|
||||||
db_votesinvalid = models.DecimalField(
|
votesinvalid = models.DecimalField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
validators=[MinValueValidator(Decimal("-2"))],
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
max_digits=15,
|
max_digits=15,
|
||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
)
|
)
|
||||||
db_votescast = models.DecimalField(
|
votescast = models.DecimalField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
validators=[MinValueValidator(Decimal("-2"))],
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
@ -160,12 +179,14 @@ class BasePoll(models.Model):
|
|||||||
PERCENT_BASE_YNA = "YNA"
|
PERCENT_BASE_YNA = "YNA"
|
||||||
PERCENT_BASE_VALID = "valid"
|
PERCENT_BASE_VALID = "valid"
|
||||||
PERCENT_BASE_CAST = "cast"
|
PERCENT_BASE_CAST = "cast"
|
||||||
|
PERCENT_BASE_ENTITLED = "entitled"
|
||||||
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"),
|
(PERCENT_BASE_YN, "Yes/No"),
|
||||||
(PERCENT_BASE_YNA, "Yes/No/Abstain"),
|
(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_ENTITLED, "All entitled users"),
|
||||||
(PERCENT_BASE_DISABLED, "Disabled (no percents)"),
|
(PERCENT_BASE_DISABLED, "Disabled (no percents)"),
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
onehundred_percent_base = models.CharField(
|
onehundred_percent_base = models.CharField(
|
||||||
@ -186,57 +207,13 @@ class BasePoll(models.Model):
|
|||||||
max_length=14, blank=False, null=False, choices=MAJORITY_METHODS
|
max_length=14, blank=False, null=False, choices=MAJORITY_METHODS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is_pseudoanonymized = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
entitled_users_at_stop = JSONField(null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def get_votesvalid(self):
|
|
||||||
if self.type == self.TYPE_ANALOG:
|
|
||||||
return self.db_votesvalid
|
|
||||||
else:
|
|
||||||
return Decimal(self.amount_users_voted_with_individual_weight())
|
|
||||||
|
|
||||||
def set_votesvalid(self, value):
|
|
||||||
if self.type != self.TYPE_ANALOG:
|
|
||||||
raise ValueError("Do not set votesvalid for non analog polls")
|
|
||||||
self.db_votesvalid = value
|
|
||||||
|
|
||||||
votesvalid = property(get_votesvalid, set_votesvalid)
|
|
||||||
|
|
||||||
def get_votesinvalid(self):
|
|
||||||
if self.type == self.TYPE_ANALOG:
|
|
||||||
return self.db_votesinvalid
|
|
||||||
else:
|
|
||||||
return Decimal(0)
|
|
||||||
|
|
||||||
def set_votesinvalid(self, value):
|
|
||||||
if self.type != self.TYPE_ANALOG:
|
|
||||||
raise ValueError("Do not set votesinvalid for non analog polls")
|
|
||||||
self.db_votesinvalid = value
|
|
||||||
|
|
||||||
votesinvalid = property(get_votesinvalid, set_votesinvalid)
|
|
||||||
|
|
||||||
def get_votescast(self):
|
|
||||||
if self.type == self.TYPE_ANALOG:
|
|
||||||
return self.db_votescast
|
|
||||||
else:
|
|
||||||
return Decimal(self.amount_users_voted())
|
|
||||||
|
|
||||||
def set_votescast(self, value):
|
|
||||||
if self.type != self.TYPE_ANALOG:
|
|
||||||
raise ValueError("Do not set votescast for non analog polls")
|
|
||||||
self.db_votescast = value
|
|
||||||
|
|
||||||
votescast = property(get_votescast, set_votescast)
|
|
||||||
|
|
||||||
def amount_users_voted(self):
|
|
||||||
return len(self.voted.all())
|
|
||||||
|
|
||||||
def amount_users_voted_with_individual_weight(self):
|
|
||||||
if config["users_activate_vote_weight"]:
|
|
||||||
return sum(user.vote_weight for user in self.voted.all())
|
|
||||||
else:
|
|
||||||
return self.amount_users_voted()
|
|
||||||
|
|
||||||
def create_options(self):
|
def create_options(self):
|
||||||
""" Should be called after creation of this model. """
|
""" Should be called after creation of this model. """
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@ -268,6 +245,8 @@ class BasePoll(models.Model):
|
|||||||
def pseudoanonymize(self):
|
def pseudoanonymize(self):
|
||||||
for option in self.get_options():
|
for option in self.get_options():
|
||||||
option.pseudoanonymize()
|
option.pseudoanonymize()
|
||||||
|
self.is_pseudoanonymized = True
|
||||||
|
self.save()
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
for option in self.get_options():
|
for option in self.get_options():
|
||||||
@ -281,4 +260,38 @@ class BasePoll(models.Model):
|
|||||||
self.votesvalid = None
|
self.votesvalid = None
|
||||||
self.votesinvalid = None
|
self.votesinvalid = None
|
||||||
self.votescast = None
|
self.votescast = None
|
||||||
|
if self.type != self.TYPE_PSEUDOANONYMOUS:
|
||||||
|
self.is_pseudoanonymized = False
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def calculate_votes(self):
|
||||||
|
if self.type != BasePoll.TYPE_ANALOG:
|
||||||
|
self.votescast = self.voted.count()
|
||||||
|
if config["users_activate_vote_weight"]:
|
||||||
|
self.votesvalid = sum(self.voted.values_list("vote_weight", flat=True))
|
||||||
|
else:
|
||||||
|
self.votesvalid = self.votescast
|
||||||
|
self.votesinvalid = Decimal(0)
|
||||||
|
|
||||||
|
def calculate_entitled_users(self):
|
||||||
|
entitled_users = []
|
||||||
|
for group in self.groups.all():
|
||||||
|
for user in group.user_set.all():
|
||||||
|
if user.is_present:
|
||||||
|
entitled_users.append(
|
||||||
|
{
|
||||||
|
"user_id": user.id,
|
||||||
|
"voted": user in self.voted.all(),
|
||||||
|
"vote_delegated_to_id": user.vote_delegated_to_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.entitled_users_at_stop = entitled_users
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Saves a snapshot of the current voted users into the relevant fields and stops the poll.
|
||||||
|
"""
|
||||||
|
self.calculate_votes()
|
||||||
|
self.calculate_entitled_users()
|
||||||
|
self.state = self.STATE_FINISHED
|
||||||
self.save()
|
self.save()
|
||||||
|
@ -5,6 +5,7 @@ from ..utils.rest_api import (
|
|||||||
CharField,
|
CharField,
|
||||||
DecimalField,
|
DecimalField,
|
||||||
IdPrimaryKeyRelatedField,
|
IdPrimaryKeyRelatedField,
|
||||||
|
JSONField,
|
||||||
ModelSerializer,
|
ModelSerializer,
|
||||||
SerializerMethodField,
|
SerializerMethodField,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
@ -18,6 +19,7 @@ BASE_VOTE_FIELDS = (
|
|||||||
"value",
|
"value",
|
||||||
"user",
|
"user",
|
||||||
"delegated_user",
|
"delegated_user",
|
||||||
|
"user_token",
|
||||||
"option",
|
"option",
|
||||||
"pollstate",
|
"pollstate",
|
||||||
)
|
)
|
||||||
@ -58,7 +60,9 @@ BASE_POLL_FIELDS = (
|
|||||||
"id",
|
"id",
|
||||||
"onehundred_percent_base",
|
"onehundred_percent_base",
|
||||||
"majority_method",
|
"majority_method",
|
||||||
|
"is_pseudoanonymized",
|
||||||
"voted",
|
"voted",
|
||||||
|
"entitled_users_at_stop",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -69,27 +73,21 @@ class BasePollSerializer(ModelSerializer):
|
|||||||
)
|
)
|
||||||
options = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
options = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
|
entitled_users_at_stop = JSONField(required=False)
|
||||||
votesvalid = DecimalField(
|
|
||||||
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
|
||||||
)
|
|
||||||
votesinvalid = DecimalField(
|
|
||||||
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
|
||||||
)
|
|
||||||
votescast = DecimalField(
|
|
||||||
max_digits=15, decimal_places=6, min_value=-2, read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
"""
|
"""
|
||||||
Match the 100 percent base to the pollmethod. Change the base, if it does not
|
Match the 100 percent base to the pollmethod. Change the base, if it does not
|
||||||
fit to the pollmethod
|
fit to the pollmethod.
|
||||||
|
Set is_pseudoanonymized if type is pseudoanonymous.
|
||||||
"""
|
"""
|
||||||
new_100_percent_base = self.norm_100_percent_base_to_pollmethod(
|
new_100_percent_base = self.norm_100_percent_base_to_pollmethod(
|
||||||
validated_data["onehundred_percent_base"], validated_data["pollmethod"]
|
validated_data["onehundred_percent_base"], validated_data["pollmethod"]
|
||||||
)
|
)
|
||||||
if new_100_percent_base is not None:
|
if new_100_percent_base is not None:
|
||||||
validated_data["onehundred_percent_base"] = new_100_percent_base
|
validated_data["onehundred_percent_base"] = new_100_percent_base
|
||||||
|
if validated_data["type"] == BasePoll.TYPE_PSEUDOANONYMOUS:
|
||||||
|
validated_data["is_pseudoanonymized"] = True
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
@ -100,8 +98,15 @@ class BasePollSerializer(ModelSerializer):
|
|||||||
|
|
||||||
E.g. the pollmethod is YN, but the 100%-base is YNA, this might not be
|
E.g. the pollmethod is YN, but the 100%-base is YNA, this might not be
|
||||||
possible (see implementing serializers to see forbidden combinations)
|
possible (see implementing serializers to see forbidden combinations)
|
||||||
|
|
||||||
|
Also updates is_pseudoanonymized, if needed.
|
||||||
"""
|
"""
|
||||||
old_100_percent_base = instance.onehundred_percent_base
|
old_100_percent_base = instance.onehundred_percent_base
|
||||||
|
if "type" in validated_data:
|
||||||
|
if validated_data["type"] == BasePoll.TYPE_PSEUDOANONYMOUS:
|
||||||
|
validated_data["is_pseudoanonymized"] = True
|
||||||
|
else:
|
||||||
|
validated_data["is_pseudoanonymized"] = False
|
||||||
instance = super().update(instance, validated_data)
|
instance = super().update(instance, validated_data)
|
||||||
|
|
||||||
new_100_percent_base = self.norm_100_percent_base_to_pollmethod(
|
new_100_percent_base = self.norm_100_percent_base_to_pollmethod(
|
||||||
|
@ -146,8 +146,7 @@ class BasePollViewSet(ModelViewSet):
|
|||||||
if poll.state != BasePoll.STATE_STARTED:
|
if poll.state != BasePoll.STATE_STARTED:
|
||||||
raise ValidationError({"detail": "Wrong poll state"})
|
raise ValidationError({"detail": "Wrong poll state"})
|
||||||
|
|
||||||
poll.state = BasePoll.STATE_FINISHED
|
poll.stop()
|
||||||
poll.save()
|
|
||||||
inform_changed_data(poll.get_votes())
|
inform_changed_data(poll.get_votes())
|
||||||
inform_changed_data(poll.get_options())
|
inform_changed_data(poll.get_options())
|
||||||
self.extend_history_information(["Voting stopped"])
|
self.extend_history_information(["Voting stopped"])
|
||||||
|
@ -6,6 +6,7 @@ from django.conf import settings
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from openslides.assignments.models import (
|
from openslides.assignments.models import (
|
||||||
Assignment,
|
Assignment,
|
||||||
@ -108,7 +109,7 @@ class CreateAssignmentPoll(TestCase):
|
|||||||
self.assignment.add_candidate(self.admin)
|
self.assignment.add_candidate(self.admin)
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
with self.assertNumQueries(40):
|
with self.assertNumQueries(38):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("assignmentpoll-list"),
|
reverse("assignmentpoll-list"),
|
||||||
{
|
{
|
||||||
@ -886,6 +887,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(AssignmentVote.objects.count(), 6)
|
self.assertEqual(AssignmentVote.objects.count(), 6)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
||||||
self.assertEqual(poll.votescast, Decimal("-2"))
|
self.assertEqual(poll.votescast, Decimal("-2"))
|
||||||
@ -1056,6 +1058,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
||||||
self.assertEqual(poll.votescast, Decimal("3"))
|
self.assertEqual(poll.votescast, Decimal("3"))
|
||||||
@ -1081,6 +1084,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
@ -1099,11 +1103,11 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(AssignmentVote.objects.count(), 3)
|
self.assertEqual(AssignmentVote.objects.count(), 3)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), Decimal("1"))
|
|
||||||
self.assertTrue(self.admin in poll.voted.all())
|
self.assertTrue(self.admin in poll.voted.all())
|
||||||
option1 = poll.options.get(pk=1)
|
option1 = poll.options.get(pk=1)
|
||||||
option2 = poll.options.get(pk=2)
|
option2 = poll.options.get(pk=2)
|
||||||
@ -1132,11 +1136,11 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(AssignmentVote.objects.count(), 3)
|
self.assertEqual(AssignmentVote.objects.count(), 3)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, weight)
|
self.assertEqual(poll.votesvalid, weight)
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), weight)
|
|
||||||
option1 = poll.options.get(pk=1)
|
option1 = poll.options.get(pk=1)
|
||||||
option2 = poll.options.get(pk=2)
|
option2 = poll.options.get(pk=2)
|
||||||
option3 = poll.options.get(pk=3)
|
option3 = poll.options.get(pk=3)
|
||||||
@ -1321,6 +1325,51 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertFalse(AssignmentVote.objects.exists())
|
self.assertFalse(AssignmentVote.objects.exists())
|
||||||
|
|
||||||
|
def test_same_user_token(self):
|
||||||
|
self.add_candidate()
|
||||||
|
self.add_candidate()
|
||||||
|
self.start_poll()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||||
|
{"data": {"1": "Y", "2": "N", "3": "A"}},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(AssignmentVote.objects.count(), 3)
|
||||||
|
votes = AssignmentVote.objects.all()
|
||||||
|
user_token = votes[0].user_token
|
||||||
|
for vote in votes[1:2]:
|
||||||
|
assert vote.user_token == user_token
|
||||||
|
|
||||||
|
def test_valid_votes_count_with_deleted_user(self):
|
||||||
|
self.add_candidate()
|
||||||
|
self.start_poll()
|
||||||
|
user, user_password = self.create_user()
|
||||||
|
user.groups.add(GROUP_ADMIN_PK)
|
||||||
|
user.is_present = True
|
||||||
|
user.save()
|
||||||
|
user_client = APIClient()
|
||||||
|
user_client.login(username=user.username, password=user_password)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||||
|
{"data": {"1": "Y", "2": "N"}},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
|
response = user_client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||||
|
{"data": {"1": "N", "2": "Y"}},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
|
self.poll.stop()
|
||||||
|
response = self.client.delete(reverse("user-detail", args=[user.pk]))
|
||||||
|
self.assertHttpStatusVerbose(response, status.HTTP_204_NO_CONTENT)
|
||||||
|
poll = AssignmentPoll.objects.get()
|
||||||
|
self.assertEqual(poll.votesvalid, Decimal("2"))
|
||||||
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
|
self.assertEqual(poll.votescast, Decimal("2"))
|
||||||
|
|
||||||
|
|
||||||
class VoteAssignmentPollNamedY(VoteAssignmentPollBaseTestClass):
|
class VoteAssignmentPollNamedY(VoteAssignmentPollBaseTestClass):
|
||||||
def create_poll(self):
|
def create_poll(self):
|
||||||
@ -1343,6 +1392,7 @@ class VoteAssignmentPollNamedY(VoteAssignmentPollBaseTestClass):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
@ -1360,6 +1410,7 @@ class VoteAssignmentPollNamedY(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(AssignmentVote.objects.count(), 1)
|
self.assertEqual(AssignmentVote.objects.count(), 1)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
@ -1671,6 +1722,7 @@ class VoteAssignmentPollNamedN(VoteAssignmentPollBaseTestClass):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
@ -1688,6 +1740,7 @@ class VoteAssignmentPollNamedN(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(AssignmentVote.objects.count(), 1)
|
self.assertEqual(AssignmentVote.objects.count(), 1)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
@ -1986,6 +2039,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
@ -2004,6 +2058,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(AssignmentVote.objects.count(), 3)
|
self.assertEqual(AssignmentVote.objects.count(), 3)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
@ -2163,6 +2218,22 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertFalse(AssignmentVote.objects.exists())
|
self.assertFalse(AssignmentVote.objects.exists())
|
||||||
|
|
||||||
|
def test_same_user_token(self):
|
||||||
|
self.add_candidate()
|
||||||
|
self.add_candidate()
|
||||||
|
self.start_poll()
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assignmentpoll-vote", args=[self.poll.pk]),
|
||||||
|
{"data": {"1": "Y", "2": "N", "3": "A"}},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(AssignmentVote.objects.count(), 3)
|
||||||
|
votes = AssignmentVote.objects.all()
|
||||||
|
user_token = votes[0].user_token
|
||||||
|
for vote in votes[1:2]:
|
||||||
|
assert vote.user_token == user_token
|
||||||
|
|
||||||
|
|
||||||
class VoteAssignmentPollPseudoanonymousY(VoteAssignmentPollBaseTestClass):
|
class VoteAssignmentPollPseudoanonymousY(VoteAssignmentPollBaseTestClass):
|
||||||
def create_poll(self):
|
def create_poll(self):
|
||||||
@ -2185,6 +2256,7 @@ class VoteAssignmentPollPseudoanonymousY(VoteAssignmentPollBaseTestClass):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
@ -2202,6 +2274,7 @@ class VoteAssignmentPollPseudoanonymousY(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(AssignmentVote.objects.count(), 1)
|
self.assertEqual(AssignmentVote.objects.count(), 1)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
@ -2433,6 +2506,7 @@ class VoteAssignmentPollPseudoanonymousN(VoteAssignmentPollBaseTestClass):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
@ -2450,6 +2524,7 @@ class VoteAssignmentPollPseudoanonymousN(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(AssignmentVote.objects.count(), 1)
|
self.assertEqual(AssignmentVote.objects.count(), 1)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
@ -2681,8 +2756,9 @@ class PseudoanonymizeAssignmentPoll(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = AssignmentPoll.objects.get()
|
poll = AssignmentPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
|
self.assertEqual(poll.is_pseudoanonymized, True)
|
||||||
self.assertEqual(poll.get_votes().count(), 2)
|
self.assertEqual(poll.get_votes().count(), 2)
|
||||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), 2)
|
|
||||||
self.assertEqual(poll.votesvalid, Decimal("2"))
|
self.assertEqual(poll.votesvalid, Decimal("2"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("2"))
|
self.assertEqual(poll.votescast, Decimal("2"))
|
||||||
|
@ -112,6 +112,7 @@ class CreateMotionPoll(TestCase):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_201_CREATED)
|
self.assertHttpStatusVerbose(response, status.HTTP_201_CREATED)
|
||||||
self.assertTrue(MotionPoll.objects.exists())
|
self.assertTrue(MotionPoll.objects.exists())
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
|
self.assertEqual(poll.is_pseudoanonymized, False)
|
||||||
self.assertEqual(poll.title, "test_title_ailai4toogh3eefaa2Vo")
|
self.assertEqual(poll.title, "test_title_ailai4toogh3eefaa2Vo")
|
||||||
self.assertEqual(poll.pollmethod, "YNA")
|
self.assertEqual(poll.pollmethod, "YNA")
|
||||||
self.assertEqual(poll.type, "named")
|
self.assertEqual(poll.type, "named")
|
||||||
@ -394,6 +395,27 @@ class UpdateMotionPoll(TestCase):
|
|||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
self.assertEqual(poll.type, "analog")
|
self.assertEqual(poll.type, "analog")
|
||||||
|
|
||||||
|
def test_patch_type_to_pseudoanonymous(self):
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("motionpoll-detail", args=[self.poll.pk]),
|
||||||
|
{"type": BasePoll.TYPE_PSEUDOANONYMOUS},
|
||||||
|
)
|
||||||
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
|
poll = MotionPoll.objects.get()
|
||||||
|
self.assertEqual(poll.type, BasePoll.TYPE_PSEUDOANONYMOUS)
|
||||||
|
self.assertTrue(poll.is_pseudoanonymized)
|
||||||
|
|
||||||
|
def test_patch_type_to_named(self):
|
||||||
|
self.test_patch_type_to_pseudoanonymous()
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("motionpoll-detail", args=[self.poll.pk]),
|
||||||
|
{"type": BasePoll.TYPE_NAMED},
|
||||||
|
)
|
||||||
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
|
poll = MotionPoll.objects.get()
|
||||||
|
self.assertEqual(poll.type, BasePoll.TYPE_NAMED)
|
||||||
|
self.assertFalse(poll.is_pseudoanonymized)
|
||||||
|
|
||||||
def test_patch_invalid_type(self):
|
def test_patch_invalid_type(self):
|
||||||
response = self.client.patch(
|
response = self.client.patch(
|
||||||
reverse("motionpoll-detail", args=[self.poll.pk]), {"type": "invalid"}
|
reverse("motionpoll-detail", args=[self.poll.pk]), {"type": "invalid"}
|
||||||
@ -585,6 +607,7 @@ class VoteMotionPollAnalog(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
||||||
self.assertEqual(poll.votescast, Decimal("-2"))
|
self.assertEqual(poll.votescast, Decimal("-2"))
|
||||||
@ -668,6 +691,7 @@ class VoteMotionPollAnalog(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
||||||
self.assertEqual(poll.votescast, Decimal("3"))
|
self.assertEqual(poll.votescast, Decimal("3"))
|
||||||
@ -715,6 +739,7 @@ class VoteMotionPollNamed(TestCase):
|
|||||||
response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk]))
|
response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk]))
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.state, MotionPoll.STATE_STARTED)
|
self.assertEqual(poll.state, MotionPoll.STATE_STARTED)
|
||||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
@ -730,6 +755,7 @@ class VoteMotionPollNamed(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
@ -754,11 +780,11 @@ class VoteMotionPollNamed(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, weight)
|
self.assertEqual(poll.votesvalid, weight)
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
self.assertEqual(poll.get_votes().count(), 1)
|
self.assertEqual(poll.get_votes().count(), 1)
|
||||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), weight)
|
|
||||||
option = poll.options.get()
|
option = poll.options.get()
|
||||||
self.assertEqual(option.yes, Decimal("0"))
|
self.assertEqual(option.yes, Decimal("0"))
|
||||||
self.assertEqual(option.no, Decimal("0"))
|
self.assertEqual(option.no, Decimal("0"))
|
||||||
@ -784,6 +810,7 @@ class VoteMotionPollNamed(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
@ -874,6 +901,7 @@ class VoteMotionPollNamed(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
@ -893,6 +921,7 @@ class VoteMotionPollNamed(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("2"))
|
self.assertEqual(poll.votesvalid, Decimal("2"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("2"))
|
self.assertEqual(poll.votescast, Decimal("2"))
|
||||||
@ -1007,6 +1036,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
|||||||
response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk]))
|
response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk]))
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.state, MotionPoll.STATE_STARTED)
|
self.assertEqual(poll.state, MotionPoll.STATE_STARTED)
|
||||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
@ -1022,11 +1052,11 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("1"))
|
self.assertEqual(poll.votescast, Decimal("1"))
|
||||||
self.assertEqual(poll.get_votes().count(), 1)
|
self.assertEqual(poll.get_votes().count(), 1)
|
||||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), 1)
|
|
||||||
option = poll.options.get()
|
option = poll.options.get()
|
||||||
self.assertEqual(option.yes, Decimal("0"))
|
self.assertEqual(option.yes, Decimal("0"))
|
||||||
self.assertEqual(option.no, Decimal("1"))
|
self.assertEqual(option.no, Decimal("1"))
|
||||||
@ -1142,6 +1172,42 @@ class StopMotionPoll(TestCase):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(MotionPoll.objects.get().state, MotionPoll.STATE_CREATED)
|
self.assertEqual(MotionPoll.objects.get().state, MotionPoll.STATE_CREATED)
|
||||||
|
|
||||||
|
def setup_entitled_users(self):
|
||||||
|
self.poll.state = MotionPoll.STATE_STARTED
|
||||||
|
self.poll.save()
|
||||||
|
self.admin = get_user_model().objects.get(username="admin")
|
||||||
|
self.admin.is_present = True
|
||||||
|
self.admin.save()
|
||||||
|
self.group = get_group_model().objects.get(pk=GROUP_ADMIN_PK)
|
||||||
|
self.poll.groups.add(self.group)
|
||||||
|
|
||||||
|
def test_stop_poll_with_entitled_users(self):
|
||||||
|
self.setup_entitled_users()
|
||||||
|
response = self.client.post(reverse("motionpoll-stop", args=[self.poll.pk]))
|
||||||
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
MotionPoll.objects.get().entitled_users_at_stop,
|
||||||
|
[{"user_id": self.admin.id, "voted": False, "vote_delegated_to_id": None}],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stop_poll_with_entitled_users_and_vote_delegation(self):
|
||||||
|
self.setup_entitled_users()
|
||||||
|
user, _ = self.create_user()
|
||||||
|
self.admin.vote_delegated_to = user
|
||||||
|
self.admin.save()
|
||||||
|
response = self.client.post(reverse("motionpoll-stop", args=[self.poll.pk]))
|
||||||
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
MotionPoll.objects.get().entitled_users_at_stop,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"user_id": self.admin.id,
|
||||||
|
"voted": False,
|
||||||
|
"vote_delegated_to_id": user.id,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PublishMotionPoll(TestCase):
|
class PublishMotionPoll(TestCase):
|
||||||
def advancedSetUp(self):
|
def advancedSetUp(self):
|
||||||
@ -1213,8 +1279,9 @@ class PseudoanonymizeMotionPoll(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
|
poll.calculate_votes()
|
||||||
|
self.assertEqual(poll.is_pseudoanonymized, True)
|
||||||
self.assertEqual(poll.get_votes().count(), 2)
|
self.assertEqual(poll.get_votes().count(), 2)
|
||||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), 2)
|
|
||||||
self.assertEqual(poll.votesvalid, Decimal("2"))
|
self.assertEqual(poll.votesvalid, Decimal("2"))
|
||||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||||
self.assertEqual(poll.votescast, Decimal("2"))
|
self.assertEqual(poll.votescast, Decimal("2"))
|
||||||
@ -1282,7 +1349,6 @@ class ResetMotionPoll(TestCase):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
self.assertEqual(poll.get_votes().count(), 0)
|
self.assertEqual(poll.get_votes().count(), 0)
|
||||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), 0)
|
|
||||||
self.assertEqual(poll.votesvalid, None)
|
self.assertEqual(poll.votesvalid, None)
|
||||||
self.assertEqual(poll.votesinvalid, None)
|
self.assertEqual(poll.votesinvalid, None)
|
||||||
self.assertEqual(poll.votescast, None)
|
self.assertEqual(poll.votescast, None)
|
||||||
@ -1292,6 +1358,24 @@ class ResetMotionPoll(TestCase):
|
|||||||
self.assertEqual(option.abstain, Decimal("0"))
|
self.assertEqual(option.abstain, Decimal("0"))
|
||||||
self.assertFalse(option.votes.exists())
|
self.assertFalse(option.votes.exists())
|
||||||
|
|
||||||
|
def test_reset_pseudoanonymized(self):
|
||||||
|
self.poll.type = BasePoll.TYPE_NAMED
|
||||||
|
self.poll.is_pseudoanonymized = True
|
||||||
|
self.poll.save()
|
||||||
|
response = self.client.post(reverse("motionpoll-reset", args=[self.poll.pk]))
|
||||||
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
|
poll = MotionPoll.objects.get()
|
||||||
|
self.assertFalse(poll.is_pseudoanonymized)
|
||||||
|
|
||||||
|
def test_reset_pseudoanonymous(self):
|
||||||
|
self.poll.type = BasePoll.TYPE_PSEUDOANONYMOUS
|
||||||
|
self.poll.is_pseudoanonymized = True
|
||||||
|
self.poll.save()
|
||||||
|
response = self.client.post(reverse("motionpoll-reset", args=[self.poll.pk]))
|
||||||
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
|
poll = MotionPoll.objects.get()
|
||||||
|
self.assertTrue(poll.is_pseudoanonymized)
|
||||||
|
|
||||||
|
|
||||||
class TestMotionPollWithVoteDelegationAutoupdate(TestCase):
|
class TestMotionPollWithVoteDelegationAutoupdate(TestCase):
|
||||||
def advancedSetUp(self):
|
def advancedSetUp(self):
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from decimal import Decimal
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from openslides.motions.models import Motion, MotionChangeRecommendation, MotionPoll
|
from openslides.motions.models import Motion, MotionChangeRecommendation
|
||||||
|
|
||||||
|
|
||||||
# TODO: test for MotionPoll.set_options()
|
# TODO: test for MotionPoll.set_options()
|
||||||
@ -51,25 +50,3 @@ class MotionChangeRecommendationTest(TestCase):
|
|||||||
other_recommendations
|
other_recommendations
|
||||||
)
|
)
|
||||||
self.assertFalse(collides)
|
self.assertFalse(collides)
|
||||||
|
|
||||||
|
|
||||||
class MotionPollAnalogFieldsTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.motion = Motion(
|
|
||||||
title="test_title_OoK9IeChe2Jeib9Deeji",
|
|
||||||
text="test_text_eichui1oobiSeit9aifo",
|
|
||||||
)
|
|
||||||
self.poll = MotionPoll(
|
|
||||||
motion=self.motion,
|
|
||||||
title="test_title_tho8PhiePh8upaex6phi",
|
|
||||||
pollmethod="YNA",
|
|
||||||
type=MotionPoll.TYPE_NAMED,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_not_set_vote_values(self):
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
self.poll.votesvalid = Decimal("1")
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
self.poll.votesinvalid = Decimal("1")
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
self.poll.votescast = Decimal("1")
|
|
||||||
|
Loading…
Reference in New Issue
Block a user