Enhance voting

- cleaned up a lot of code
- removed required majotiry from forms
- renamed verbose "Majority method" to "Required majority"
- poll-progress-bar only counts present user
- enhanced motion poll tile chart layout
- removed PercentBase.Votes
- added pollPercentBase pipe
- Show the voting percent next to chart in motion detail
- change the head bar to "Voting is open"
  and "Ballot is open"
- merged the voting configs to their corresponding config-categories
- re-add ballot paper configs
- Add "more" button to motion polls
- Adjusted the motion results table
  - Hide entries without information
  - Show icons for Y N A
  - Show percentage next to Y N A
This commit is contained in:
Sean 2020-02-04 16:25:42 +01:00 committed by FinnStutzenstein
parent 6c1317e25f
commit 524a97cdcc
22 changed files with 413 additions and 145 deletions

View File

@ -172,6 +172,7 @@ _('Number of all delegates');
_('Number of all participants');
_('Use the following custom number');
_('Custom number of ballot papers');
_('Voting');
// subgroup PDF export
_('PDF export');
_('Title for PDF documents of motions');
@ -278,8 +279,8 @@ _('Next states');
// other translations
_('Searching for candidates');
_('Voting');
_('Finished');
_('In the election process');
// ** Users **
// permission strings (see models.py of each Django app)

View File

