Cleanups and enhancements

Cleans up and reviews some methods
This commit is contained in:
Sean Engelhardt 2019-04-05 16:15:21 +02:00 committed by Maximilian Krambach
parent 464fb89b53
commit 054f76a5d4
13 changed files with 119 additions and 109 deletions

View File

@ -10,6 +10,7 @@ import { DataSendService } from 'app/core/core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { HttpService } from 'app/core/core-services/http.service';
import { Item } from 'app/shared/models/agenda/item';
import { Poll } from 'app/shared/models/assignments/poll';
import { Tag } from 'app/shared/models/core/tag';
import { User } from 'app/shared/models/users/user';
import { ViewAssignment } from 'app/site/assignments/models/view-assignment';
@ -17,7 +18,6 @@ import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewUser } from 'app/site/users/models/view-user';
import { Poll } from 'app/shared/models/assignments/poll';
/**
* Repository Service for Assignments.
@ -28,14 +28,22 @@ import { Poll } from 'app/shared/models/assignments/poll';
providedIn: 'root'
})
export class AssignmentRepositoryService extends BaseAgendaContentObjectRepository<ViewAssignment, Assignment> {
private readonly restPath = '/rest/assignments/assignment/';
private readonly restPollPath = '/rest/assignments/poll/';
private readonly candidatureOtherPath = '/candidature_other/';
private readonly candidatureSelfPath = '/candidature_self/';
private readonly createPollPath = '/create_poll/';
private readonly markElectedPath = '/mark_elected/';
/**
* Constructor for the Assignment Repository.
*
* @param DS The DataStore
* @param mapperService Maps collection strings to classes
* @param viewModelStoreService
* @param translate
* @param httpService
* @param DS DataStore access
* @param dataSend Sending data
* @param mapperService Map models to object
* @param viewModelStoreService Access view models
* @param translate Translate string
* @param httpService make HTTP Requests
*/
public constructor(
DS: DataStoreService,
@ -76,55 +84,42 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
* Adds another user as a candidate
*
* @param userId User id of a candidate
* @param assignment
* @param assignment The assignment to add the candidate to
*/
public async addCandidate(userId: number, assignment: ViewAssignment): Promise<void> {
const restPath = `/rest/assignments/assignment/${assignment.id}/candidature_other/`;
public async changeCandidate(userId: number, assignment: ViewAssignment): Promise<void> {
const data = { user: userId };
await this.httpService.post(restPath, data);
}
/**
* Removes an user from the list of candidates for an assignment
*
* @param user note: AssignmentUser, not a ViewUser
* @param assignment
*/
public async deleteCandidate(user: AssignmentUser, assignment: ViewAssignment): Promise<void> {
const restPath = `/rest/assignments/assignment/${assignment.id}/candidature_other/`;
const data = { user: user.id };
await this.httpService.delete(restPath, data);
if (assignment.candidates.some(candidate => candidate.id === userId)) {
await this.httpService.delete(this.restPath + assignment.id + this.candidatureOtherPath, data);
} else {
await this.httpService.post(this.restPath + assignment.id + this.candidatureOtherPath, data);
}
}
/**
* Add the operator as candidate to the assignment
*
* @param assignment
* @param assignment The assignment to add the candidate to
*/
public async addSelf(assignment: ViewAssignment): Promise<void> {
const restPath = `/rest/assignments/assignment/${assignment.id}/candidature_self/`;
await this.httpService.post(restPath);
await this.httpService.post(this.restPath + assignment.id + this.candidatureSelfPath);
}
/**
* Removes the current user (operator) from the list of candidates for an assignment
*
* @param assignment
* @param assignment The assignment to remove ourself from
*/
public async deleteSelf(assignment: ViewAssignment): Promise<void> {
const restPath = `/rest/assignments/assignment/${assignment.id}/candidature_self/`;
await this.httpService.delete(restPath);
await this.httpService.delete(this.restPath + assignment.id + this.candidatureSelfPath);
}
/**
* Creates a new Poll to a given assignment
*
* @param assignment
* @param assignment The assignment to add the poll to
*/
public async addPoll(assignment: ViewAssignment): Promise<void> {
const restPath = `/rest/assignments/assignment/${assignment.id}/create_poll/`;
await this.httpService.post(restPath);
// TODO set phase, too, if phase was 0. Should be done server side?
await this.httpService.post(this.restPath + assignment.id + this.createPollPath);
}
/**
@ -133,8 +128,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
* @param id id of the poll to delete
*/
public async deletePoll(poll: Poll): Promise<void> {
const restPath = `/rest/assignments/poll/${poll.id}/`;
await this.httpService.delete(restPath);
await this.httpService.delete(`${this.restPollPath}${poll.id}/`);
}
/**
@ -143,12 +137,11 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
* @param poll the (partial) data to update
* @param originalPoll the poll to update
*
* TODO check if votes is untouched
* TODO: check if votes is untouched
*/
public async updatePoll(poll: Partial<Poll>, originalPoll: Poll): Promise<void> {
const restPath = `/rest/assignments/poll/${originalPoll.id}/`;
const data: Poll = Object.assign(originalPoll, poll);
await this.httpService.patch(restPath, data);
await this.httpService.patch(`${this.restPollPath}${originalPoll.id}/`, data);
}
/**
@ -186,8 +179,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
votesno: null,
votesvalid: poll.votesvalid || null
};
const restPath = `/rest/assignments/poll/${originalPoll.id}/`;
await this.httpService.put(restPath, data);
await this.httpService.put(`${this.restPollPath}${originalPoll.id}/`, data);
}
/**
@ -198,12 +190,11 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
* @param elected true if the candidate is to be elected, false if unelected
*/
public async markElected(user: AssignmentUser, assignment: ViewAssignment, elected: boolean): Promise<void> {
const restPath = `/rest/assignments/assignment/${assignment.id}/mark_elected/`;
const data = { user: user.user_id };
if (elected) {
await this.httpService.post(restPath, data);
await this.httpService.post(this.restPath + assignment.id + this.markElectedPath, data);
} else {
await this.httpService.delete(restPath, data);
await this.httpService.delete(this.restPath + assignment.id + this.markElectedPath, data);
}
}

View File

@ -6,7 +6,6 @@ import { _ } from 'app/core/translate/translation-marker';
* The possible keys of a poll object that represent numbers.
* TODO Should be 'key of MotionPoll if type of key is number'
* TODO: normalize MotionPoll model and other poll models
* TODO: reuse more motion-poll-service stuff
*/
export type CalculablePollKey = 'votesvalid' | 'votesinvalid' | 'votescast' | 'yes' | 'no' | 'abstain';
@ -102,9 +101,9 @@ export abstract class PollService {
/**
* Gets an icon for a Poll Key
*
* @param key
* @param key yes, no, abstain or something like that
* @returns a string for material-icons to represent the icon for
* this key(e.g. yes: positiv sign, no: negative sign)
* this key(e.g. yes: positive sign, no: negative sign)
*/
public getIcon(key: CalculablePollKey): string {
switch (key) {
@ -128,6 +127,7 @@ export abstract class PollService {
/**
* Gets a label for a poll Key
*
* @param key yes, no, abstain or something like that
* @returns A short descriptive name for the poll keys
*/
public getLabel(key: CalculablePollKey | PollVoteValue): string {
@ -151,11 +151,11 @@ export abstract class PollService {
/**
* retrieve special labels for a poll value
*
* @param value
* @returns the label for a non-positive value, according to
* {@link specialPollVotes}. Positive values will return as string
* representation of themselves
*
* @param value check value for special numbers
* @returns the label for a non-positive value, according to
*/
public getSpecialLabel(value: number): string {
if (value >= 0) {
@ -166,10 +166,9 @@ export abstract class PollService {
}
/**
* Get the progressbar class for a decision key
*
* @param key
* Get the progress bar class for a decision key
*
* @param key a calculable poll key (like yes or no)
* @returns a css class designing a progress bar in a color, or an empty string
*/
public getProgressBarColor(key: CalculablePollKey | PollVoteValue): string {

View File

@ -32,10 +32,10 @@ export class PollOption extends Deserializer {
}
});
if (input.votes) {
input.votes = input.votes.map(v => {
input.votes = input.votes.map(vote => {
return {
value: v.value,
weight: parseInt(v.weight, 10)
value: vote.value,
weight: parseInt(vote.weight, 10)
};
});
}

View File

@ -33,11 +33,12 @@ export class Poll extends Deserializer {
// cast stringify numbers
if (typeof input === 'object') {
const numberifyKeys = ['id', 'votesvalid', 'votesinvalid', 'votescast', 'assignment_id'];
Object.keys(input).forEach(key => {
for (const key of Object.keys(input)) {
if (numberifyKeys.includes(key) && typeof input[key] === 'string') {
input[key] = parseInt(input[key], 10);
}
});
}
}
super(input);
}

View File

@ -1,4 +1,8 @@
<os-head-bar [mainButton]="operator.hasPerms('assignments.can_manage')" (mainEvent)="onPlusButton()" [multiSelectMode]="isMultiSelect">
<os-head-bar
[mainButton]="operator.hasPerms('assignments.can_manage')"
(mainEvent)="onPlusButton()"
[multiSelectMode]="isMultiSelect"
>
<!-- Title -->
<div class="title-slot"><h2 translate>Elections</h2></div>
<!-- Menu -->
@ -91,6 +95,10 @@
<mat-icon>done_all</mat-icon>
<span translate>Select all</span>
</button>
<button mat-menu-item (click)="deselectAll()">
<mat-icon>clear</mat-icon>
<span translate>Deselect all</span>
</button>
<mat-divider></mat-divider>
<button
mat-menu-item

View File

@ -15,8 +15,7 @@ import { StorageService } from 'app/core/core-services/storage.service';
import { ViewAssignment } from '../models/view-assignment';
/**
* Listview for the assignments
*
* List view for the assignments
*/
@Component({
selector: 'os-assignment-list',
@ -26,7 +25,9 @@ import { ViewAssignment } from '../models/view-assignment';
export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignment, Assignment> implements OnInit {
/**
* Constructor.
*
* @param titleService
* @param storage
* @param translate
* @param matSnackBar
* @param repo the repository
@ -51,7 +52,7 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
public operator: OperatorService
) {
super(titleService, translate, matSnackBar, route, storage, filterService, sortService);
// activate multiSelect mode for this listview
// activate multiSelect mode for this list view
this.canMultiSelect = true;
}

View File

@ -3,10 +3,10 @@ import { NgModule } from '@angular/core';
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component';
import { AssignmentListComponent } from './assignment-list/assignment-list.component';
import { AssignmentsRoutingModule } from './assignments-routing.module';
import { SharedModule } from '../../shared/shared.module';
import { AssignmentPollComponent } from './components/assignment-poll/assignment-poll.component';
import { AssignmentPollDialogComponent } from './components/assignment-poll/assignment-poll-dialog.component';
import { AssignmentsRoutingModule } from './assignments-routing.module';
import { SharedModule } from '../../shared/shared.module';
@NgModule({
imports: [CommonModule, AssignmentsRoutingModule, SharedModule],

View File

@ -1,18 +1,16 @@
import { Router, ActivatedRoute } from '@angular/router';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatSnackBar, MatSelectChange } from '@angular/material';
import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { BehaviorSubject } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from '../../../base/base-view';
import { Assignment } from 'app/shared/models/assignments/assignment';
import { AssignmentPollService } from '../../services/assignment-poll.service';
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
import { AssignmentUser } from 'app/shared/models/assignments/assignment-user';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ConstantsService } from 'app/core/ui-services/constants.service';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
@ -34,7 +32,7 @@ import { ViewUser } from 'app/site/users/models/view-user';
templateUrl: './assignment-detail.component.html',
styleUrls: ['./assignment-detail.component.scss']
})
export class AssignmentDetailComponent extends BaseViewComponent implements OnInit, OnDestroy {
export class AssignmentDetailComponent extends BaseViewComponent implements OnInit {
/**
* Determines if the assignment is new
*/
@ -72,17 +70,17 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
public assignmentForm: FormGroup;
/**
* Used in the seacrhValue selector to assign tags
* Used in the search Value selector to assign tags
*/
public tagsObserver: BehaviorSubject<ViewTag[]>;
/**
* Used in the seacrhValue selector to assign an agenda item
* Used in the search Value selector to assign an agenda item
*/
public agendaObserver: BehaviorSubject<ViewItem[]>;
/**
* Sets the assignment, e.g. via an autoupdate. Reload important things here:
* Sets the assignment, e.g. via an auto update. Reload important things here:
* - Poll base values are be recalculated
*
* @param assignment the assignment to set
@ -253,7 +251,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
}
/**
* TODO: change/update the assignment form values
* Changes/updates the assignment form values
*
* @param assignment
*/
@ -282,7 +280,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
/**
* Creates a new Poll
* TODO: directly open poll dialog
* TODO: directly open poll dialog?
*/
public async createPoll(): Promise<void> {
await this.repo.addPoll(this.assignment).then(null, this.raiseError);
@ -304,14 +302,12 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
/**
* Adds a user to the list of candidates
*
* @param user
*/
public async addUser(): Promise<void> {
const candId = this.candidatesForm.get('candidate').value;
this.candidatesForm.setValue({ candidate: null });
if (candId) {
await this.repo.addCandidate(candId, this.assignment).then(null, this.raiseError);
await this.repo.changeCandidate(candId, this.assignment).then(null, this.raiseError);
}
}
@ -320,8 +316,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
*
* @param user Assignment User
*/
public async removeUser(user: AssignmentUser): Promise<void> {
await this.repo.deleteCandidate(user, this.assignment).then(null, this.raiseError);
public async removeUser(user: ViewUser): Promise<void> {
await this.repo.changeCandidate(user.id, this.assignment).then(null, this.raiseError);
}
/**
@ -343,7 +339,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
);
} else {
this.newAssignment = true;
// TODO set defaults
// TODO set defaults?
this.assignment = new ViewAssignment(new Assignment());
this.patchForm(this.assignment);
this.setEditMode(true);

View File

@ -63,12 +63,11 @@
<input
type="number"
matInput
[value]="getSumValue('votestotal')"
(change)="setSumValue('votestotal', $event.target.value)"
[value]="getSumValue('votescast')"
(change)="setSumValue('votescast', $event.target.value)"
/>
<mat-label translate>Total votes</mat-label>
</mat-form-field>
</div>
<div class="submit-buttons">
<button mat-button (click)="submit()">{{ 'Save' | translate }}</button>

View File

@ -69,7 +69,7 @@ export class AssignmentPollDialogComponent {
/**
* Validates candidates input (every candidate has their options filled in),
* submits and closes the dialog if successfull, else displays an error popup.
* submits and closes the dialog if successful, else displays an error popup.
* TODO better validation
*/
public submit(): void {
@ -123,14 +123,14 @@ export class AssignmentPollDialogComponent {
* @param candidate the candidate for whom to update the value
* @param newData the new value
*/
public setValue(value: PollVoteValue, candidate: PollOption, newData: number): void {
public setValue(value: PollVoteValue, candidate: PollOption, newData: string): void {
const vote = candidate.votes.find(v => v.value === value);
if (vote) {
vote.weight = +newData;
vote.weight = parseInt(newData, 10);
} else {
candidate.votes.push({
value: value,
weight: +newData
weight: parseInt(newData, 10)
});
}
}
@ -142,7 +142,7 @@ export class AssignmentPollDialogComponent {
* @param candidate the pollOption
* @returns the currently entered number or undefined if no number has been set
*/
public getValue(value: PollVoteValue, candidate: PollOption): number {
public getValue(value: PollVoteValue, candidate: PollOption): number | undefined {
const val = candidate.votes.find(v => v.value === value);
return val ? val.weight : undefined;
}
@ -151,10 +151,10 @@ export class AssignmentPollDialogComponent {
* Retrieves a per-poll value
*
* @param value
* @returns integer or null
* @returns integer or undefined
*/
public getSumValue(value: summaryPollKeys): number | null {
return this.data.poll[value] || null;
public getSumValue(value: summaryPollKeys): number | undefined {
return this.data.poll[value] || undefined;
}
/**
@ -164,6 +164,6 @@ export class AssignmentPollDialogComponent {
* @param weight
*/
public setSumValue(value: summaryPollKeys, weight: string): void {
this.data.poll[value] = +weight;
this.data.poll[value] = parseInt(weight, 10);
}
}

View File

@ -1,5 +1,5 @@
import { Component, OnInit, Input } from '@angular/core';
import { MatDialog } from '@angular/material';
import { MatDialog, MatSnackBar } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
@ -12,17 +12,18 @@ import { Poll } from 'app/shared/models/assignments/poll';
import { PollOption } from 'app/shared/models/assignments/poll-option';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewAssignment } from '../../models/view-assignment';
import { BaseViewComponent } from 'app/site/base/base-view';
import { Title } from '@angular/platform-browser';
/**
* Component for a single assignment poll. Used in assignment detail view
* TODO DOCU
*/
@Component({
selector: 'os-assignment-poll',
templateUrl: './assignment-poll.component.html',
styleUrls: ['./assignment-poll.component.scss']
})
export class AssignmentPollComponent implements OnInit {
export class AssignmentPollComponent extends BaseViewComponent implements OnInit {
/**
* The related assignment (used for metainfos, e.g. related user names)
*/
@ -52,6 +53,8 @@ export class AssignmentPollComponent implements OnInit {
}
/**
* Gets the voting options
*
* @returns all used (not undefined) option-independent values that are
* used in this poll (e.g.)
*/
@ -60,6 +63,8 @@ export class AssignmentPollComponent implements OnInit {
}
/**
* Gets the translated poll method name
*
* TODO: check/improve text here
*
* @returns a name for the poll method this poll is set to (which is determined
@ -92,15 +97,20 @@ export class AssignmentPollComponent implements OnInit {
* @param promptService Prompts for confirmation dialogs
*/
public constructor(
titleService: Title,
matSnackBar: MatSnackBar,
public pollService: AssignmentPollService,
private operator: OperatorService,
private assignmentRepo: AssignmentRepositoryService,
public translate: TranslateService,
public dialog: MatDialog,
private promptService: PromptService
) {}
) {
super(titleService, translate, matSnackBar);
}
/**
* Gets the currently selected majority choice option from the repo
*/
public ngOnInit(): void {
this.majorityChoice =
@ -116,17 +126,17 @@ export class AssignmentPollComponent implements OnInit {
public async onDeletePoll(): Promise<void> {
const title = this.translate.instant('Are you sure you want to delete this poll?');
if (await this.promptService.open(title, null)) {
await this.assignmentRepo.deletePoll(this.poll);
await this.assignmentRepo.deletePoll(this.poll).then(null, this.raiseError);
}
// TODO error handling
}
/**
* Print the PDF of this poll with the corresponding options and numbers
*
* TODO Print the ballots for this poll.
*/
public printBallot(poll: Poll): void {
this.promptService.open('TODO', 'TODO');
// TODO Print ballot not implemented
this.raiseError('Not yet implemented');
}
/**
@ -136,7 +146,7 @@ export class AssignmentPollComponent implements OnInit {
* @returns the full_name for the candidate
*/
public getCandidateName(option: PollOption): string {
const user = this.assignment.candidates.find(c => c.id === option.candidate_id);
const user = this.assignment.candidates.find(candidate => candidate.id === option.candidate_id);
return user ? user.full_name : '';
// TODO this.assignment.candidates may not contain every candidates' name (if deleted later)
// so we should rather use this.userRepo.getViewModel(option.id).full_name
@ -161,7 +171,6 @@ export class AssignmentPollComponent implements OnInit {
/**
* Opens the {@link AssignmentPollDialogComponent} dialog and then updates the votes, if the dialog
* closes successfully (validation is done there)
*
*/
public enterVotes(): void {
// TODO deep copy of this.poll (JSON parse is ugly workaround)
@ -174,13 +183,12 @@ export class AssignmentPollComponent implements OnInit {
data: data,
maxHeight: '90vh',
minWidth: '300px',
maxWidth: '80vh',
maxWidth: '80vw',
disableClose: true
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.assignmentRepo.updateVotes(result, this.poll);
// TODO error handling
this.assignmentRepo.updateVotes(result, this.poll).then(null, this.raiseError);
}
});
}
@ -188,6 +196,7 @@ export class AssignmentPollComponent implements OnInit {
/**
* Updates the majority method for this poll
*
* @param method the selected majority method
*/
public setMajority(method: MajorityMethod): void {
this.majorityChoice = method;
@ -209,9 +218,10 @@ export class AssignmentPollComponent implements OnInit {
if (!this.operator.hasPerms('assignments.can_manage')) {
return;
}
// TODO additional conditions: assignment not finished?
const candidate = this.assignment.assignment.assignment_related_users.find(
u => u.user_id === option.candidate_id
user => user.user_id === option.candidate_id
);
if (candidate) {
this.assignmentRepo.markElected(candidate, this.assignment, !option.is_elected);

View File

@ -69,6 +69,9 @@ export class ViewAssignment extends BaseAgendaViewModel {
public constructor(assignment: Assignment, relatedUser?: ViewUser[], agendaItem?: ViewItem, tags?: ViewTag[]) {
super(Assignment.COLLECTIONSTRING);
console.log('related user: ', relatedUser);
this._assignment = assignment;
this._relatedUser = relatedUser;
this._agendaItem = agendaItem;

View File

@ -16,7 +16,7 @@ export type AssignmentPollMethod = 'yn' | 'yna' | 'votes';
type AssignmentPercentBase = 'YES_NO_ABSTAIN' | 'YES_NO' | 'VALID' | 'CAST' | 'DISABLED';
/**
* Service class for motion polls.
* Service class for assignment polls.
*/
@Injectable({
providedIn: 'root'
@ -46,6 +46,7 @@ export class AssignmentPollService extends PollService {
/**
* Constructor. Subscribes to the configuration values needed
*
* @param config ConfigService
*/
public constructor(config: ConfigService) {
@ -59,12 +60,11 @@ export class AssignmentPollService extends PollService {
config
.get<AssignmentPercentBase>('assignments_poll_100_percent_base')
.subscribe(base => (this.percentBase = base));
// assignments_add_candidates_to_list_of_speakers boolean
}
/**
* Get the base amount for the 100% calculations. Note that some poll methods
* (e.g. yes/no/abstain may have a diffferent percentage base and will return null here)
* (e.g. yes/no/abstain may have a different percentage base and will return null here)
*
* @param poll
* @returns The amount of votes indicating the 100% base
@ -120,6 +120,8 @@ export class AssignmentPollService extends PollService {
/**
* Check if the option in a poll is abstract (percentages should not be calculated)
*
* @param poll
* @param option
* @returns true if the poll has no percentages, the poll option is a special value,
* or if the calculations are disabled in the config
*/