Even more voting refinement

Various additional refinements for a more well rounded
voting experience
This commit is contained in:
Sean Engelhardt 2020-03-13 16:11:28 +01:00 committed by FinnStutzenstein
parent a05662a0f8
commit ee4c6aa0bf
31 changed files with 267 additions and 255 deletions

View File

@ -1,4 +1,4 @@
@import '~assets/styles/poll-colors.scss';
@import '~assets/styles/poll-styles-common.scss';
.result-wrapper {
display: grid;
@ -21,18 +21,6 @@
text-align: right;
}
}
.yes {
color: $votes-yes-color;
}
.no {
color: $votes-no-color;
}
.abstain {
color: $votes-abstain-color;
}
}
.doughnut-chart {

View File

@ -77,6 +77,22 @@ export abstract class BasePoll<
return this.onehundred_percent_base === PercentBase.Cast;
}
public get isAnalog(): boolean {
return this.type === PollType.Analog;
}
public get isNamed(): boolean {
return this.type === PollType.Named;
}
public get isAnon(): boolean {
return this.type === PollType.Pseudoanonymous;
}
public get isEVoting(): boolean {
return this.isNamed || this.isAnon;
}
/**
* Determine if the state is finished or published
*/

View File

@ -36,13 +36,11 @@ export class PollPercentBasePipe implements PipeTransform {
totalByBase = this.motionPollService.getPercentBase(poll);
}
if (totalByBase) {
if (totalByBase && totalByBase > 0) {
const percentNumber = (value / totalByBase) * 100;
if (percentNumber > 0) {
const result = percentNumber % 1 === 0 ? percentNumber : percentNumber.toFixed(this.decimalPlaces);
return `(${result} %)`;
}
}
return null;
}
}

View File

@ -263,7 +263,7 @@
<mat-form-field>
<input
matInput
placeholder="{{ 'Default comment on the ballot paper' | translate }}"
placeholder="{{ 'Hint on voting' | translate }}"
formControlName="default_poll_description"
/>
</mat-form-field>

View File

@ -9,4 +9,21 @@
border-bottom: 1px solid mat-color($background, focused-button);
}
}
.openslides-theme .pbl-ngrid-row:hover {
background-color: mat-color($background, card);
}
.openslides-theme os-list-view-table os-sort-filter-bar .custom-table-header {
&,
.action-buttons .input-container input {
background: mat-color($background, card);
}
}
.openslides-theme .pbl-ngrid-header-cell:first-child {
&::after {
border-right: 1px solid mat-color($background, focused-button);
}
}
}

View File

@ -27,7 +27,7 @@
<tbody>
<tr>
<th class="voting-option" translate>Candidates</th>
<th class="result voted-yes">
<th class="result yes">
<span *ngIf="!poll.isMethodY" translate>
Yes
</span>
@ -35,8 +35,8 @@
Votes
</span>
</th>
<th class="result voted-no" translate *ngIf="!poll.isMethodY">No</th>
<th class="result voted-abstain" translate *ngIf="poll.isMethodYNA">Abstain</th>
<th class="result no" translate *ngIf="!poll.isMethodY">No</th>
<th class="result abstain" translate *ngIf="poll.isMethodYNA">Abstain</th>
</tr>
<tr *ngFor="let row of getTableData()" [class]="row.class">
<td class="voting-option">

View File

@ -1,5 +1,5 @@
@import '~assets/styles/variables.scss';
@import '~assets/styles/poll-colors.scss';
@import '~assets/styles/poll-styles-common.scss';
.assignment-result-wrapper {
.assignment-result-table {
@ -88,30 +88,6 @@
padding-top: 20px;
}
.voted-yes {
color: $votes-yes-color;
}
.voted-no {
color: $votes-no-color;
}
.voted-abstain {
color: $votes-abstain-color;
}
// theme
.openslides-theme .pbl-ngrid-row:hover {
background-color: #f9f9f9;
}
.openslides-theme os-list-view-table os-sort-filter-bar .custom-table-header {
&,
.action-buttons .input-container input {
background: white;
}
}
.openslides-theme .pbl-ngrid-no-data {
top: 10%;
}
@ -128,6 +104,5 @@
top: 0;
right: -1px;
height: 100%;
border-right: 1px solid #e0e0e0;
}
}

