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
|
||||
|
||||
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:
|
||||
docker-compose -f docker/docker-compose.dev.yml down
|
||||
|
||||
server-shell:
|
||||
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
|
||||
|
||||
reload-proxy:
|
||||
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>
|
||||
</td>
|
||||
</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>
|
||||
</table>
|
||||
|
||||
|
@ -46,4 +46,9 @@
|
||||
.single-result {
|
||||
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 { OperatorService } from 'app/core/core-services/operator.service';
|
||||
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 { AssignmentPollService } from 'app/site/assignments/modules/assignment-poll/services/assignment-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;
|
||||
}
|
||||
|
||||
public get isPercentBaseEntitled(): boolean {
|
||||
return this.poll.onehundred_percent_base === PercentBase.Entitled;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translateService: TranslateService,
|
||||
|
@ -30,6 +30,15 @@
|
||||
{{ row.value[0].amount | parsePollNumber }}
|
||||
</td>
|
||||
</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>
|
||||
</table>
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
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 { MotionPollService } from 'app/site/motions/services/motion-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;
|
||||
}
|
||||
|
||||
public get isPercentBaseEntitled(): boolean {
|
||||
return this.poll.onehundred_percent_base === PercentBase.Entitled;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
|
@ -15,6 +15,7 @@ export enum AssignmentPollPercentBase {
|
||||
YNA = 'YNA',
|
||||
Valid = 'valid',
|
||||
Cast = 'cast',
|
||||
Entitled = 'entitled',
|
||||
Disabled = 'disabled'
|
||||
}
|
||||
|
||||
|
@ -35,9 +35,16 @@ export enum PercentBase {
|
||||
YNA = 'YNA',
|
||||
Valid = 'valid',
|
||||
Cast = 'cast',
|
||||
Entitled = 'entitled',
|
||||
Disabled = 'disabled'
|
||||
}
|
||||
|
||||
export interface EntitledUsersEntry {
|
||||
user_id: number;
|
||||
voted: boolean;
|
||||
vote_delegated_to_id?: number;
|
||||
}
|
||||
|
||||
export const VOTE_MAJORITY = -1;
|
||||
export const VOTE_UNDOCUMENTED = -2;
|
||||
export const LOWEST_VOTE_VALUE = VOTE_UNDOCUMENTED;
|
||||
@ -61,6 +68,8 @@ export abstract class BasePoll<
|
||||
public user_has_voted_for_delegations: number[];
|
||||
public pollmethod: PM;
|
||||
public onehundred_percent_base: PB;
|
||||
public is_pseudoanonymized: boolean;
|
||||
public entitled_users_at_stop: EntitledUsersEntry[];
|
||||
|
||||
public get isCreated(): boolean {
|
||||
return this.state === PollState.Created;
|
||||
|
@ -28,6 +28,7 @@ export abstract class BaseVote<T = any> extends BaseDecimalModel<T> {
|
||||
public value: VoteValue;
|
||||
public option_id: number;
|
||||
public user_id?: number;
|
||||
public user_token: string;
|
||||
|
||||
public get valueVerbose(): string {
|
||||
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 { BannerComponent } from './components/banner/banner.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 { ParsePollNumberPipe } from './pipes/parse-poll-number.pipe';
|
||||
import { ReversePipe } from './pipes/reverse.pipe';
|
||||
@ -293,6 +294,7 @@ import { ApplauseParticleDisplayComponent } from './components/applause-particle
|
||||
CheckInputComponent,
|
||||
BannerComponent,
|
||||
PollFormComponent,
|
||||
EntitledUsersTableComponent,
|
||||
MotionPollDialogComponent,
|
||||
ParsePollNumberPipe,
|
||||
ReversePipe,
|
||||
@ -356,6 +358,7 @@ import { ApplauseParticleDisplayComponent } from './components/applause-particle
|
||||
CheckInputComponent,
|
||||
BannerComponent,
|
||||
PollFormComponent,
|
||||
EntitledUsersTableComponent,
|
||||
MotionPollDialogComponent,
|
||||
ParsePollNumberPipe,
|
||||
ReversePipe,
|
||||
|
@ -30,6 +30,7 @@ export const AssignmentPollPercentBaseVerbose = {
|
||||
YNA: _('Yes/No/Abstain per candidate'),
|
||||
valid: _('All valid ballots'),
|
||||
cast: _('All casted ballots'),
|
||||
entitled: _('All entitled users'),
|
||||
disabled: _('Disabled (no percents)')
|
||||
};
|
||||
|
||||
|
@ -37,67 +37,74 @@
|
||||
<os-charts class="assignment-result-chart" [labels]="candidatesLabels" [data]="chartData"></os-charts>
|
||||
</div>
|
||||
|
||||
<!-- Single Votes Table -->
|
||||
<div class="named-result-table" *ngIf="showResults && poll.stateHasVotes && poll.type === 'named'">
|
||||
<h3>{{ 'Single votes' | translate }}</h3>
|
||||
<os-list-view-table
|
||||
class="single-votes-table"
|
||||
*ngIf="votesDataObservable"
|
||||
[listObservable]="votesDataObservable"
|
||||
[columns]="columnDefinitionSingleVotes"
|
||||
[filterProps]="filterProps"
|
||||
[allowProjector]="false"
|
||||
[fullScreen]="false"
|
||||
[vScrollFixed]="-1"
|
||||
listStorageKey="assignment-poll-vote"
|
||||
[showListOfSpeakers]="false"
|
||||
[showMenu]="false"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div *pblNgridHeaderCellDef="'user'; col as col">
|
||||
{{ col.label | translate }}
|
||||
</div>
|
||||
<div *pblNgridHeaderCellDef="'votes'; col as col">
|
||||
{{ col.label | translate }}
|
||||
</div>
|
||||
<mat-tab-group *ngIf="showResults && poll.stateHasVotes && poll.isEVoting">
|
||||
<mat-tab label="{{ 'Single votes' | translate }}">
|
||||
<!-- Single Votes Table -->
|
||||
<div class="named-result-table">
|
||||
<os-list-view-table
|
||||
class="single-votes-table"
|
||||
*ngIf="votesDataObservable"
|
||||
[listObservable]="votesDataObservable"
|
||||
[columns]="columnDefinitionSingleVotes"
|
||||
[filterProps]="filterProps"
|
||||
[allowProjector]="false"
|
||||
[fullScreen]="false"
|
||||
[vScrollFixed]="-1"
|
||||
listStorageKey="assignment-poll-vote"
|
||||
[showListOfSpeakers]="false"
|
||||
[showMenu]="false"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div *pblNgridHeaderCellDef="'user'; col as col">
|
||||
{{ col.label | translate }}
|
||||
</div>
|
||||
<div *pblNgridHeaderCellDef="'votes'; col as col">
|
||||
{{ col.label | translate }}
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div *pblNgridCellDef="'user'; row as vote">
|
||||
<div *ngIf="vote.user">
|
||||
{{ vote.user.getShortName() }}
|
||||
<div class="user-subtitle">
|
||||
<!-- Level and number -->
|
||||
<div *ngIf="vote.user.getLevelAndNumber()">
|
||||
{{ vote.user.getLevelAndNumber() }}
|
||||
<!-- Content -->
|
||||
<div *pblNgridCellDef="'user'; row as vote">
|
||||
<div *ngIf="vote.user">
|
||||
{{ vote.user.getShortName() }}
|
||||
<div class="user-subtitle">
|
||||
<!-- Level and number -->
|
||||
<div *ngIf="vote.user.getLevelAndNumber()">
|
||||
{{ vote.user.getLevelAndNumber() }}
|
||||
</div>
|
||||
|
||||
<!-- Vote weight -->
|
||||
<div *ngIf="isVoteWeightActive">
|
||||
{{ 'Vote weight' | translate }}: {{ vote.user.vote_weight }}
|
||||
</div>
|
||||
|
||||
<!-- Delegation -->
|
||||
<div *ngIf="userHasVoteDelegation(vote.user)">
|
||||
<span>
|
||||
({{ 'represented by' | translate }}
|
||||
{{ getUsersVoteDelegation(vote.user).getShortName().trim() }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vote weight -->
|
||||
<div *ngIf="isVoteWeightActive">
|
||||
{{ 'Vote weight' | translate }}: {{ vote.user.vote_weight }}
|
||||
</div>
|
||||
|
||||
<!-- Delegation -->
|
||||
<div *ngIf="userHasVoteDelegation(vote.user)">
|
||||
<span>
|
||||
({{ 'represented by' | translate }}
|
||||
{{ getUsersVoteDelegation(vote.user).getShortName().trim() }})
|
||||
</span>
|
||||
<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 *ngIf="!vote.user">
|
||||
{{ 'Anonymous' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *pblNgridCellDef="'votes'; row as vote">
|
||||
<div class="single-vote-result" *ngFor="let candidate of vote.votes">{{ candidate }}</div>
|
||||
<div *pblNgridCellDef="'votes'; row as vote">
|
||||
<div class="single-vote-result" *ngFor="let candidate of vote.votes">{{ candidate }}</div>
|
||||
</div>
|
||||
</os-list-view-table>
|
||||
</div>
|
||||
</os-list-view-table>
|
||||
<div *ngIf="!votesDataObservable">
|
||||
{{ 'The individual votes were anonymized.' | translate }}
|
||||
</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>
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
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 { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-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 { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||
@ -62,7 +64,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponentDirect
|
||||
votesRepo: AssignmentVoteRepositoryService,
|
||||
protected operator: OperatorService,
|
||||
private router: Router,
|
||||
protected cd: ChangeDetectorRef
|
||||
protected cd: ChangeDetectorRef,
|
||||
protected userRepo: UserRepositoryService
|
||||
) {
|
||||
super(
|
||||
title,
|
||||
@ -76,7 +79,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponentDirect
|
||||
pollService,
|
||||
votesRepo,
|
||||
operator,
|
||||
cd
|
||||
cd,
|
||||
userRepo
|
||||
);
|
||||
configService
|
||||
.get<boolean>('users_activate_vote_weight')
|
||||
@ -100,47 +104,33 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponentDirect
|
||||
];
|
||||
|
||||
const votes = {};
|
||||
let isPseudoanonymized = true;
|
||||
for (const option of this.poll.options) {
|
||||
for (const vote of option.votes) {
|
||||
const userId = vote.user_id;
|
||||
if (userId) {
|
||||
isPseudoanonymized = false;
|
||||
if (!votes[userId]) {
|
||||
votes[userId] = {
|
||||
user: vote.user,
|
||||
votes: []
|
||||
};
|
||||
}
|
||||
|
||||
if (vote.weight > 0) {
|
||||
if (this.poll.isMethodY) {
|
||||
if (vote.value === 'Y') {
|
||||
votes[userId].votes.push(option.user.getFullName());
|
||||
} else {
|
||||
votes[userId].votes.push(this.voteValueToLabel(vote.value));
|
||||
}
|
||||
} else {
|
||||
votes[userId].votes.push(
|
||||
`${option.user.getShortName()}: ${this.voteValueToLabel(vote.value)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
const token = vote.user_token;
|
||||
if (!token) {
|
||||
throw new Error(`assignment_vote/${vote.id} does not contain a user_token`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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')]
|
||||
if (!votes[token]) {
|
||||
votes[token] = {
|
||||
user: vote.user,
|
||||
votes: []
|
||||
};
|
||||
}
|
||||
|
||||
if (vote.weight > 0) {
|
||||
if (this.poll.isMethodY) {
|
||||
if (vote.value === 'Y') {
|
||||
votes[token].votes.push(option.user.getFullName());
|
||||
} else {
|
||||
votes[token].votes.push(this.voteValueToLabel(vote.value));
|
||||
}
|
||||
} else {
|
||||
const candidate_name = option.user?.getShortName() ?? this.translate.instant('Deleted user');
|
||||
votes[token].votes.push(`${candidate_name}: ${this.voteValueToLabel(vote.value)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setVotesData(Object.values(votes));
|
||||
this.candidatesLabels = this.pollService.getChartLabels(this.poll);
|
||||
this.columnDefinitionSingleVotes = definitions;
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
AssignmentPollMethod,
|
||||
AssignmentPollPercentBase
|
||||
} 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 { PollKeyVerbosePipe } from 'app/shared/pipes/poll-key-verbose.pipe';
|
||||
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||
@ -227,6 +227,9 @@ export class AssignmentPollService extends PollService {
|
||||
case AssignmentPollPercentBase.Valid:
|
||||
totalByBase = poll.votesvalid;
|
||||
break;
|
||||
case AssignmentPollPercentBase.Entitled:
|
||||
totalByBase = poll.entitled_users_at_stop.length;
|
||||
break;
|
||||
case AssignmentPollPercentBase.Cast:
|
||||
totalByBase = poll.votescast;
|
||||
break;
|
||||
|
@ -20,6 +20,7 @@ export const MotionPollPercentBaseVerbose = {
|
||||
YNA: 'Yes/No/Abstain',
|
||||
valid: 'All valid ballots',
|
||||
cast: 'All casted ballots',
|
||||
entitled: 'All entitled users',
|
||||
disabled: 'Disabled (no percents)'
|
||||
};
|
||||
|
||||
|
@ -59,7 +59,7 @@ export class MotionDetailOriginalChangeRecommendationsComponent implements OnIni
|
||||
@Output()
|
||||
public gotoChangeRecommendation: EventEmitter<ViewMotionChangeRecommendation> = new EventEmitter<
|
||||
ViewMotionChangeRecommendation
|
||||
>();
|
||||
>(); // prettier-ignore
|
||||
|
||||
@Input()
|
||||
public html: string;
|
||||
|
@ -32,62 +32,70 @@
|
||||
<os-motion-poll-detail-content [poll]="poll"></os-motion-poll-detail-content>
|
||||
|
||||
<!-- Named table: only show if votes are present -->
|
||||
<div class="named-result-table" *ngIf="showResults && poll.stateHasVotes && poll.type === 'named'">
|
||||
<h2>{{ 'Single votes' | translate }}</h2>
|
||||
<os-list-view-table
|
||||
*ngIf="votesDataObservable"
|
||||
class="single-votes-table"
|
||||
[listObservable]="votesDataObservable"
|
||||
[columns]="columnDefinition"
|
||||
[filterProps]="filterProps"
|
||||
[allowProjector]="false"
|
||||
[fullScreen]="true"
|
||||
[vScrollFixed]="-1"
|
||||
listStorageKey="motion-poll-vote"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div *pblNgridHeaderCellDef="'*'; col as col">
|
||||
{{ col.label | translate }}
|
||||
</div>
|
||||
<mat-tab-group *ngIf="showResults && poll.stateHasVotes && poll.isEVoting">
|
||||
<mat-tab label="{{ 'Single votes' | translate }}">
|
||||
<div class="named-result-table">
|
||||
<os-list-view-table
|
||||
class="single-votes-table"
|
||||
[listObservable]="votesDataObservable"
|
||||
[columns]="columnDefinitionSingleVotesTable"
|
||||
[filterProps]="filterPropsSingleVotesTable"
|
||||
[allowProjector]="false"
|
||||
[fullScreen]="true"
|
||||
[vScrollFixed]="-1"
|
||||
listStorageKey="motion-poll-vote"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div *pblNgridHeaderCellDef="'*'; col as col">
|
||||
{{ col.label | translate }}
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div *pblNgridCellDef="'user'; row as vote">
|
||||
<div *ngIf="vote.user">
|
||||
{{ vote.user.getShortName() }}
|
||||
<!-- Content -->
|
||||
<div *pblNgridCellDef="'user'; row as vote">
|
||||
<div *ngIf="vote.user">
|
||||
{{ vote.user.getShortName() }}
|
||||
|
||||
<div class="user-subtitle">
|
||||
<!-- Level and number -->
|
||||
<div *ngIf="vote.user.getLevelAndNumber()">
|
||||
{{ vote.user.getLevelAndNumber() }}
|
||||
<div class="user-subtitle">
|
||||
<!-- Level and number -->
|
||||
<div *ngIf="vote.user.getLevelAndNumber()">
|
||||
{{ vote.user.getLevelAndNumber() }}
|
||||
</div>
|
||||
|
||||
<!-- Vote weight -->
|
||||
<div *ngIf="isVoteWeightActive">
|
||||
{{ 'Vote weight' | translate }}: {{ vote.user.vote_weight }}
|
||||
</div>
|
||||
|
||||
<!-- Delegation -->
|
||||
<div *ngIf="userHasVoteDelegation(vote.user)">
|
||||
<span>
|
||||
({{ 'represented by' | translate }}
|
||||
{{ getUsersVoteDelegation(vote.user).getShortName().trim() }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vote weight -->
|
||||
<div *ngIf="isVoteWeightActive">
|
||||
{{ 'Vote weight' | translate }}: {{ vote.user.vote_weight }}
|
||||
</div>
|
||||
|
||||
<!-- Delegation -->
|
||||
<div *ngIf="userHasVoteDelegation(vote.user)">
|
||||
<span>
|
||||
({{ 'represented by' | translate }}
|
||||
{{ getUsersVoteDelegation(vote.user).getShortName().trim() }})
|
||||
</span>
|
||||
<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 *ngIf="!vote.user">{{ 'Anonymous' | translate }}</div>
|
||||
<div *pblNgridCellDef="'vote'; row as vote" class="vote-cell">
|
||||
<div class="vote-cell-icon-container" [ngClass]="voteOptionStyle[vote.value].css">
|
||||
<mat-icon>{{ voteOptionStyle[vote.value].icon }}</mat-icon>
|
||||
</div>
|
||||
<div>{{ vote.valueVerbose | translate }}</div>
|
||||
</div>
|
||||
</os-list-view-table>
|
||||
</div>
|
||||
<div *pblNgridCellDef="'vote'; row as vote" class="vote-cell">
|
||||
<div class="vote-cell-icon-container" [ngClass]="voteOptionStyle[vote.value].css">
|
||||
<mat-icon>{{ voteOptionStyle[vote.value].icon }}</mat-icon>
|
||||
</div>
|
||||
<div>{{ vote.valueVerbose | translate }}</div>
|
||||
</div>
|
||||
</os-list-view-table>
|
||||
<div *ngIf="!votesDataObservable">
|
||||
{{ 'The individual votes were anonymized.' | translate }}
|
||||
</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">
|
||||
<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 { MotionVoteRepositoryService } from 'app/core/repositories/motions/motion-vote-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 { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
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> {
|
||||
public motion: ViewMotion;
|
||||
public columnDefinition: PblColumnDefinition[] = [
|
||||
public columnDefinitionSingleVotesTable: PblColumnDefinition[] = [
|
||||
{
|
||||
prop: 'user',
|
||||
width: 'auto',
|
||||
@ -40,7 +41,7 @@ export class MotionPollDetailComponent extends BasePollDetailComponentDirective<
|
||||
}
|
||||
];
|
||||
|
||||
public filterProps = ['user.getFullName', 'valueVerbose'];
|
||||
public filterPropsSingleVotesTable = ['user.getFullName', 'valueVerbose'];
|
||||
|
||||
public isVoteWeightActive: boolean;
|
||||
|
||||
@ -62,7 +63,8 @@ export class MotionPollDetailComponent extends BasePollDetailComponentDirective<
|
||||
configService: ConfigService,
|
||||
protected operator: OperatorService,
|
||||
private router: Router,
|
||||
protected cd: ChangeDetectorRef
|
||||
protected cd: ChangeDetectorRef,
|
||||
protected userRepo: UserRepositoryService
|
||||
) {
|
||||
super(
|
||||
title,
|
||||
@ -76,7 +78,8 @@ export class MotionPollDetailComponent extends BasePollDetailComponentDirective<
|
||||
pollService,
|
||||
votesRepo,
|
||||
operator,
|
||||
cd
|
||||
cd,
|
||||
userRepo
|
||||
);
|
||||
configService
|
||||
.get<boolean>('users_activate_vote_weight')
|
||||
|
@ -137,6 +137,9 @@ export class MotionPollService extends PollService {
|
||||
case PercentBase.Cast:
|
||||
totalByBase = poll.votescast;
|
||||
break;
|
||||
case PercentBase.Entitled:
|
||||
totalByBase = poll.entitled_users_at_stop.length;
|
||||
break;
|
||||
case PercentBase.Disabled:
|
||||
break;
|
||||
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 { 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 } from 'rxjs';
|
||||
import { BehaviorSubject, from, Observable, Subscription } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
|
||||
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';
|
||||
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 { ViewBasePoll } from '../models/view-base-poll';
|
||||
import { ViewBaseVote } from '../models/view-base-vote';
|
||||
@ -31,7 +35,7 @@ export interface BaseVoteData {
|
||||
@Directive()
|
||||
export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S extends PollService>
|
||||
extends BaseViewComponentDirective
|
||||
implements OnInit {
|
||||
implements OnInit, OnDestroy {
|
||||
/**
|
||||
* 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
|
||||
public votesDataObservable: Observable<BaseVoteData[]>;
|
||||
|
||||
// The observable for the entitled-users-table
|
||||
public entitledUsersObservable: Observable<EntitledUsersTableEntry[]>;
|
||||
|
||||
protected optionsLoaded = new Deferred();
|
||||
|
||||
private entitledUsersSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@ -102,7 +111,8 @@ export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S
|
||||
protected pollService: S,
|
||||
protected votesRepo: BaseRepository<ViewBaseVote, BaseVote, object>,
|
||||
protected operator: OperatorService,
|
||||
protected cd: ChangeDetectorRef
|
||||
protected cd: ChangeDetectorRef,
|
||||
protected userRepo: UserRepositoryService
|
||||
) {
|
||||
super(title, translate, matSnackbar);
|
||||
this.setup();
|
||||
@ -168,14 +178,10 @@ 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 {
|
||||
if (data.every(voteDate => !voteDate.user)) {
|
||||
this.votesDataObservable = null;
|
||||
} else {
|
||||
this.votesDataObservable = from([data]);
|
||||
}
|
||||
this.votesDataObservable = from([data]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -196,12 +202,46 @@ export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S
|
||||
this.createVotesData();
|
||||
this.optionsLoaded.resolve();
|
||||
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 {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
valid: 'Valid votes',
|
||||
cast: 'Total votes cast',
|
||||
entitled: 'All entitled users',
|
||||
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 {
|
||||
BasePoll,
|
||||
EntitledUsersEntry,
|
||||
MajorityMethod,
|
||||
PercentBase,
|
||||
PollColor,
|
||||
@ -111,6 +112,7 @@ export interface PollData {
|
||||
votesvalid: number;
|
||||
votesinvalid: number;
|
||||
votescast: number;
|
||||
entitled_users_at_stop: EntitledUsersEntry[];
|
||||
amount_global_yes?: number;
|
||||
amount_global_no?: number;
|
||||
amount_global_abstain?: number;
|
||||
@ -286,7 +288,11 @@ export abstract class PollService {
|
||||
}
|
||||
|
||||
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[] {
|
||||
|
@ -3,6 +3,7 @@ import { Component, Input } from '@angular/core';
|
||||
import { BaseSlideComponentDirective } from 'app/slides/base-slide-component';
|
||||
import { CommonListOfSpeakersSlideData, SlideSpeaker } from '../common/common-list-of-speakers-slide-data';
|
||||
|
||||
// prettier-ignore
|
||||
@Component({
|
||||
selector: 'os-current-list-of-speakers-overlay-slide',
|
||||
templateUrl: './current-list-of-speakers-overlay-slide.component.html',
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { BasePollSlideData } from 'app/slides/polls/base-poll-slide-data';
|
||||
|
||||
@ -25,6 +25,8 @@ export interface AssignmentPollSlideData extends BasePollSlideData {
|
||||
abstain?: number;
|
||||
}[];
|
||||
|
||||
entitled_users_at_stop: EntitledUsersEntry[];
|
||||
|
||||
// optional for published polls:
|
||||
amount_global_yes?: number;
|
||||
amount_global_no?: number;
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { BasePollSlideData } from 'app/slides/polls/base-poll-slide-data';
|
||||
|
||||
@ -19,6 +19,8 @@ export interface MotionPollSlideData extends BasePollSlideData {
|
||||
abstain?: number;
|
||||
}[];
|
||||
|
||||
entitled_users_at_stop: EntitledUsersEntry[];
|
||||
|
||||
// optional for published polls:
|
||||
votesvalid: 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 {
|
||||
poll: {
|
||||
@ -15,6 +15,8 @@ export interface BasePollSlideData {
|
||||
abstain?: number;
|
||||
}[];
|
||||
|
||||
entitled_users_at_stop: EntitledUsersEntry[];
|
||||
|
||||
votesvalid: number;
|
||||
votesinvalid: number;
|
||||
votescast: number;
|
||||
|
@ -10,7 +10,7 @@ services:
|
||||
|
||||
server:
|
||||
image: os3-server-dev
|
||||
user: $UID:$GID
|
||||
user: $USER_ID:$GROUP_ID
|
||||
build:
|
||||
context: ../server
|
||||
dockerfile: docker/Dockerfile.dev
|
||||
|
@ -19,8 +19,9 @@ RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements /app/requirements
|
||||
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
|
||||
|
||||
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.models import Tag
|
||||
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.exceptions import OpenSlidesError
|
||||
from openslides.utils.manager import BaseManager
|
||||
@ -212,7 +212,7 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
||||
return {"title": self.title}
|
||||
|
||||
|
||||
class AssignmentVoteManager(BaseManager):
|
||||
class AssignmentVoteManager(BaseVoteManager):
|
||||
"""
|
||||
Customized model manager to support our get_prefetched_queryset method.
|
||||
"""
|
||||
@ -325,6 +325,7 @@ class AssignmentPoll(RESTModelMixin, BasePoll):
|
||||
PERCENT_BASE_YNA = "YNA"
|
||||
PERCENT_BASE_VALID = "valid"
|
||||
PERCENT_BASE_CAST = "cast"
|
||||
PERCENT_BASE_ENTITLED = "entitled"
|
||||
PERCENT_BASE_DISABLED = "disabled"
|
||||
PERCENT_BASES = (
|
||||
(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_VALID, "All valid ballots"),
|
||||
(PERCENT_BASE_CAST, "All casted ballots"),
|
||||
(PERCENT_BASE_ENTITLED, "All entitled users"),
|
||||
(PERCENT_BASE_DISABLED, "Disabled (no percents)"),
|
||||
)
|
||||
onehundred_percent_base = models.CharField(
|
||||
|
@ -522,6 +522,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
"""
|
||||
options = poll.get_options()
|
||||
if isinstance(data, dict):
|
||||
user_token = AssignmentVote.objects.generate_user_token()
|
||||
for option_id, amount in data.items():
|
||||
# Add user to the option's voted array
|
||||
option = options.get(pk=option_id)
|
||||
@ -540,6 +541,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
delegated_user=request_user,
|
||||
weight=weight,
|
||||
value=value,
|
||||
user_token=user_token,
|
||||
)
|
||||
inform_changed_data(vote)
|
||||
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
|
||||
"""
|
||||
options = poll.get_options()
|
||||
user_token = AssignmentVote.objects.generate_user_token()
|
||||
weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1)
|
||||
for option_id, result in data.items():
|
||||
option = options.get(pk=option_id)
|
||||
@ -575,6 +578,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
||||
delegated_user=request_user,
|
||||
value=result,
|
||||
weight=weight,
|
||||
user_token=user_token,
|
||||
)
|
||||
inform_changed_data(vote)
|
||||
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.models import Tag
|
||||
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.exceptions import OpenSlidesError
|
||||
from openslides.utils.manager import BaseManager
|
||||
@ -828,7 +828,7 @@ class MotionBlock(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Mode
|
||||
return {"title": self.title}
|
||||
|
||||
|
||||
class MotionVoteManager(BaseManager):
|
||||
class MotionVoteManager(BaseVoteManager):
|
||||
"""
|
||||
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.core.validators import MinValueValidator
|
||||
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 ..utils.autoupdate import inform_changed_data, inform_deleted_data
|
||||
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):
|
||||
"""
|
||||
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,
|
||||
related_name="%(class)s_delegated_votes",
|
||||
)
|
||||
user_token = models.CharField(default=generate_user_token, max_length=16)
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
voted = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)
|
||||
|
||||
db_votesvalid = models.DecimalField(
|
||||
votesvalid = models.DecimalField(
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(Decimal("-2"))],
|
||||
max_digits=15,
|
||||
decimal_places=6,
|
||||
)
|
||||
db_votesinvalid = models.DecimalField(
|
||||
votesinvalid = models.DecimalField(
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(Decimal("-2"))],
|
||||
max_digits=15,
|
||||
decimal_places=6,
|
||||
)
|
||||
db_votescast = models.DecimalField(
|
||||
votescast = models.DecimalField(
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(Decimal("-2"))],
|
||||
@ -160,12 +179,14 @@ class BasePoll(models.Model):
|
||||
PERCENT_BASE_YNA = "YNA"
|
||||
PERCENT_BASE_VALID = "valid"
|
||||
PERCENT_BASE_CAST = "cast"
|
||||
PERCENT_BASE_ENTITLED = "entitled"
|
||||
PERCENT_BASE_DISABLED = "disabled"
|
||||
PERCENT_BASES: Iterable[Tuple[str, str]] = (
|
||||
(PERCENT_BASE_YN, "Yes/No"),
|
||||
(PERCENT_BASE_YNA, "Yes/No/Abstain"),
|
||||
(PERCENT_BASE_VALID, "All valid ballots"),
|
||||
(PERCENT_BASE_CAST, "All casted ballots"),
|
||||
(PERCENT_BASE_ENTITLED, "All entitled users"),
|
||||
(PERCENT_BASE_DISABLED, "Disabled (no percents)"),
|
||||
) # type: ignore
|
||||
onehundred_percent_base = models.CharField(
|
||||
@ -186,57 +207,13 @@ class BasePoll(models.Model):
|
||||
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:
|
||||
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):
|
||||
""" Should be called after creation of this model. """
|
||||
raise NotImplementedError()
|
||||
@ -268,6 +245,8 @@ class BasePoll(models.Model):
|
||||
def pseudoanonymize(self):
|
||||
for option in self.get_options():
|
||||
option.pseudoanonymize()
|
||||
self.is_pseudoanonymized = True
|
||||
self.save()
|
||||
|
||||
def reset(self):
|
||||
for option in self.get_options():
|
||||
@ -281,4 +260,38 @@ class BasePoll(models.Model):
|
||||
self.votesvalid = None
|
||||
self.votesinvalid = 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()
|
||||
|
@ -5,6 +5,7 @@ from ..utils.rest_api import (
|
||||
CharField,
|
||||
DecimalField,
|
||||
IdPrimaryKeyRelatedField,
|
||||
JSONField,
|
||||
ModelSerializer,
|
||||
SerializerMethodField,
|
||||
ValidationError,
|
||||
@ -18,6 +19,7 @@ BASE_VOTE_FIELDS = (
|
||||
"value",
|
||||
"user",
|
||||
"delegated_user",
|
||||
"user_token",
|
||||
"option",
|
||||
"pollstate",
|
||||
)
|
||||
@ -58,7 +60,9 @@ BASE_POLL_FIELDS = (
|
||||
"id",
|
||||
"onehundred_percent_base",
|
||||
"majority_method",
|
||||
"is_pseudoanonymized",
|
||||
"voted",
|
||||
"entitled_users_at_stop",
|
||||
)
|
||||
|
||||
|
||||
@ -69,27 +73,21 @@ class BasePollSerializer(ModelSerializer):
|
||||
)
|
||||
options = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||
voted = IdPrimaryKeyRelatedField(many=True, read_only=True)
|
||||
|
||||
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
|
||||
)
|
||||
entitled_users_at_stop = JSONField(required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
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(
|
||||
validated_data["onehundred_percent_base"], validated_data["pollmethod"]
|
||||
)
|
||||
if new_100_percent_base is not None:
|
||||
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)
|
||||
|
||||
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
|
||||
possible (see implementing serializers to see forbidden combinations)
|
||||
|
||||
Also updates is_pseudoanonymized, if needed.
|
||||
"""
|
||||
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)
|
||||
|
||||
new_100_percent_base = self.norm_100_percent_base_to_pollmethod(
|
||||
|
@ -146,8 +146,7 @@ class BasePollViewSet(ModelViewSet):
|
||||
if poll.state != BasePoll.STATE_STARTED:
|
||||
raise ValidationError({"detail": "Wrong poll state"})
|
||||
|
||||
poll.state = BasePoll.STATE_FINISHED
|
||||
poll.save()
|
||||
poll.stop()
|
||||
inform_changed_data(poll.get_votes())
|
||||
inform_changed_data(poll.get_options())
|
||||
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.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from openslides.assignments.models import (
|
||||
Assignment,
|
||||
@ -108,7 +109,7 @@ class CreateAssignmentPoll(TestCase):
|
||||
self.assignment.add_candidate(self.admin)
|
||||
|
||||
def test_simple(self):
|
||||
with self.assertNumQueries(40):
|
||||
with self.assertNumQueries(38):
|
||||
response = self.client.post(
|
||||
reverse("assignmentpoll-list"),
|
||||
{
|
||||
@ -886,6 +887,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertEqual(AssignmentVote.objects.count(), 6)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
||||
self.assertEqual(poll.votescast, Decimal("-2"))
|
||||
@ -1056,6 +1058,7 @@ class VoteAssignmentPollAnalogYNA(VoteAssignmentPollBaseTestClass):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
||||
self.assertEqual(poll.votescast, Decimal("3"))
|
||||
@ -1081,6 +1084,7 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
@ -1099,11 +1103,11 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertEqual(AssignmentVote.objects.count(), 3)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
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())
|
||||
option1 = poll.options.get(pk=1)
|
||||
option2 = poll.options.get(pk=2)
|
||||
@ -1132,11 +1136,11 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertEqual(AssignmentVote.objects.count(), 3)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, weight)
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), weight)
|
||||
option1 = poll.options.get(pk=1)
|
||||
option2 = poll.options.get(pk=2)
|
||||
option3 = poll.options.get(pk=3)
|
||||
@ -1321,6 +1325,51 @@ class VoteAssignmentPollNamedYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
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):
|
||||
def create_poll(self):
|
||||
@ -1343,6 +1392,7 @@ class VoteAssignmentPollNamedY(VoteAssignmentPollBaseTestClass):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
@ -1360,6 +1410,7 @@ class VoteAssignmentPollNamedY(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertEqual(AssignmentVote.objects.count(), 1)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
@ -1671,6 +1722,7 @@ class VoteAssignmentPollNamedN(VoteAssignmentPollBaseTestClass):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
@ -1688,6 +1740,7 @@ class VoteAssignmentPollNamedN(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertEqual(AssignmentVote.objects.count(), 1)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
@ -1986,6 +2039,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
@ -2004,6 +2058,7 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertEqual(AssignmentVote.objects.count(), 3)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
@ -2163,6 +2218,22 @@ class VoteAssignmentPollPseudoanonymousYNA(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
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):
|
||||
def create_poll(self):
|
||||
@ -2185,6 +2256,7 @@ class VoteAssignmentPollPseudoanonymousY(VoteAssignmentPollBaseTestClass):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
@ -2202,6 +2274,7 @@ class VoteAssignmentPollPseudoanonymousY(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertEqual(AssignmentVote.objects.count(), 1)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
@ -2433,6 +2506,7 @@ class VoteAssignmentPollPseudoanonymousN(VoteAssignmentPollBaseTestClass):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.state, AssignmentPoll.STATE_STARTED)
|
||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
@ -2450,6 +2524,7 @@ class VoteAssignmentPollPseudoanonymousN(VoteAssignmentPollBaseTestClass):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
self.assertEqual(AssignmentVote.objects.count(), 1)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
@ -2681,8 +2756,9 @@ class PseudoanonymizeAssignmentPoll(TestCase):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = AssignmentPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.is_pseudoanonymized, True)
|
||||
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.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("2"))
|
||||
|
@ -112,6 +112,7 @@ class CreateMotionPoll(TestCase):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_201_CREATED)
|
||||
self.assertTrue(MotionPoll.objects.exists())
|
||||
poll = MotionPoll.objects.get()
|
||||
self.assertEqual(poll.is_pseudoanonymized, False)
|
||||
self.assertEqual(poll.title, "test_title_ailai4toogh3eefaa2Vo")
|
||||
self.assertEqual(poll.pollmethod, "YNA")
|
||||
self.assertEqual(poll.type, "named")
|
||||
@ -394,6 +395,27 @@ class UpdateMotionPoll(TestCase):
|
||||
poll = MotionPoll.objects.get()
|
||||
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):
|
||||
response = self.client.patch(
|
||||
reverse("motionpoll-detail", args=[self.poll.pk]), {"type": "invalid"}
|
||||
@ -585,6 +607,7 @@ class VoteMotionPollAnalog(TestCase):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
||||
self.assertEqual(poll.votescast, Decimal("-2"))
|
||||
@ -668,6 +691,7 @@ class VoteMotionPollAnalog(TestCase):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("4.64"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("-2"))
|
||||
self.assertEqual(poll.votescast, Decimal("3"))
|
||||
@ -715,6 +739,7 @@ class VoteMotionPollNamed(TestCase):
|
||||
response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk]))
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.state, MotionPoll.STATE_STARTED)
|
||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
@ -730,6 +755,7 @@ class VoteMotionPollNamed(TestCase):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
@ -754,11 +780,11 @@ class VoteMotionPollNamed(TestCase):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, weight)
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
self.assertEqual(poll.get_votes().count(), 1)
|
||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), weight)
|
||||
option = poll.options.get()
|
||||
self.assertEqual(option.yes, Decimal("0"))
|
||||
self.assertEqual(option.no, Decimal("0"))
|
||||
@ -784,6 +810,7 @@ class VoteMotionPollNamed(TestCase):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
poll = MotionPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
@ -874,6 +901,7 @@ class VoteMotionPollNamed(TestCase):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
@ -893,6 +921,7 @@ class VoteMotionPollNamed(TestCase):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("2"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("2"))
|
||||
@ -1007,6 +1036,7 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
||||
response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk]))
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.state, MotionPoll.STATE_STARTED)
|
||||
self.assertEqual(poll.votesvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
@ -1022,11 +1052,11 @@ class VoteMotionPollPseudoanonymous(TestCase):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.votesvalid, Decimal("1"))
|
||||
self.assertEqual(poll.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("1"))
|
||||
self.assertEqual(poll.get_votes().count(), 1)
|
||||
self.assertEqual(poll.amount_users_voted_with_individual_weight(), 1)
|
||||
option = poll.options.get()
|
||||
self.assertEqual(option.yes, Decimal("0"))
|
||||
self.assertEqual(option.no, Decimal("1"))
|
||||
@ -1142,6 +1172,42 @@ class StopMotionPoll(TestCase):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||
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):
|
||||
def advancedSetUp(self):
|
||||
@ -1213,8 +1279,9 @@ class PseudoanonymizeMotionPoll(TestCase):
|
||||
)
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
poll.calculate_votes()
|
||||
self.assertEqual(poll.is_pseudoanonymized, True)
|
||||
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.votesinvalid, Decimal("0"))
|
||||
self.assertEqual(poll.votescast, Decimal("2"))
|
||||
@ -1282,7 +1349,6 @@ class ResetMotionPoll(TestCase):
|
||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||
poll = MotionPoll.objects.get()
|
||||
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.votesinvalid, None)
|
||||
self.assertEqual(poll.votescast, None)
|
||||
@ -1292,6 +1358,24 @@ class ResetMotionPoll(TestCase):
|
||||
self.assertEqual(option.abstain, Decimal("0"))
|
||||
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):
|
||||
def advancedSetUp(self):
|
||||
|
@ -1,7 +1,6 @@
|
||||
from decimal import Decimal
|
||||
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()
|
||||
@ -51,25 +50,3 @@ class MotionChangeRecommendationTest(TestCase):
|
||||
other_recommendations
|
||||
)
|
||||
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