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:
parent
6c1317e25f
commit
524a97cdcc
@ -172,6 +172,7 @@ _('Number of all delegates');
|
|||||||
_('Number of all participants');
|
_('Number of all participants');
|
||||||
_('Use the following custom number');
|
_('Use the following custom number');
|
||||||
_('Custom number of ballot papers');
|
_('Custom number of ballot papers');
|
||||||
|
_('Voting');
|
||||||
// subgroup PDF export
|
// subgroup PDF export
|
||||||
_('PDF export');
|
_('PDF export');
|
||||||
_('Title for PDF documents of motions');
|
_('Title for PDF documents of motions');
|
||||||
@ -278,8 +279,8 @@ _('Next states');
|
|||||||
|
|
||||||
// other translations
|
// other translations
|
||||||
_('Searching for candidates');
|
_('Searching for candidates');
|
||||||
_('Voting');
|
|
||||||
_('Finished');
|
_('Finished');
|
||||||
|
_('In the election process');
|
||||||
|
|
||||||
// ** Users **
|
// ** Users **
|
||||||
// permission strings (see models.py of each Django app)
|
// permission strings (see models.py of each Django app)
|
||||||
|
@ -74,9 +74,9 @@ export class VotingBannerService {
|
|||||||
private getTextForPoll(poll: ViewBasePoll): string {
|
private getTextForPoll(poll: ViewBasePoll): string {
|
||||||
return poll instanceof ViewMotionPoll
|
return poll instanceof ViewMotionPoll
|
||||||
? `${this.translate.instant('Motion') + ' ' + poll.motion.getIdentifierOrTitle()}: ${this.translate.instant(
|
? `${this.translate.instant('Motion') + ' ' + poll.motion.getIdentifierOrTitle()}: ${this.translate.instant(
|
||||||
'Voting started'
|
'Voting is open'
|
||||||
)}!`
|
)}`
|
||||||
: `${poll.getTitle()}: ${this.translate.instant('Ballots opened')}!`;
|
: `${poll.getTitle()}: ${this.translate.instant('Ballot is open')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,7 +27,6 @@ export enum PercentBase {
|
|||||||
YN = 'YN',
|
YN = 'YN',
|
||||||
YNA = 'YNA',
|
YNA = 'YNA',
|
||||||
Valid = 'valid',
|
Valid = 'valid',
|
||||||
Votes = 'votes',
|
|
||||||
Cast = 'cast',
|
Cast = 'cast',
|
||||||
Disabled = 'disabled'
|
Disabled = 'disabled'
|
||||||
}
|
}
|
||||||
@ -68,6 +67,10 @@ export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends
|
|||||||
return this.state === PollState.Published;
|
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.
|
* If the state is finished.
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import { PollPercentBasePipe } from './poll-percent-base.pipe';
|
||||||
|
|
||||||
|
describe('PollPercentBasePipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new PollPercentBasePipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
36
client/src/app/shared/pipes/poll-percent-base.pipe.ts
Normal file
36
client/src/app/shared/pipes/poll-percent-base.pipe.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -122,6 +122,7 @@ import { AssignmentPollDialogComponent } from 'app/site/assignments/components/a
|
|||||||
import { ParsePollNumberPipe } from './pipes/parse-poll-number.pipe';
|
import { ParsePollNumberPipe } from './pipes/parse-poll-number.pipe';
|
||||||
import { ReversePipe } from './pipes/reverse.pipe';
|
import { ReversePipe } from './pipes/reverse.pipe';
|
||||||
import { PollKeyVerbosePipe } from './pipes/poll-key-verbose.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.
|
* Share Module for all "dumb" components and pipes.
|
||||||
@ -285,7 +286,8 @@ import { PollKeyVerbosePipe } from './pipes/poll-key-verbose.pipe';
|
|||||||
AssignmentPollDialogComponent,
|
AssignmentPollDialogComponent,
|
||||||
ParsePollNumberPipe,
|
ParsePollNumberPipe,
|
||||||
ReversePipe,
|
ReversePipe,
|
||||||
PollKeyVerbosePipe
|
PollKeyVerbosePipe,
|
||||||
|
PollPercentBasePipe
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
@ -342,7 +344,8 @@ import { PollKeyVerbosePipe } from './pipes/poll-key-verbose.pipe';
|
|||||||
AssignmentPollDialogComponent,
|
AssignmentPollDialogComponent,
|
||||||
ParsePollNumberPipe,
|
ParsePollNumberPipe,
|
||||||
ReversePipe,
|
ReversePipe,
|
||||||
PollKeyVerbosePipe
|
PollKeyVerbosePipe,
|
||||||
|
PollPercentBasePipe
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
@ -361,7 +364,8 @@ import { PollKeyVerbosePipe } from './pipes/poll-key-verbose.pipe';
|
|||||||
LocalizedDatePipe,
|
LocalizedDatePipe,
|
||||||
ParsePollNumberPipe,
|
ParsePollNumberPipe,
|
||||||
ReversePipe,
|
ReversePipe,
|
||||||
PollKeyVerbosePipe
|
PollKeyVerbosePipe,
|
||||||
|
PollPercentBasePipe
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
SortBottomSheetComponent,
|
SortBottomSheetComponent,
|
||||||
|
@ -86,6 +86,10 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPercentBase(): number {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewAssignmentPoll extends AssignmentPoll {
|
export interface ViewAssignmentPoll extends AssignmentPoll {
|
||||||
|
@ -27,7 +27,7 @@ export const AssignmentPhases: { name: string; value: number; display_name: stri
|
|||||||
{
|
{
|
||||||
name: 'PHASE_VOTING',
|
name: 'PHASE_VOTING',
|
||||||
value: 1,
|
value: 1,
|
||||||
display_name: 'Voting'
|
display_name: 'In the election process'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'PHASE_FINISHED',
|
name: 'PHASE_FINISHED',
|
||||||
|
@ -9,6 +9,19 @@ export class ViewMotionOption extends BaseViewModel<MotionOption> {
|
|||||||
}
|
}
|
||||||
public static COLLECTIONSTRING = MotionOption.COLLECTIONSTRING;
|
public static COLLECTIONSTRING = MotionOption.COLLECTIONSTRING;
|
||||||
protected _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 {
|
interface TIMotionOptionRelations {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ChartData } from 'app/shared/components/charts/charts.component';
|
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||||
import { MotionPoll, MotionPollMethods } from 'app/shared/models/motions/motion-poll';
|
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 { BaseViewModel } from 'app/site/base/base-view-model';
|
||||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||||
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||||
@ -16,14 +16,57 @@ export const MotionPollMethodsVerbose = {
|
|||||||
YNA: 'Yes/No/Abstain'
|
YNA: 'Yes/No/Abstain'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface TableKey {
|
||||||
|
vote: string;
|
||||||
|
icon?: string;
|
||||||
|
canHide: boolean;
|
||||||
|
showPercent: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPollTitleInformation {
|
export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPollTitleInformation {
|
||||||
public static COLLECTIONSTRING = MotionPoll.COLLECTIONSTRING;
|
public static COLLECTIONSTRING = MotionPoll.COLLECTIONSTRING;
|
||||||
protected _collectionString = MotionPoll.COLLECTIONSTRING;
|
protected _collectionString = MotionPoll.COLLECTIONSTRING;
|
||||||
|
|
||||||
public readonly pollClassType: 'assignment' | 'motion' = 'motion';
|
public readonly pollClassType: 'assignment' | 'motion' = 'motion';
|
||||||
|
|
||||||
private tableKeys = ['yes', 'no', 'abstain'];
|
private tableKeys: TableKey[] = [
|
||||||
private voteKeys = ['votesvalid', 'votesinvalid', 'votescast'];
|
{
|
||||||
|
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 {
|
public get hasVotes(): boolean {
|
||||||
return !!this.options[0].votes.length;
|
return !!this.options[0].votes.length;
|
||||||
@ -38,9 +81,19 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateTableData(): PollData[] {
|
public generateTableData(): PollData[] {
|
||||||
let tableData = this.options.flatMap(vote => this.tableKeys.map(key => ({ key: key, value: vote[key] })));
|
let tableData = this.options.flatMap(vote =>
|
||||||
tableData.push(...this.voteKeys.map(key => ({ key: key, value: this[key] })));
|
this.tableKeys.map(key => ({
|
||||||
tableData = tableData.map(entry => (entry.value >= 0 ? entry : { key: entry.key, value: null }));
|
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;
|
return tableData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,6 +129,10 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
|
|||||||
return MotionPollMethodsVerbose[this.pollmethod];
|
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
|
* 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();
|
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 {
|
export interface ViewMotionPoll extends MotionPoll {
|
||||||
|
@ -43,34 +43,51 @@
|
|||||||
<th translate>Votes</th>
|
<th translate>Votes</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngFor="let row of poll.tableData">
|
<tr *ngFor="let row of poll.tableData">
|
||||||
<td>{{ row.key | pollKeyVerbose | translate }}</td>
|
<td>
|
||||||
<td class="result-cell-definition">{{ row.value }}</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Named table: only show if votes are present -->
|
<!-- 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>
|
<h3>{{ 'Single votes' | translate }}</h3>
|
||||||
<mat-form-field>
|
<div *ngIf="votesDataSource.data">
|
||||||
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter" />
|
<mat-form-field>
|
||||||
</mat-form-field>
|
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter" />
|
||||||
<mat-table [dataSource]="votesDataSource">
|
</mat-form-field>
|
||||||
<ng-container matColumnDef="key" sticky>
|
<mat-table [dataSource]="votesDataSource">
|
||||||
<mat-header-cell *matHeaderCellDef>{{ 'User' | translate }}</mat-header-cell>
|
<ng-container matColumnDef="key" sticky>
|
||||||
<mat-cell *matCellDef="let vote">
|
<mat-header-cell *matHeaderCellDef>{{ 'Participant' | translate }}</mat-header-cell>
|
||||||
<div *ngIf="vote.user">{{ vote.user.getFullName() }}</div>
|
<mat-cell *matCellDef="let vote">
|
||||||
<div *ngIf="!vote.user">{{ 'Unknown user' | translate }}</div>
|
<div *ngIf="vote.user">{{ vote.user.getFullName() }}</div>
|
||||||
</mat-cell>
|
<div *ngIf="!vote.user">{{ 'Anonymous' | translate }}</div>
|
||||||
</ng-container>
|
</mat-cell>
|
||||||
<ng-container matColumnDef="value" sticky>
|
</ng-container>
|
||||||
<mat-header-cell *matHeaderCellDef>{{ 'Vote' | translate }}</mat-header-cell>
|
<ng-container matColumnDef="value" sticky>
|
||||||
<mat-cell *matCellDef="let vote">{{ vote.valueVerbose }}</mat-cell>
|
<mat-header-cell *matHeaderCellDef>{{ 'Vote' | translate }}</mat-header-cell>
|
||||||
</ng-container>
|
<mat-cell *matCellDef="let vote">{{ vote.valueVerbose }}</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
|
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
|
||||||
<mat-row *matRowDef="let vote; columns: columnDefinition"></mat-row>
|
<mat-row *matRowDef="let vote; columns: columnDefinition"></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!votesDataSource.data">
|
||||||
|
{{ 'The individual votes were made anonymous.' | translate }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Publish immediately button. Only show for new polls -->
|
<!-- Publish immediately button. Only show for new polls -->
|
||||||
<div *ngIf="!pollData.state">
|
<div *ngIf="!pollData.isStatePublished">
|
||||||
<mat-checkbox [(ngModel)]="publishImmediately" (change)="publishStateChanged($event.checked)">
|
<mat-checkbox [(ngModel)]="publishImmediately" (change)="publishStateChanged($event.checked)">
|
||||||
<span translate>Publish immediately</span>
|
<span translate>Publish immediately</span>
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
|
@ -55,53 +55,60 @@
|
|||||||
|
|
||||||
<ng-template #viewTemplate>
|
<ng-template #viewTemplate>
|
||||||
<div class="poll-chart-wrapper">
|
<div class="poll-chart-wrapper">
|
||||||
<!-- empty helper div to center the grid wrapper -->
|
|
||||||
<div></div>
|
|
||||||
<div class="doughnut-chart">
|
<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>
|
</os-charts>
|
||||||
</div>
|
</div>
|
||||||
<div class="vote-legend">
|
<div class="vote-legend">
|
||||||
<div class="votes-yes" *ngIf="isVoteDocumented(voteYes)">
|
<div class="votes-yes" *ngIf="isVoteDocumented(voteYes)">
|
||||||
<os-icon-container icon="thumb_up" size="large">
|
<os-icon-container icon="thumb_up" size="large">
|
||||||
{{ voteYes | parsePollNumber }}
|
{{ voteYes | parsePollNumber }}
|
||||||
|
{{ voteYes | pollPercentBase: poll }}
|
||||||
</os-icon-container>
|
</os-icon-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="votes-no" *ngIf="isVoteDocumented(voteNo)">
|
<div class="votes-no" *ngIf="isVoteDocumented(voteNo)">
|
||||||
<os-icon-container icon="thumb_down" size="large">
|
<os-icon-container icon="thumb_down" size="large">
|
||||||
{{ voteNo | parsePollNumber }}
|
{{ voteNo | parsePollNumber }}
|
||||||
|
{{ voteNo | pollPercentBase: poll }}
|
||||||
</os-icon-container>
|
</os-icon-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="votes-abstain" *ngIf="isVoteDocumented(voteAbstain)">
|
<div class="votes-abstain" *ngIf="isVoteDocumented(voteAbstain)">
|
||||||
<os-icon-container icon="trip_origin" size="large">
|
<os-icon-container icon="trip_origin" size="large">
|
||||||
{{ voteAbstain | parsePollNumber }}
|
{{ voteAbstain | parsePollNumber }}
|
||||||
|
{{ voteAbstain | pollPercentBase: poll }}
|
||||||
</os-icon-container>
|
</os-icon-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="poll-detail-button-wrapper" *ngIf="poll.type !== 'analog'">
|
<div class="poll-detail-button-wrapper">
|
||||||
<button mat-button [routerLink]="pollLink">
|
<button mat-button [routerLink]="pollLink">
|
||||||
{{ 'Single Votes' | translate }}
|
{{ 'More' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #emptyTemplate>
|
<ng-template #emptyTemplate>
|
||||||
<div *osPerms="'motions.can_manage_polls'">
|
<div *osPerms="'motions.can_manage_polls'">
|
||||||
{{ 'An empty poll - you have to enter votes.' | translate }}
|
{{ 'Edit to enter votes.' | translate }}
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<!-- More Menu -->
|
<!-- More Menu -->
|
||||||
<mat-menu #pollDetailMenu="matMenu">
|
<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()">
|
<button *osPerms="'motions.can_manage_polls'" mat-menu-item (click)="openDialog()">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
<span translate>Edit</span>
|
<span translate>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
|
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
|
||||||
<button mat-menu-item (click)="downloadPdf()">
|
<button mat-menu-item (click)="downloadPdf()">
|
||||||
<mat-icon>picture_as_pdf</mat-icon>
|
<mat-icon>picture_as_pdf</mat-icon>
|
||||||
<span translate>PDF</span>
|
<span translate>Ballot paper</span>
|
||||||
</button>
|
</button>
|
||||||
<div *osPerms="'motions.can_manage_polls'">
|
<div *osPerms="'motions.can_manage_polls'">
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
@ -51,15 +51,20 @@
|
|||||||
.poll-chart-wrapper {
|
.poll-chart-wrapper {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-gap: 10px;
|
||||||
|
grid-template-areas: 'placeholder chart legend';
|
||||||
grid-template-columns: auto minmax(50px, 20%) auto;
|
grid-template-columns: auto minmax(50px, 20%) auto;
|
||||||
|
|
||||||
.doughnut-chart {
|
.doughnut-chart {
|
||||||
|
grid-area: chart;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vote-legend {
|
.vote-legend {
|
||||||
margin: auto 10px;
|
grid-area: legend;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
|
||||||
div + div {
|
div + div {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
<div class="os-form-card-mobile">
|
<div class="os-form-card-mobile">
|
||||||
|
<!-- Poll Title -->
|
||||||
<form [formGroup]="contentForm">
|
<form [formGroup]="contentForm">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<h2 class="poll-preview--title">
|
<h2 class="poll-preview-title">
|
||||||
<input matInput required formControlName="title" [placeholder]="'Title' | translate" />
|
<input matInput required formControlName="title" [placeholder]="'Title' | translate" />
|
||||||
</h2>
|
</h2>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</form>
|
</form>
|
||||||
<div *ngIf="data && data.state > 1" class="poll-preview-meta-info">
|
<div *ngIf="data && data.state > 1" class="poll-preview-meta-info">
|
||||||
<span class="short-description" *ngFor="let value of pollValues">
|
<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] }}
|
{{ value[0] }}
|
||||||
</span>
|
</span>
|
||||||
<span class="short-description--value" translate>
|
<span class="short-description-value" translate>
|
||||||
{{ value[1] }}
|
{{ value[1] }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<ng-container *ngIf="!data || !data.state || data.isStateCreated">
|
||||||
|
<!-- Poll Type -->
|
||||||
<mat-form-field *ngIf="pollService.isElectronicVotingEnabled">
|
<mat-form-field *ngIf="pollService.isElectronicVotingEnabled">
|
||||||
<mat-select [placeholder]="PollPropertyVerbose.type | translate" formControlName="type" required>
|
<mat-select [placeholder]="PollPropertyVerbose.type | translate" formControlName="type" required>
|
||||||
<mat-option *ngFor="let option of pollTypes | keyvalue" [value]="option.key">
|
<mat-option *ngFor="let option of pollTypes | keyvalue" [value]="option.key">
|
||||||
@ -26,7 +28,9 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
<mat-error translate>This field is required</mat-error>
|
<mat-error translate>This field is required</mat-error>
|
||||||
</mat-form-field>
|
</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
|
<os-search-value-selector
|
||||||
formControlName="groups_id"
|
formControlName="groups_id"
|
||||||
[multiple]="true"
|
[multiple]="true"
|
||||||
@ -36,8 +40,14 @@
|
|||||||
[inputListValues]="groupObservable"
|
[inputListValues]="groupObservable"
|
||||||
></os-search-value-selector>
|
></os-search-value-selector>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!-- Poll Methods -->
|
||||||
<mat-form-field *ngIf="pollMethods">
|
<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">
|
<mat-option *ngFor="let option of pollMethods | keyvalue" [value]="option.key">
|
||||||
{{ option.value }}
|
{{ option.value }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
@ -46,29 +56,37 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- 100 Percent Base -->
|
||||||
<mat-form-field>
|
<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">
|
<ng-container *ngFor="let option of percentBases | keyvalue">
|
||||||
<mat-option [value]="option.key">{{ option.value | translate }}</mat-option>
|
<mat-option [value]="option.key">{{ option.value | translate }}</mat-option>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</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">
|
<!-- Amount of Votes -->
|
||||||
<ng-container *ngIf="contentForm.get('pollmethod').value === 'votes'">
|
<ng-container
|
||||||
<mat-form-field>
|
*ngIf="contentForm.get('pollmethod').value === 'votes' && (!data || !data.state || data.isStateCreated)"
|
||||||
<input type="number" matInput placeholder="{{ PollPropertyVerbose.votes_amount | translate }}" formControlName="votes_amount" min="1" required>
|
>
|
||||||
</mat-form-field>
|
<mat-form-field>
|
||||||
<mat-checkbox formControlName="global_no">{{ PollPropertyVerbose.global_no | translate }}</mat-checkbox>
|
<input
|
||||||
<mat-checkbox formControlName="global_abstain">{{ PollPropertyVerbose.global_abstain | translate }}</mat-checkbox>
|
type="number"
|
||||||
</ng-container>
|
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>
|
</ng-container>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.poll-preview--title {
|
.poll-preview-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,13 +14,13 @@
|
|||||||
span {
|
span {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
&--label {
|
&-label {
|
||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-preview--meta-info-form {
|
.poll-preview-meta-info-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -114,9 +114,10 @@ export class PollFormComponent extends BaseViewComponent implements OnInit {
|
|||||||
this.data.groups_id = this.configService.instant('motion_poll_default_groups');
|
this.data.groups_id = this.configService.instant('motion_poll_default_groups');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(this.contentForm.controls).forEach(key => {
|
Object.keys(this.contentForm.controls).forEach(key => {
|
||||||
if (this.data[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 => {
|
this.contentForm.get('pollmethod').valueChanges.subscribe(method => {
|
||||||
let forbiddenBases: string[];
|
let forbiddenBases: string[];
|
||||||
if (method === 'YN') {
|
if (method === 'YN') {
|
||||||
forbiddenBases = [PercentBase.YNA, PercentBase.Votes];
|
forbiddenBases = [PercentBase.YNA, PercentBase.Cast];
|
||||||
} else if (method === 'YNA') {
|
} else if (method === 'YNA') {
|
||||||
forbiddenBases = [PercentBase.Votes];
|
forbiddenBases = [PercentBase.Cast];
|
||||||
} else if (method === 'votes') {
|
} else if (method === 'votes') {
|
||||||
forbiddenBases = [PercentBase.YN, PercentBase.YNA];
|
forbiddenBases = [PercentBase.YN, PercentBase.YNA];
|
||||||
}
|
}
|
||||||
|
@ -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>
|
<mat-progress-bar class="voting-progress-bar" [value]="valueInPercent"></mat-progress-bar>
|
||||||
|
@ -40,7 +40,11 @@ export class PollProgressComponent extends BaseViewComponent implements OnInit {
|
|||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
this.userRepo
|
this.userRepo
|
||||||
.getViewModelListObservable()
|
.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 => {
|
.subscribe(users => {
|
||||||
this.max = users.length;
|
this.max = users.length;
|
||||||
});
|
});
|
||||||
|
@ -21,8 +21,11 @@ export interface PollData {
|
|||||||
value?: number;
|
value?: number;
|
||||||
yes?: number;
|
yes?: number;
|
||||||
no?: number;
|
no?: number;
|
||||||
abstain: number;
|
abstain?: number;
|
||||||
user?: string;
|
user?: string;
|
||||||
|
canHide?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
showPercent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PollClassTypeVerbose = {
|
export const PollClassTypeVerbose = {
|
||||||
@ -40,7 +43,7 @@ export const PollStateVerbose = {
|
|||||||
export const PollStateChangeActionVerbose = {
|
export const PollStateChangeActionVerbose = {
|
||||||
1: 'Reset',
|
1: 'Reset',
|
||||||
2: 'Start voting',
|
2: 'Start voting',
|
||||||
3: 'End voting',
|
3: 'Stop voting',
|
||||||
4: 'Publish'
|
4: 'Publish'
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -51,7 +54,7 @@ export const PollTypeVerbose = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PollPropertyVerbose = {
|
export const PollPropertyVerbose = {
|
||||||
majority_method: 'Majority method',
|
majority_method: 'Required majority',
|
||||||
onehundred_percent_base: '100% base',
|
onehundred_percent_base: '100% base',
|
||||||
type: 'Poll type',
|
type: 'Poll type',
|
||||||
pollmethod: 'Poll method',
|
pollmethod: 'Poll method',
|
||||||
@ -69,10 +72,12 @@ export const MajorityMethodVerbose = {
|
|||||||
disabled: 'Disabled'
|
disabled: 'Disabled'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: These need to be in order
|
||||||
|
*/
|
||||||
export const PercentBaseVerbose = {
|
export const PercentBaseVerbose = {
|
||||||
YN: 'Yes/No',
|
YN: 'Yes/No',
|
||||||
YNA: 'Yes/No/Abstain',
|
YNA: 'Yes/No/Abstain',
|
||||||
votes: 'All votes',
|
|
||||||
valid: 'Valid votes',
|
valid: 'Valid votes',
|
||||||
cast: 'Total votes cast',
|
cast: 'Total votes cast',
|
||||||
disabled: 'Disabled'
|
disabled: 'Disabled'
|
||||||
@ -145,6 +150,8 @@ export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends Bas
|
|||||||
public abstract generateChartData(): ChartData;
|
public abstract generateChartData(): ChartData;
|
||||||
|
|
||||||
public abstract generateTableData(): PollData[];
|
public abstract generateTableData(): PollData[];
|
||||||
|
|
||||||
|
public abstract getPercentBase(): number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewBasePoll<M extends BasePoll<M, any> = any> extends BasePoll<M, any> {
|
export interface ViewBasePoll<M extends BasePoll<M, any> = any> extends BasePoll<M, any> {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from django.core.validators import MinValueValidator
|
||||||
|
|
||||||
from openslides.assignments.models import AssignmentPoll
|
from openslides.assignments.models import AssignmentPoll
|
||||||
from openslides.core.config import ConfigVariable
|
from openslides.core.config import ConfigVariable
|
||||||
|
|
||||||
@ -19,8 +21,8 @@ def get_config_variables():
|
|||||||
for base in AssignmentPoll.PERCENT_BASES
|
for base in AssignmentPoll.PERCENT_BASES
|
||||||
),
|
),
|
||||||
weight=400,
|
weight=400,
|
||||||
group="Voting",
|
group="Elections",
|
||||||
subgroup="Elections",
|
subgroup="Voting",
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -35,18 +37,8 @@ def get_config_variables():
|
|||||||
help_text="Default method to check whether a candidate has reached the required majority.",
|
help_text="Default method to check whether a candidate has reached the required majority.",
|
||||||
weight=405,
|
weight=405,
|
||||||
hidden=True,
|
hidden=True,
|
||||||
group="Voting",
|
group="Elections",
|
||||||
subgroup="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=410,
|
|
||||||
group="Voting",
|
|
||||||
subgroup="Elections",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -54,9 +46,52 @@ def get_config_variables():
|
|||||||
default_value=[],
|
default_value=[],
|
||||||
input_type="groups",
|
input_type="groups",
|
||||||
label="Default groups for named and pseudoanonymous assignment polls",
|
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,
|
weight=415,
|
||||||
group="Voting",
|
group="Elections",
|
||||||
subgroup="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
|
# PDF
|
||||||
|
@ -332,22 +332,77 @@ def get_config_variables():
|
|||||||
# Voting and ballot papers
|
# Voting and ballot papers
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name="motions_poll_100_percent_base",
|
name="motion_poll_default_100_percent_base",
|
||||||
default_value="YES_NO_ABSTAIN",
|
default_value="YNA",
|
||||||
input_type="choice",
|
input_type="choice",
|
||||||
label="The 100 % base of a voting result consists of",
|
label="The 100 % base of a voting result consists of",
|
||||||
choices=(
|
choices=tuple(
|
||||||
{"value": "YES_NO_ABSTAIN", "display_name": "Yes/No/Abstain"},
|
{"value": base[0], "display_name": base[1]}
|
||||||
{"value": "YES_NO", "display_name": "Yes/No"},
|
for base in MotionPoll.PERCENT_BASES
|
||||||
{"value": "VALID", "display_name": "All valid ballots"},
|
|
||||||
{"value": "CAST", "display_name": "All casted ballots"},
|
|
||||||
{"value": "DISABLED", "display_name": "Disabled (no percents)"},
|
|
||||||
),
|
),
|
||||||
weight=370,
|
weight=370,
|
||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="Voting and ballot papers",
|
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
|
# PDF export
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
@ -387,44 +442,3 @@ def get_config_variables():
|
|||||||
group="Motions",
|
group="Motions",
|
||||||
subgroup="PDF export",
|
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",
|
|
||||||
)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user