View File

@ -1,7 +1,7 @@
import { Component, ViewEncapsulation } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
@ -49,7 +49,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
pollService: PollService,
votesRepo: AssignmentVoteRepositoryService,
private operator: OperatorService,
private assignmentPollService: AssignmentPollService
private assignmentPollService: AssignmentPollService,
private router: Router
) {
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService, votesRepo);
}
@ -126,8 +127,7 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
}
public getVoteClass(votingResult: VotingResult): string {
const cssPrefix = 'voted-';
return `${cssPrefix}${votingResult.vote}`;
return votingResult.vote;
}
public voteFitsMethod(result: VotingResult): boolean {
@ -143,6 +143,10 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
return true;
}
protected onDeleted(): void {
this.router.navigate(['assignments', this.poll.assignment_id]);
}
public getTableData(): PollTableData[] {
return this.assignmentPollService.generateTableData(this.poll);
}

View File

@ -1,11 +1,13 @@
<ng-container *ngIf="poll">
<ng-container *ngIf="vmanager.canVote(poll) && !alreadyVoted; else cannotVote">
<!-- Submit Vote -->
<ng-container [ngTemplateOutlet]="sendNow"></ng-container>
<!-- Poll hint -->
<p *ngIf="pollHint">
<i>{{ pollHint }}</i>
</p>
<!-- Leftover votes -->
<h4
*ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1 && !isGlobalOptionSelected()"
*ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1"
>
{{ 'Votes for this poll' | translate }}: {{ getVotesCount() }}/{{ poll.votes_amount }}
</h4>
@ -32,10 +34,12 @@
<div *ngFor="let action of voteActions">
<button
class="vote-button"
mat-raised-button
(click)="saveSingleVote(option.id, action.vote)"
[ngClass]="
voteRequestData.votes[option.id] === action.vote || voteRequestData.votes[option.id] === 1
voteRequestData.votes[option.id] === action.vote ||
voteRequestData.votes[option.id] === 1
? action.css
: ''
"
@ -57,6 +61,7 @@
<div class="global-option-grid">
<div *ngIf="poll.global_no">
<button
class="vote-button"
mat-raised-button
(click)="saveGlobalVote('N')"
[ngClass]="voteRequestData.global === 'N' ? 'voted-no' : ''"
@ -95,9 +100,13 @@
<ng-template #cannotVote>
<div class="centered-button-wrapper">
<os-icon-container icon="check">
{{ 'You already voted on this poll' | translate}}
</os-icon-container>
<div>
<mat-icon class="vote-submitted">
check_circle
</mat-icon>
<br />
<span>{{ 'You already voted on this poll.' | translate }}</span>
</div>
</div>
</ng-template>

View File

