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');
|
||||
_('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)
|
||||
|
@ -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')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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 { 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,
|
||||
|
@ -86,6 +86,10 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public getPercentBase(): number {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ViewAssignmentPoll extends AssignmentPoll {
|
||||
|
@ -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',
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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> {
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user