@ -74,9 +74,9 @@ export class VotingBannerService {
private getTextForPoll(poll: ViewBasePoll): string {
return poll instanceof ViewMotionPoll
? `${this.translate.instant('Motion') + ' ' + poll.motion.getIdentifierOrTitle()}: ${this.translate.instant(
'Voting started'
)}!`
: `${poll.getTitle()}: ${this.translate.instant('Ballots opened')}!`;
'Voting is open'
)}`
: `${poll.getTitle()}: ${this.translate.instant('Ballot is open')}`;
}
/**

View File

@ -27,7 +27,6 @@ export enum PercentBase {
YN = 'YN',
YNA = 'YNA',
Valid = 'valid',
Votes = 'votes',
Cast = 'cast',
Disabled = 'disabled'
}
@ -68,6 +67,10 @@ export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends
return this.state === PollState.Published;
}
public get isPercentBaseValidOrCast(): boolean {
return this.onehundred_percent_base === PercentBase.Valid || this.onehundred_percent_base === PercentBase.Cast;
}
/**
* If the state is finished.
*/

View File

@ -0,0 +1,8 @@
import { PollPercentBasePipe } from './poll-percent-base.pipe';
describe('PollPercentBasePipe', () => {
it('create an instance', () => {
const pipe = new PollPercentBasePipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,36 @@
import { Pipe, PipeTransform } from '@angular/core';
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
/**
* Uses a number and a ViewPoll-object.
* Converts the number to the voting percent base using the
* given 100%-Base option in the poll object
*
* returns null if a percent calculation is not possible
* or the result is 0
*
* @example
* ```html
* <span> {{ voteYes | pollPercentBase: poll }} </span>
* ```
*/
@Pipe({
name: 'pollPercentBase'
})
export class PollPercentBasePipe implements PipeTransform {
private decimalPlaces = 3;
public transform(value: number, viewPoll: ViewBasePoll): string | null {
const totalByBase = viewPoll.getPercentBase();
if (totalByBase) {
const percentNumber = (value / totalByBase) * 100;
if (percentNumber > 0) {
const result = percentNumber % 1 === 0 ? percentNumber : percentNumber.toFixed(this.decimalPlaces);
return `(${result}%)`;
}
}
return null;
}
}

View File

@ -122,6 +122,7 @@ import { AssignmentPollDialogComponent } from 'app/site/assignments/components/a
import { ParsePollNumberPipe } from './pipes/parse-poll-number.pipe';
import { ReversePipe } from './pipes/reverse.pipe';
import { PollKeyVerbosePipe } from './pipes/poll-key-verbose.pipe';
import { PollPercentBasePipe } from './pipes/poll-percent-base.pipe';
/**
* Share Module for all "dumb" components and pipes.
@ -285,7 +286,8 @@ import { PollKeyVerbosePipe } from './pipes/poll-key-verbose.pipe';
AssignmentPollDialogComponent,
ParsePollNumberPipe,
ReversePipe,
PollKeyVerbosePipe
PollKeyVerbosePipe,
PollPercentBasePipe
],
declarations: [
PermsDirective,
@ -342,7 +344,8 @@ import { PollKeyVerbosePipe } from './pipes/poll-key-verbose.pipe';
AssignmentPollDialogComponent,
ParsePollNumberPipe,
ReversePipe,
PollKeyVerbosePipe
PollKeyVerbosePipe,
PollPercentBasePipe
],
providers: [
{
@ -361,7 +364,8 @@ import { PollKeyVerbosePipe } from './pipes/poll-key-verbose.pipe';
LocalizedDatePipe,
ParsePollNumberPipe,
ReversePipe,
PollKeyVerbosePipe
PollKeyVerbosePipe,
PollPercentBasePipe
],
entryComponents: [
SortBottomSheetComponent,

View File

@ -86,6 +86,10 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
return data;
}
public getPercentBase(): number {
return 0;
}
}
export interface ViewAssignmentPoll extends AssignmentPoll {

View File

@ -27,7 +27,7 @@ export const AssignmentPhases: { name: string; value: number; display_name: stri
{
name: 'PHASE_VOTING',
value: 1,
display_name: 'Voting'
display_name: 'In the election process'
},
{
name: 'PHASE_FINISHED',

View File

@ -9,6 +9,19 @@ export class ViewMotionOption extends BaseViewModel<MotionOption> {
}
public static COLLECTIONSTRING = MotionOption.COLLECTIONSTRING;
protected _collectionString = MotionOption.COLLECTIONSTRING;
public sumYN(): number {
let sum = 0;
sum += this.yes > 0 ? this.yes : 0;
sum += this.no > 0 ? this.no : 0;
return sum;
}
public sumYNA(): number {
let sum = this.sumYN();
sum += this.abstain > 0 ? this.abstain : 0;
return sum;
}
}
interface TIMotionOptionRelations {

View File

@ -1,6 +1,6 @@
import { ChartData } from 'app/shared/components/charts/charts.component';
import { MotionPoll, MotionPollMethods } from 'app/shared/models/motions/motion-poll';
import { PollColor, PollState } from 'app/shared/models/poll/base-poll';
import { PercentBase, PollColor, PollState } from 'app/shared/models/poll/base-poll';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
@ -16,14 +16,57 @@ export const MotionPollMethodsVerbose = {
YNA: 'Yes/No/Abstain'
};
interface TableKey {
vote: string;
icon?: string;
canHide: boolean;
showPercent: boolean;
}
export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPollTitleInformation {
public static COLLECTIONSTRING = MotionPoll.COLLECTIONSTRING;
protected _collectionString = MotionPoll.COLLECTIONSTRING;
public readonly pollClassType: 'assignment' | 'motion' = 'motion';
private tableKeys = ['yes', 'no', 'abstain'];
private voteKeys = ['votesvalid', 'votesinvalid', 'votescast'];
private tableKeys: TableKey[] = [
{
vote: 'yes',
icon: 'thumb_up',
canHide: false,
showPercent: true
},
{
vote: 'no',
icon: 'thumb_down',
canHide: false,
showPercent: true
},
{
vote: 'abstain',
icon: 'trip_origin',
canHide: false,
showPercent: true
}
];
private voteKeys: TableKey[] = [
{
vote: 'votesvalid',
canHide: true,
showPercent: this.poll.isPercentBaseValidOrCast
},
{
vote: 'votesinvalid',
canHide: true,
showPercent: this.poll.isPercentBaseValidOrCast
},
{
vote: 'votescast',
canHide: true,
showPercent: this.poll.isPercentBaseValidOrCast
}
];
public get hasVotes(): boolean {
return !!this.options[0].votes.length;
@ -38,9 +81,19 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
}
public generateTableData(): PollData[] {
let tableData = this.options.flatMap(vote => this.tableKeys.map(key => ({ key: key, value: vote[key] })));
tableData.push(...this.voteKeys.map(key => ({ key: key, value: this[key] })));
tableData = tableData.map(entry => (entry.value >= 0 ? entry : { key: entry.key, value: null }));
let tableData = this.options.flatMap(vote =>
this.tableKeys.map(key => ({
key: key.vote,
value: vote[key.vote],
canHide: key.canHide,
icon: key.icon,
showPercent: key.showPercent
}))
);
tableData.push(
...this.voteKeys.map(key => ({ key: key.vote, value: this[key.vote], showPercent: key.showPercent }))
);
tableData = tableData.filter(entry => entry.canHide === false || entry.value || entry.value !== -2);
return tableData;
}
@ -76,6 +129,10 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
return MotionPollMethodsVerbose[this.pollmethod];
}
public anySpecialVotes(): boolean {
return this.options[0].yes < 0 || this.options[0].no < 0 || this.options[0].abstain < 0;
}
/**
* Override from base poll to skip started state in analog poll type
*/
@ -85,6 +142,40 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
}
return super.getNextStates();
}
public getPercentBase(): number {
const base: PercentBase = this.poll.onehundred_percent_base;
const options = this.options[0];
let totalByBase: number;
switch (base) {
case PercentBase.YN:
if (options.yes >= 0 && options.no >= 0) {
totalByBase = options.sumYN();
}
break;
case PercentBase.YNA:
if (options.yes >= 0 && options.no >= 0 && options.abstain >= 0) {
totalByBase = options.sumYNA();
}
break;
case PercentBase.Valid:
// auslagern
if (options.yes >= 0 && options.no >= 0 && options.abstain >= 0) {
totalByBase = this.poll.votesvalid;
}
break;
case PercentBase.Cast:
totalByBase = this.poll.votescast;
break;
case PercentBase.Disabled:
break;
default:
throw new Error('The given poll has no percent base: ' + this);
}
return totalByBase;
}
}
export interface ViewMotionPoll extends MotionPoll {

View File

@ -43,34 +43,51 @@
<th translate>Votes</th>
</tr>
<tr *ngFor="let row of poll.tableData">
<td>{{ row.key | pollKeyVerbose | translate }}</td>
<td class="result-cell-definition">{{ row.value }}</td>
<td>
<os-icon-container *ngIf="row.icon" [icon]="row.icon">
{{ row.key | pollKeyVerbose | translate }}
</os-icon-container>
<span *ngIf="!row.icon">
{{ row.key | pollKeyVerbose | translate }}
</span>
</td>
<td class="result-cell-definition">
{{ row.value | parsePollNumber }}
<span *ngIf="row.showPercent">
{{ row.value | pollPercentBase: poll }}
</span>
</td>
</tr>
</tbody>
</table>
<!-- Named table: only show if votes are present -->
<div class="named-result-table" *ngIf="poll.type === 'named' && votesDataSource.data">
<div class="named-result-table" *ngIf="poll.type === 'named'">
<h3>{{ 'Single votes' | translate }}</h3>
<mat-form-field>
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter" />
</mat-form-field>
<mat-table [dataSource]="votesDataSource">
<ng-container matColumnDef="key" sticky>
<mat-header-cell *matHeaderCellDef>{{ 'User' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let vote">
<div *ngIf="vote.user">{{ vote.user.getFullName() }}</div>
<div *ngIf="!vote.user">{{ 'Unknown user' | translate }}</div>
</mat-cell>
</ng-container>
<ng-container matColumnDef="value" sticky>
<mat-header-cell *matHeaderCellDef>{{ 'Vote' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let vote">{{ vote.valueVerbose }}</mat-cell>
</ng-container>
<div *ngIf="votesDataSource.data">
<mat-form-field>
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter" />
</mat-form-field>
<mat-table [dataSource]="votesDataSource">
<ng-container matColumnDef="key" sticky>
<mat-header-cell *matHeaderCellDef>{{ 'Participant' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let vote">
<div *ngIf="vote.user">{{ vote.user.getFullName() }}</div>
<div *ngIf="!vote.user">{{ 'Anonymous' | translate }}</div>
</mat-cell>
</ng-container>
<ng-container matColumnDef="value" sticky>
<mat-header-cell *matHeaderCellDef>{{ 'Vote' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let vote">{{ vote.valueVerbose }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
<mat-row *matRowDef="let vote; columns: columnDefinition"></mat-row>
</mat-table>
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
<mat-row *matRowDef="let vote; columns: columnDefinition"></mat-row>
</mat-table>
</div>
<div *ngIf="!votesDataSource.data">
{{ 'The individual votes were made anonymous.' | translate }}
</div>
</div>
</div>
</div>

View File

@ -42,7 +42,7 @@
</div>
<!-- Publish immediately button. Only show for new polls -->
<div *ngIf="!pollData.state">
<div *ngIf="!pollData.isStatePublished">
<mat-checkbox [(ngModel)]="publishImmediately" (change)="publishStateChanged($event.checked)">
<span translate>Publish immediately</span>
</mat-checkbox>

View File

@ -55,53 +55,60 @@
<ng-template #viewTemplate>
<div class="poll-chart-wrapper">
<!-- empty helper div to center the grid wrapper -->
<div></div>
<div class="doughnut-chart">
<os-charts *ngIf="showChart" [type]="'doughnut'" [data]="chartDataSubject" [showLegend]="false">
<os-charts
*ngIf="showChart"
[type]="'doughnut'"
[data]="chartDataSubject"
[showLegend]="false"
[hasPadding]="false"
>
</os-charts>
</div>
<div class="vote-legend">
<div class="votes-yes" *ngIf="isVoteDocumented(voteYes)">
<os-icon-container icon="thumb_up" size="large">
{{ voteYes | parsePollNumber }}
{{ voteYes | pollPercentBase: poll }}
</os-icon-container>
</div>
<div class="votes-no" *ngIf="isVoteDocumented(voteNo)">
<os-icon-container icon="thumb_down" size="large">
{{ voteNo | parsePollNumber }}
{{ voteNo | pollPercentBase: poll }}
</os-icon-container>
</div>
<div class="votes-abstain" *ngIf="isVoteDocumented(voteAbstain)">
<os-icon-container icon="trip_origin" size="large">
{{ voteAbstain | parsePollNumber }}
{{ voteAbstain | pollPercentBase: poll }}
</os-icon-container>
</div>
</div>
</div>
<div class="poll-detail-button-wrapper" *ngIf="poll.type !== 'analog'">
<div class="poll-detail-button-wrapper">
<button mat-button [routerLink]="pollLink">
{{ 'Single Votes' | translate }}
{{ 'More' | translate }}
</button>
</div>
</ng-template>
<ng-template #emptyTemplate>
<div *osPerms="'motions.can_manage_polls'">
{{ 'An empty poll - you have to enter votes.' | translate }}
{{ 'Edit to enter votes.' | translate }}
</div>
</ng-template>
<!-- More Menu -->
<mat-menu #pollDetailMenu="matMenu">
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
<button *osPerms="'motions.can_manage_polls'" mat-menu-item (click)="openDialog()">
<mat-icon>edit</mat-icon>
<span translate>Edit</span>
</button>
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
<button mat-menu-item (click)="downloadPdf()">
<mat-icon>picture_as_pdf</mat-icon>
<span translate>PDF</span>
<span translate>Ballot paper</span>
</button>
<div *osPerms="'motions.can_manage_polls'">
<mat-divider></mat-divider>

View File

@ -51,15 +51,20 @@
.poll-chart-wrapper {
cursor: pointer;
display: grid;
grid-gap: 10px;
grid-template-areas: 'placeholder chart legend';
grid-template-columns: auto minmax(50px, 20%) auto;
.doughnut-chart {
grid-area: chart;
margin-top: auto;
margin-bottom: auto;
}
.vote-legend {
margin: auto 10px;
grid-area: legend;
margin-top: auto;
margin-bottom: auto;
div + div {
margin-top: 10px;

View File

@ -1,23 +1,25 @@
<div class="os-form-card-mobile">
<!-- Poll Title -->
<form [formGroup]="contentForm">
<mat-form-field>
<h2 class="poll-preview--title">
<h2 class="poll-preview-title">
<input matInput required formControlName="title" [placeholder]="'Title' | translate" />
</h2>
</mat-form-field>
</form>
<div *ngIf="data && data.state > 1" class="poll-preview-meta-info">
<span class="short-description" *ngFor="let value of pollValues">
<span class="short-description--label subtitle" translate>
<span class="short-description-label subtitle" translate>
{{ value[0] }}
</span>
<span class="short-description--value" translate>
<span class="short-description-value" translate>
{{ value[1] }}
</span>
</span>
</div>
<form [formGroup]="contentForm" class="poll-preview--meta-info-form">
<form [formGroup]="contentForm" class="poll-preview-meta-info-form">
<ng-container *ngIf="!data || !data.state || data.isStateCreated">
<!-- Poll Type -->
<mat-form-field *ngIf="pollService.isElectronicVotingEnabled">
<mat-select [placeholder]="PollPropertyVerbose.type | translate" formControlName="type" required>
<mat-option *ngFor="let option of pollTypes | keyvalue" [value]="option.key">
@ -26,7 +28,9 @@
</mat-select>
<mat-error translate>This field is required</mat-error>
</mat-form-field>
<mat-form-field *ngIf="contentForm.get('type').value && contentForm.get('type').value != 'analog'">
<!-- Groups entitled to Vote -->
<mat-form-field *ngIf="contentForm.get('type').value && contentForm.get('type').value !== 'analog'">
<os-search-value-selector
formControlName="groups_id"
[multiple]="true"
@ -36,8 +40,14 @@
[inputListValues]="groupObservable"
></os-search-value-selector>
</mat-form-field>
<!-- Poll Methods -->
<mat-form-field *ngIf="pollMethods">
<mat-select [placeholder]="PollPropertyVerbose.pollmethod | translate" formControlName="pollmethod" required>
<mat-select
[placeholder]="PollPropertyVerbose.pollmethod | translate"
formControlName="pollmethod"
required
>
<mat-option *ngFor="let option of pollMethods | keyvalue" [value]="option.key">
{{ option.value }}
</mat-option>
@ -46,29 +56,37 @@
</mat-form-field>
</ng-container>
<!-- 100 Percent Base -->
<mat-form-field>
<mat-select placeholder="{{ PollPropertyVerbose.onehundred_percent_base | translate }}" formControlName="onehundred_percent_base" required>
<mat-select
placeholder="{{ PollPropertyVerbose.onehundred_percent_base | translate }}"
formControlName="onehundred_percent_base"
required
>
<ng-container *ngFor="let option of percentBases | keyvalue">
<mat-option [value]="option.key">{{ option.value | translate }}</mat-option>
</ng-container>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-select placeholder="{{ PollPropertyVerbose.majority_method | translate }}" formControlName="majority_method" required>
<mat-option *ngFor="let option of majorityMethods | keyvalue" [value]="option.key">
{{ option.value | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<ng-container *ngIf="!data || !data.state || data.state === 1">
<ng-container *ngIf="contentForm.get('pollmethod').value === 'votes'">
<mat-form-field>
<input type="number" matInput placeholder="{{ PollPropertyVerbose.votes_amount | translate }}" formControlName="votes_amount" min="1" required>
</mat-form-field>
<mat-checkbox formControlName="global_no">{{ PollPropertyVerbose.global_no | translate }}</mat-checkbox>
<mat-checkbox formControlName="global_abstain">{{ PollPropertyVerbose.global_abstain | translate }}</mat-checkbox>
</ng-container>
<!-- Amount of Votes -->
<ng-container
*ngIf="contentForm.get('pollmethod').value === 'votes' && (!data || !data.state || data.isStateCreated)"
>
<mat-form-field>
<input
type="number"
matInput
placeholder="{{ PollPropertyVerbose.votes_amount | translate }}"
formControlName="votes_amount"
min="1"
required
/>
</mat-form-field>
<mat-checkbox formControlName="global_no">{{ PollPropertyVerbose.global_no | translate }}</mat-checkbox>
<mat-checkbox formControlName="global_abstain">{{
PollPropertyVerbose.global_abstain | translate
}}</mat-checkbox>
</ng-container>
</form>
</div>

View File

@ -1,4 +1,4 @@
.poll-preview--title {
.poll-preview-title {
margin: 0;
}
@ -14,13 +14,13 @@
span {
display: block;
}
&--label {
&-label {
font-size: 75%;
}
}
}
.poll-preview--meta-info-form {
.poll-preview-meta-info-form {
display: flex;
align-items: center;
flex-wrap: wrap;

View File

@ -114,9 +114,10 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
this.data.groups_id = this.configService.instant('motion_poll_default_groups');
}
}
Object.keys(this.contentForm.controls).forEach(key => {
if (this.data[key]) {
this.contentForm.get(key).setValue(this.data[key]);
this.contentForm.get(key).patchValue(this.data[key]);
}
});
}
@ -132,9 +133,9 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
this.contentForm.get('pollmethod').valueChanges.subscribe(method => {
let forbiddenBases: string[];
if (method === 'YN') {
forbiddenBases = [PercentBase.YNA, PercentBase.Votes];
forbiddenBases = [PercentBase.YNA, PercentBase.Cast];
} else if (method === 'YNA') {
forbiddenBases = [PercentBase.Votes];
forbiddenBases = [PercentBase.Cast];
} else if (method === 'votes') {
forbiddenBases = [PercentBase.YN, PercentBase.YNA];
}

View File

@ -1,3 +1,3 @@
<span>{{ poll.voted_id.length }} von {{ max }} haben abgestimmt.</span>
<span> {{ 'Casted votes' }}: {{ poll.voted_id.length }} / {{ max }} </span>
<mat-progress-bar class="voting-progress-bar" [value]="valueInPercent"></mat-progress-bar>

View File

@ -40,7 +40,11 @@ export class PollProgressComponent extends BaseViewComponent implements OnInit {
public ngOnInit(): void {
this.userRepo
.getViewModelListObservable()
.pipe(map(users => users.filter(user => this.poll.groups_id.intersect(user.groups_id).length)))
.pipe(
map(users =>
users.filter(user => user.is_present && this.poll.groups_id.intersect(user.groups_id).length)
)
)
.subscribe(users => {
this.max = users.length;
});

View File

@ -21,8 +21,11 @@ export interface PollData {
value?: number;
yes?: number;
no?: number;
abstain: number;
abstain?: number;
user?: string;
canHide?: boolean;
icon?: string;
showPercent?: boolean;
}
export const PollClassTypeVerbose = {
@ -40,7 +43,7 @@ export const PollStateVerbose = {
export const PollStateChangeActionVerbose = {
1: 'Reset',
2: 'Start voting',
3: 'End voting',
3: 'Stop voting',
4: 'Publish'
};
@ -51,7 +54,7 @@ export const PollTypeVerbose = {
};
export const PollPropertyVerbose = {
majority_method: 'Majority method',
majority_method: 'Required majority',
onehundred_percent_base: '100% base',
type: 'Poll type',
pollmethod: 'Poll method',
@ -69,10 +72,12 @@ export const MajorityMethodVerbose = {
disabled: 'Disabled'
};
/**
* TODO: These need to be in order
*/
export const PercentBaseVerbose = {
YN: 'Yes/No',
YNA: 'Yes/No/Abstain',
votes: 'All votes',
valid: 'Valid votes',
cast: 'Total votes cast',
disabled: 'Disabled'
@ -145,6 +150,8 @@ export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends Bas
public abstract generateChartData(): ChartData;
public abstract generateTableData(): PollData[];
public abstract getPercentBase(): number;
}
export interface ViewBasePoll<M extends BasePoll<M, any> = any> extends BasePoll<M, any> {

View File

@ -1,3 +1,5 @@
from django.core.validators import MinValueValidator
from openslides.assignments.models import AssignmentPoll
from openslides.core.config import ConfigVariable
@ -19,8 +21,8 @@ def get_config_variables():
for base in AssignmentPoll.PERCENT_BASES
),
weight=400,
group="Voting",
subgroup="Elections",
group="Elections",
subgroup="Voting",
)
yield ConfigVariable(
@ -35,18 +37,8 @@ def get_config_variables():
help_text="Default method to check whether a candidate has reached the required majority.",
weight=405,
hidden=True,
group="Voting",
subgroup="Elections",
)
yield ConfigVariable(
name="assignment_poll_add_candidates_to_list_of_speakers",
default_value=True,
input_type="boolean",
label="Put all candidates on the list of speakers",
weight=410,
group="Voting",
subgroup="Elections",
group="Elections",
subgroup="Voting",
)
yield ConfigVariable(
@ -54,9 +46,52 @@ def get_config_variables():
default_value=[],
input_type="groups",
label="Default groups for named and pseudoanonymous assignment polls",
weight=410,
group="Elections",
subgroup="Voting",
)
yield ConfigVariable(
name="assignment_poll_add_candidates_to_list_of_speakers",
default_value=True,
input_type="boolean",
label="Put all candidates on the list of speakers",
weight=415,
group="Voting",
subgroup="Elections",
group="Elections",
subgroup="Voting",
)
# Ballot Paper
yield ConfigVariable(
name="assignments_pdf_ballot_papers_selection",
default_value="CUSTOM_NUMBER",
input_type="choice",
label="Number of ballot papers (selection)",
choices=(
{"value": "NUMBER_OF_DELEGATES", "display_name": "Number of all delegates"},
{
"value": "NUMBER_OF_ALL_PARTICIPANTS",
"display_name": "Number of all participants",
},
{
"value": "CUSTOM_NUMBER",
"display_name": "Use the following custom number",
},
),
weight=430,
group="Elections",
subgroup="Ballot papers",
)
yield ConfigVariable(
name="assignments_pdf_ballot_papers_number",
default_value=8,
input_type="integer",
label="Custom number of ballot papers",
weight=435,
group="Elections",
subgroup="Ballot papers",
validators=(MinValueValidator(1),),
)
# PDF

View File

@ -332,22 +332,77 @@ def get_config_variables():
# Voting and ballot papers
yield ConfigVariable(
name="motions_poll_100_percent_base",
default_value="YES_NO_ABSTAIN",
name="motion_poll_default_100_percent_base",
default_value="YNA",
input_type="choice",
label="The 100 % base of a voting result consists of",
choices=(
{"value": "YES_NO_ABSTAIN", "display_name": "Yes/No/Abstain"},
{"value": "YES_NO", "display_name": "Yes/No"},
{"value": "VALID", "display_name": "All valid ballots"},
{"value": "CAST", "display_name": "All casted ballots"},
{"value": "DISABLED", "display_name": "Disabled (no percents)"},
choices=tuple(
{"value": base[0], "display_name": base[1]}
for base in MotionPoll.PERCENT_BASES
),
weight=370,
group="Motions",
subgroup="Voting and ballot papers",
)
yield ConfigVariable(
name="motion_poll_default_majority_method",
default_value="simple",
input_type="choice",
choices=tuple(
{"value": method[0], "display_name": method[1]}
for method in MotionPoll.MAJORITY_METHODS
),
label="Required majority",
help_text="Default method to check whether a motion has reached the required majority.",
weight=371,
hidden=True,
group="Motions",
subgroup="Voting and ballot papers",
)
yield ConfigVariable(
name="motion_poll_default_groups",
default_value=[],
input_type="groups",
label="Default groups for named and pseudoanonymous motion polls",
weight=372,
group="Motions",
subgroup="Voting and ballot papers",
)
yield ConfigVariable(
name="motions_pdf_ballot_papers_selection",
default_value="CUSTOM_NUMBER",
input_type="choice",
label="Number of ballot papers (selection)",
choices=(
{"value": "NUMBER_OF_DELEGATES", "display_name": "Number of all delegates"},
{
"value": "NUMBER_OF_ALL_PARTICIPANTS",
"display_name": "Number of all participants",
},
{
"value": "CUSTOM_NUMBER",
"display_name": "Use the following custom number",
},
),
weight=373,
group="Motions",
subgroup="Voting and ballot papers",
)
yield ConfigVariable(
name="motions_pdf_ballot_papers_number",
default_value=8,
input_type="integer",
label="Custom number of ballot papers",
weight=374,
group="Motions",
subgroup="Voting and ballot papers",
validators=(MinValueValidator(1),),
)
# PDF export
yield ConfigVariable(
@ -387,44 +442,3 @@ def get_config_variables():
group="Motions",
subgroup="PDF export",
)
# Voting
yield ConfigVariable(
name="motion_poll_default_100_percent_base",
default_value="YNA",
input_type="choice",
label="The 100 % base of a voting result consists of",
choices=tuple(
{"value": base[0], "display_name": base[1]}
for base in MotionPoll.PERCENT_BASES
),
weight=420,
group="Voting",
subgroup="Motions",
)
yield ConfigVariable(
name="motion_poll_default_majority_method",
default_value="simple",
input_type="choice",
choices=tuple(
{"value": method[0], "display_name": method[1]}
for method in MotionPoll.MAJORITY_METHODS
),
label="Required majority",
help_text="Default method to check whether a motion has reached the required majority.",
weight=425,
hidden=True,
group="Voting",
subgroup="Motions",
)
yield ConfigVariable(
name="motion_poll_default_groups",
default_value=[],
input_type="groups",
label="Default groups for named and pseudoanonymous motion polls",
weight=430,
group="Voting",
subgroup="Motions",
)