@ -1,4 +1,5 @@
@import '~assets/styles/poll-colors.scss';
@import '~assets/styles/poll-styles-common.scss';
%vote-grid-base {
display: grid;
@ -42,23 +43,21 @@
.centered-button-wrapper {
display: flex;
text-align: center;
> * {
margin-left: auto;
margin-right: auto;
}
.vote-submitted {
color: $votes-yes-color;
font-size: 200%;
}
}
// TODO: Could be some more general component
.voted-yes {
background-color: $votes-yes-color;
}
.voted-no {
background-color: $votes-no-color;
}
.voted-abstain {
background-color: $votes-abstain-color;
.vote-button {
min-width: 50px;
min-height: 50px;
}
.vote-label {

View File

@ -62,6 +62,10 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
}
}
public get pollHint(): string {
return this.poll.assignment.default_poll_description;
}
private defineVoteOptions(): void {
this.voteActions.push({
vote: 'Y',
@ -93,7 +97,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
return Object.keys(this.voteRequestData.votes).filter(key => this.voteRequestData.votes[key]).length;
}
public isGlobalOptionSelected(): boolean {
private isGlobalOptionSelected(): boolean {
return !!this.voteRequestData.global;
}
@ -145,7 +149,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
}
} else {
this.raiseError(
this.translate.instant('You reached the maximum amount of votes. Deselect somebody first')
this.translate.instant('You reached the maximum amount of votes. Deselect somebody first.')
);
}
} else {
@ -165,7 +169,11 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
public saveGlobalVote(globalVote: GlobalVote): void {
this.voteRequestData.votes = {};
if (this.voteRequestData.global && this.voteRequestData.global === globalVote) {
delete this.voteRequestData.global;
} else {
this.voteRequestData.global = globalVote;
this.submitVote();
}
}
}

View File

@ -30,10 +30,9 @@
</div>
<!-- Change state button -->
<div *osPerms="'assignments.can_manage'">
<div *osPerms="'assignments.can_manage'; and: !hideChangeState">
<button
mat-stroked-button
*ngIf="!poll.isPublished"
[ngClass]="pollStateActions[poll.state].css"
(click)="changeState(poll.nextState)"
>
@ -43,6 +42,11 @@
</span>
</button>
</div>
<!-- Enter Votes Hint -->
<div *osPerms="'assignments.can_manage'; and: poll.type === 'analog' && !poll.stateHasVotes">
{{ 'Edit to enter votes.' | translate }}
</div>
</div>
<!-- Chart -->
@ -70,9 +74,18 @@
<div *osPerms="'assignments.can_manage'; and: poll && poll.isStarted">
<os-poll-progress [poll]="poll"></os-poll-progress>
</div>
<!-- The Vote -->
<os-assignment-poll-vote *ngIf="poll.canBeVotedFor" [poll]="poll"></os-assignment-poll-vote>
<!-- More-Button -->
<div class="poll-detail-button-wrapper">
<a mat-icon-button routerLink="/assignments/polls/{{ poll.id }}" matTooltip="{{ 'More' | translate }}">
<a
mat-icon-button
routerLink="/assignments/polls/{{ poll.id }}"
matTooltip="{{ 'More' | translate }}"
*ngIf="poll.isPublished"
>
<mat-icon class="small-icon">
visibility
</mat-icon>

View File

@ -1,4 +1,5 @@
@import '~assets/styles/poll-colors.scss';
@import '~assets/styles/poll-styles-common.scss';
.assignment-poll-wrapper {
position: relative;
@ -17,16 +18,4 @@
margin-left: auto;
}
}
.start-poll-button {
color: green !important;
}
.stop-poll-button {
color: $poll-stop-color;
}
.publish-poll-button {
color: $poll-publish-color;
}
}

View File

@ -1,4 +1,4 @@
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
@ -21,8 +21,7 @@ import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
@Component({
selector: 'os-assignment-poll',
templateUrl: './assignment-poll.component.html',
styleUrls: ['./assignment-poll.component.scss'],
encapsulation: ViewEncapsulation.None
styleUrls: ['./assignment-poll.component.scss']
})
export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPoll> implements OnInit {
@Input()

View File

@ -190,7 +190,7 @@ export class AssignmentPollPdfService extends PollPdfService {
*/
private createPollHint(poll: ViewAssignmentPoll): object {
return {
text: poll.description || '',
text: poll.assignment.default_poll_description || '',
style: 'description'
};
}

View File

@ -32,6 +32,8 @@ export class AssignmentPollService extends PollService {
public defaultPollMethod: AssignmentPollMethod;
private sortByVote: boolean;
/**
* Constructor. Subscribes to the configuration values needed
* @param config ConfigService
@ -53,6 +55,7 @@ export class AssignmentPollService extends PollService {
config
.get<AssignmentPollMethod>(AssignmentPoll.defaultPollMethodConfig)
.subscribe(method => (this.defaultPollMethod = method));
config.get<boolean>('assignment_poll_sort_poll_result_by_votes').subscribe(sort => (this.sortByVote = sort));
}
public getDefaultPollData(contextId?: number): AssignmentPoll {
@ -74,22 +77,31 @@ export class AssignmentPollService extends PollService {
}
private getGlobalVoteKeys(poll: ViewAssignmentPoll): VotingResult[] {
// debugger;
return [
{
vote: 'amount_global_no',
showPercent: false,
hide: poll.amount_global_no === -2 || poll.amount_global_no === 0
showPercent: this.showPercentOfValidOrCast(poll),
hide: poll.amount_global_no === -2 || !poll.amount_global_no
},
{
vote: 'amount_global_abstain',
showPercent: false,
hide: poll.amount_global_abstain === -2 || poll.amount_global_abstain === 0
showPercent: this.showPercentOfValidOrCast(poll),
hide: poll.amount_global_abstain === -2 || !poll.amount_global_abstain
}
];
}
public generateTableData(poll: ViewAssignmentPoll): PollTableData[] {
const tableData: PollTableData[] = poll.options.map(candidate => ({
const tableData: PollTableData[] = poll.options
.sort((a, b) => {
if (this.sortByVote) {
return b.yes - a.yes;
} else {
return b.weight - a.weight;
}
})
.map(candidate => ({
votingOption: candidate.user.short_name,
votingOptionSubtitle: candidate.user.getLevelAndNumber(),
class: 'user',
@ -104,8 +116,8 @@ export class AssignmentPollService extends PollService {
} as VotingResult)
)
}));
tableData.push(...this.formatVotingResultToTableData(super.getSumTableKeys(poll), poll));
tableData.push(...this.formatVotingResultToTableData(this.getGlobalVoteKeys(poll), poll));
tableData.push(...this.formatVotingResultToTableData(super.getSumTableKeys(poll), poll));
return tableData;
}

View File

@ -31,8 +31,9 @@
<div *ngIf="!poll.hasVotes || !poll.stateHasVotes">{{ 'No results to show' | translate }}</div>
<div *ngIf="poll.stateHasVotes">
<os-motion-poll-detail-content [poll]="poll" [chartData]="chartDataSubject"> </os-motion-poll-detail-content>
<div *ngIf="poll.stateHasVotes && (hasPerms() || poll.isPublished)">
<os-motion-poll-detail-content [poll]="poll" [chartData]="chartDataSubject">
</os-motion-poll-detail-content>
<!-- Named table: only show if votes are present -->
<div class="named-result-table" *ngIf="poll.type === 'named'">

View File

@ -1,5 +1,4 @@
@import '~assets/styles/variables.scss';
@import '~assets/styles/poll-colors.scss';
@import '~assets/styles/poll-styles-common.scss';
.poll-content {
text-align: right;
@ -30,18 +29,6 @@
height: 500px;
}
.voted-yes {
color: $votes-yes-color;
}
.voted-no {
color: $votes-no-color;
}
.voted-abstain {
color: $votes-abstain-color;
}
.openslides-theme .pbl-ngrid-no-data {
top: 10%;
}

View File

@ -1,14 +1,14 @@
<ng-container *ngIf="poll && !poll.user_has_voted; else userHasVotes">
<div *osPerms="'motions.can_manage_polls'; and: poll && poll.isStarted">
<div *osPerms="'motions.can_manage_polls'; and: poll && poll.isStarted">
<os-poll-progress [poll]="poll"></os-poll-progress>
</div>
</div>
<ng-container *ngIf="poll && !poll.user_has_voted; else userHasVotes">
<div *ngIf="vmanager.canVote(poll)" class="vote-button-grid">
<!-- Voting -->
<div class="vote-button" *ngFor="let option of voteOptions">
<button
mat-raised-button
(click)="saveVote(option.vote)"
[ngClass]="currentVote && currentVote.value === option.vote ? option.css : ''"
[ngClass]="currentVote && currentVote.vote === option.vote ? option.css : ''"
>
<mat-icon> {{ option.icon }}</mat-icon>
</button>
@ -19,8 +19,12 @@
<ng-template #userHasVotes>
<div class="user-has-voted">
<os-icon-container icon="check">
{{ 'You already voted on this poll' | translate }}
</os-icon-container>
<div>
<mat-icon class="vote-submitted">
check_circle
</mat-icon>
<br />
<span>{{ 'You already voted on this poll.' | translate }}</span>
</div>
</div>
</ng-template>

View File

@ -1,4 +1,5 @@
@import '~assets/styles/poll-colors.scss';
@import '~assets/styles/poll-styles-common.scss';
.vote-button-grid {
display: grid;
@ -19,24 +20,15 @@
.user-has-voted {
display: flex;
text-align: center;
> * {
margin-top: 1em;
margin-left: auto;
margin-right: auto;
}
}
.voted-yes {
background-color: $votes-yes-color;
color: $vote-active-color;
}
.voted-no {
background-color: $votes-no-color;
color: $vote-active-color;
}
.voted-abstain {
background-color: $votes-abstain-color;
color: $vote-active-color;
.vote-submitted {
color: $votes-yes-color;
font-size: 200%;
}
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
@ -6,20 +6,16 @@ import { TranslateService } from '@ngx-translate/core';
import { OperatorService } from 'app/core/core-services/operator.service';
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
import { MotionVoteRepositoryService } from 'app/core/repositories/motions/motion-vote-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { VotingService } from 'app/core/ui-services/voting.service';
import { MotionPollMethod } from 'app/shared/models/motions/motion-poll';
import { PollType } from 'app/shared/models/poll/base-poll';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
import { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component';
interface VoteOption {
vote: 'Y' | 'N' | 'A';
css: string;
icon: string;
label: string;
vote?: 'Y' | 'N' | 'A';
css?: string;
icon?: string;
label?: string;
}
@Component({
@ -27,19 +23,8 @@ interface VoteOption {
templateUrl: './motion-poll-vote.component.html',
styleUrls: ['./motion-poll-vote.component.scss']
})
export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPoll> implements OnInit {
/**
* holds the last saved vote
*
* TODO: There will be a bug. This has to be reset if the currently observed poll changes it's state back
* to started
*/
public currentVote: ViewMotionVote;
public MotionPollMethod = MotionPollMethod;
private votes: ViewMotionVote[];
export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPoll> {
public currentVote: VoteOption = {};
public voteOptions: VoteOption[] = [
{
vote: 'Y',
@ -67,43 +52,17 @@ export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPol
matSnackbar: MatSnackBar,
vmanager: VotingService,
operator: OperatorService,
private voteRepo: MotionVoteRepositoryService,
private pollRepo: MotionPollRepositoryService,
private promptService: PromptService
) {
super(title, translate, matSnackbar, vmanager, operator);
}
public ngOnInit(): void {
this.subscriptions.push(
this.voteRepo.getViewModelListObservable().subscribe(votes => {
this.votes = votes;
this.updateVotes();
})
);
}
protected updateVotes(): void {
if (this.user && this.votes && this.poll) {
this.currentVote = null;
const filtered = this.votes.filter(
vote => vote.option.poll_id === this.poll.id && vote.user_id === this.user.id
);
if (filtered.length) {
if (filtered.length > 1) {
// output warning and continue to keep the error case user friendly
console.error('A user should never have more than one vote on the same poll.');
}
this.currentVote = filtered[0];
}
}
}
/**
* TODO: 'Y' | 'N' | 'A' should refer to some ENUM
*/
public saveVote(vote: 'Y' | 'N' | 'A'): void {
if (this.poll.type === PollType.Pseudoanonymous) {
this.currentVote.vote = vote;
const title = this.translate.instant('Are you sure?');
const content = this.translate.instant('Your decision cannot be changed afterwards');
this.promptService.open(title, content).then(confirmed => {
@ -111,8 +70,5 @@ export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPol
this.pollRepo.vote(vote, this.poll.id).catch(this.raiseError);
}
});
} else {
this.pollRepo.vote(vote, this.poll.id).catch(this.raiseError);
}
}
}

View File

@ -1,4 +1,4 @@
<mat-card class="motion-poll-wrapper" *ngIf="poll">
<mat-card class="motion-poll-wrapper" *ngIf="showPoll">
<!-- Poll Infos -->
<div class="poll-title-wrapper">
<!-- Title Area -->
@ -56,7 +56,7 @@
<!-- Detail link -->
<div class="poll-detail-button-wrapper">
<a mat-icon-button [routerLink]="pollLink" matTooltip="{{ 'More' | translate }}">
<a mat-icon-button [routerLink]="pollLink" matTooltip="{{ 'More' | translate }}" *ngIf="poll.isPublished">
<mat-icon class="small-icon">
visibility
</mat-icon>

View File

@ -1,4 +1,5 @@
@import '~assets/styles/poll-colors.scss';
@import '~assets/styles/poll-styles-common.scss';
.poll-link-wrapper {
outline: none;
@ -46,18 +47,6 @@
div + div {
margin-top: 20px;
}
.yes {
color: $votes-yes-color;
}
.no {
color: $votes-no-color;
}
.abstain {
color: $votes-abstain-color;
}
}
}
}
@ -75,18 +64,6 @@
margin-bottom: auto;
}
.start-poll-button {
color: green !important;
}
.stop-poll-button {
color: $poll-stop-color;
}
.publish-poll-button {
color: $poll-publish-color;
}
.motion-couting-in-progress-hint {
margin-top: 1em;
font-style: italic;

View File

@ -7,7 +7,6 @@ import { TranslateService } from '@ngx-translate/core';
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component';
import { PollType } from 'app/shared/models/poll/base-poll';
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service';
@ -15,6 +14,7 @@ import { MotionPollPdfService } from 'app/site/motions/services/motion-poll-pdf.
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
import { PollService, PollTableData } from 'app/site/polls/services/poll.service';
import { OperatorService } from 'app/core/core-services/operator.service';
/**
* Component to show a motion-poll.
@ -48,16 +48,25 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
return this.motionPollService.showChart(this.poll);
}
public get hideChangeState(): boolean {
return this.poll.isPublished || (this.poll.isCreated && this.poll.type === PollType.Analog);
}
public get reducedPollTableData(): PollTableData[] {
return this.motionPollService
.generateTableData(this.poll)
.filter(data => ['yes', 'no', 'abstain', 'votesinvalid'].includes(data.votingOption));
}
public get showPoll(): boolean {
if (this.poll) {
if (
this.operator.hasPerms('motions.can_manage_polls') ||
this.poll.isPublished ||
(this.poll.isEVoting && !this.poll.isCreated)
) {
return true;
}
}
return false;
}
/**
* Constructor.
*
@ -77,7 +86,8 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
pollDialog: MotionPollDialogService,
public pollService: PollService,
private pdfService: MotionPollPdfService,
private motionPollService: MotionPollService
private motionPollService: MotionPollService,
private operator: OperatorService
) {
super(titleService, matSnackBar, translate, dialog, promptService, pollRepo, pollDialog);
}

View File

@ -42,15 +42,15 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
*/
public voteOptionStyle = {
Y: {
css: 'voted-yes',
css: 'yes',
icon: 'thumb_up'
},
N: {
css: 'voted-no',
css: 'no',
icon: 'thumb_down'
},
A: {
css: 'voted-abstain',
css: 'abstain',
icon: 'trip_origin'
}
};
@ -151,8 +151,6 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
this.pollDialog.openDialog(viewPoll);
}
protected onDeleted(): void {}
/**
* Called after the poll has been loaded. Meant to be overwritten by subclasses who need initial access to the poll
*/
@ -166,6 +164,8 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
protected abstract hasPerms(): boolean;
protected abstract onDeleted(): void;
protected get canSeeVotes(): boolean {
return (this.hasPerms && this.poll.isFinished) || this.poll.isPublished;
}

View File

@ -8,7 +8,7 @@ import { BehaviorSubject } from 'rxjs';
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { ChartData } from 'app/shared/components/charts/charts.component';
import { PollState } from 'app/shared/models/poll/base-poll';
import { PollState, PollType } from 'app/shared/models/poll/base-poll';
import { BaseViewComponent } from 'app/site/base/base-view';
import { BasePollRepositoryService } from '../services/base-poll-repository.service';
import { ViewBasePoll } from '../models/view-base-poll';
@ -33,6 +33,10 @@ export abstract class BasePollComponent<V extends ViewBasePoll> extends BaseView
}
};
public get hideChangeState(): boolean {
return this._poll.isPublished || (this._poll.isCreated && this._poll.type === PollType.Analog);
}
public constructor(
titleService: Title,
matSnackBar: MatSnackBar,

View File

@ -234,23 +234,27 @@ export abstract class PollService {
);
}
public showPercentOfValidOrCast(poll: PollData | ViewBasePoll): boolean {
return poll.onehundred_percent_base === PercentBase.Valid || poll.onehundred_percent_base === PercentBase.Cast;
}
public getSumTableKeys(poll: PollData | ViewBasePoll): VotingResult[] {
return [
{
vote: 'votesvalid',
hide: poll.votesvalid === -2,
showPercent:
poll.onehundred_percent_base === PercentBase.Valid ||
poll.onehundred_percent_base === PercentBase.Cast
showPercent: this.showPercentOfValidOrCast(poll)
},
{
vote: 'votesinvalid',
icon: 'not_interested',
// TODO || PollType === analog
hide: poll.votesinvalid === -2,
showPercent: poll.onehundred_percent_base === PercentBase.Cast
},
{
vote: 'votescast',
// TODO || PollType === analog
hide: poll.votescast === -2,
showPercent: poll.onehundred_percent_base === PercentBase.Cast
}

View File

@ -44,8 +44,7 @@ export const allSlides: SlideManifest[] = [
{
slide: 'motions/motion-poll',
path: 'motions/motion-poll',
loadChildren: () =>
import('./motions/motion-poll/motion-poll-slide.module').then(m => m.MotionPollSlideModule),
loadChildren: () => import('./motions/motion-poll/motion-poll-slide.module').then(m => m.MotionPollSlideModule),
verboseName: 'Motion Poll',
elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true
@ -137,7 +136,8 @@ export const allSlides: SlideManifest[] = [
{
slide: 'assignments/assignment-poll',
path: 'assignments/assignment-poll',
loadChildren: () => import('./assignments/assignment-poll/assignment-poll-slide.module').then(m => m.AssignmentPollSlideModule),
loadChildren: () =>
import('./assignments/assignment-poll/assignment-poll-slide.module').then(m => m.AssignmentPollSlideModule),
verboseName: 'Assignment Poll',
elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true

View File

@ -5,6 +5,6 @@ $votes-yes-color: #4caf50;
$votes-no-color: #cc6c5b;
$votes-abstain-color: #a6a6a6;
$vote-active-color: white;
$poll-create-color: #4caf50;
$poll-start-color: #4caf50;
$poll-stop-color: #ff5252;
$poll-publish-color: #e6b100;

View File

@ -0,0 +1,40 @@
@import '~assets/styles/poll-colors.scss';
.yes {
color: $votes-yes-color;
}
.no {
color: $votes-no-color;
}
.abstain {
color: $votes-abstain-color;
}
.voted-yes {
background-color: $votes-yes-color;
color: $vote-active-color;
}
.voted-no {
background-color: $votes-no-color;
color: $vote-active-color;
}
.voted-abstain {
background-color: $votes-abstain-color;
color: $vote-active-color;
}
.start-poll-button {
color: $poll-start-color;
}
.stop-poll-button {
color: $poll-stop-color;
}
.publish-poll-button {
color: $poll-publish-color;
}

View File

@ -65,12 +65,22 @@ def get_config_variables():
subgroup="Ballot",
)
yield ConfigVariable(
name="assignment_poll_sort_poll_result_by_votes",
default_value=True,
input_type="boolean",
label="Sort election results by amount of votes",
weight=420,
group="Elections",
subgroup="Ballot",
)
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=420,
weight=425,
group="Elections",
subgroup="Ballot",
)