From df1047fc765d5db2d7e2d05f9607867226e448f6 Mon Sep 17 00:00:00 2001 From: Joshua Sangmeister Date: Thu, 30 Jan 2020 16:59:46 +0100 Subject: [PATCH] various improvements for polls --- client/src/app/app.component.ts | 26 ++++++++++++ .../search-value-selector.component.html | 2 +- .../search-value-selector.component.ts | 4 ++ client/src/app/shared/models/core/config.ts | 5 ++- .../assignment-poll.component.html | 2 +- .../config-field/config-field.component.html | 14 +++++++ .../config-field/config-field.component.ts | 22 +++++++++- .../site/motions/models/view-motion-poll.ts | 12 +++++- .../motion-poll/motion-poll.component.html | 11 ++++- .../polls/components/base-poll.component.ts | 11 ++++- .../poll-form/poll-form.component.ts | 26 +++++++++--- .../app/site/polls/models/view-base-poll.ts | 41 ++++++++++++------- openslides/assignments/config_variables.py | 11 ++++- openslides/core/config.py | 8 ++++ openslides/motions/config_variables.py | 10 +++++ 15 files changed, 173 insertions(+), 32 deletions(-) diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 2730ea888..6a1334f08 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -30,6 +30,10 @@ declare global { mapToObject(f: (item: T) => { [key: string]: any }): { [key: string]: any }; } + interface Set { + equals(other: Set): boolean; + } + /** * Enhances the number object to calculate real modulo operations. * (not remainder) @@ -96,6 +100,7 @@ export class AppComponent { // change default JS functions this.overloadArrayFunctions(); + this.overloadSetFunctions(); this.overloadModulo(); // Wait until the App reaches a stable state. @@ -179,6 +184,27 @@ export class AppComponent { }); } + /** + * Adds some functions to Set. + */ + private overloadSetFunctions(): void { + // equals + Object.defineProperty(Set.prototype, 'equals', { + value: function(other: Set): boolean { + const _difference = new Set(this); + for (const elem of other) { + if (_difference.has(elem)) { + _difference.delete(elem); + } else { + return false; + } + } + return !_difference.size; + }, + enumerable: false + }); + } + /** * Enhances the number object with a real modulo operation (not remainder). * TODO: Remove this, if the remainder operation is changed to modulo. diff --git a/client/src/app/shared/components/search-value-selector/search-value-selector.component.html b/client/src/app/shared/components/search-value-selector/search-value-selector.component.html index 22d1a77ee..34089a553 100644 --- a/client/src/app/shared/components/search-value-selector/search-value-selector.component.html +++ b/client/src/app/shared/components/search-value-selector/search-value-selector.component.html @@ -1,4 +1,4 @@ - +
diff --git a/client/src/app/shared/components/search-value-selector/search-value-selector.component.ts b/client/src/app/shared/components/search-value-selector/search-value-selector.component.ts index 6275ebb61..2e66331fb 100644 --- a/client/src/app/shared/components/search-value-selector/search-value-selector.component.ts +++ b/client/src/app/shared/components/search-value-selector/search-value-selector.component.ts @@ -17,6 +17,7 @@ import { Observable } from 'rxjs'; import { auditTime } from 'rxjs/operators'; import { BaseFormControlComponent } from 'app/shared/models/base/base-form-control'; +import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher'; import { Selectable } from '../selectable'; /** @@ -69,6 +70,9 @@ export class SearchValueSelectorComponent extends BaseFormControlComponent - diff --git a/client/src/app/site/config/components/config-field/config-field.component.html b/client/src/app/site/config/components/config-field/config-field.component.html index 6336fc4e5..f146b2a10 100644 --- a/client/src/app/site/config/components/config-field/config-field.component.html +++ b/client/src/app/site/config/components/config-field/config-field.component.html @@ -7,6 +7,9 @@ + + + @@ -29,6 +32,17 @@ + + + + (); + /** used by the groups config type */ + public groupObservable: Observable = null; + /** * The usual component constructor. datetime pickers will set their locale * to the current language chosen @@ -130,7 +136,8 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes protected translate: TranslateService, private formBuilder: FormBuilder, private cd: ChangeDetectorRef, - public repo: ConfigRepositoryService + public repo: ConfigRepositoryService, + private groupRepo: GroupRepositoryService ) { super(titleService, translate); } @@ -139,6 +146,9 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes * Sets up the form for this config field. */ public ngOnInit(): void { + // filter out empty results in group observable. We never have no groups and it messes up the settings change detection + this.groupObservable = this.groupRepo.getViewModelListObservable().pipe(filter(groups => !!groups.length)); + this.form = this.formBuilder.group({ value: [''], date: [''], @@ -226,6 +236,14 @@ export class ConfigFieldComponent extends BaseComponent implements OnInit, OnDes const time = this.form.get('time').value; value = this.dateAndTimeToUnix(date, time); } + if (this.configItem.inputType === 'groups') { + // we have to check here explicitly if nothing changed because of the search value selector + const newS = new Set(value); + const oldS = new Set(this.configItem.value); + if (newS.equals(oldS)) { + return; + } + } this.sendUpdate(value); this.cd.detectChanges(); } diff --git a/client/src/app/site/motions/models/view-motion-poll.ts b/client/src/app/site/motions/models/view-motion-poll.ts index 737fe7913..e1280df2c 100644 --- a/client/src/app/site/motions/models/view-motion-poll.ts +++ b/client/src/app/site/motions/models/view-motion-poll.ts @@ -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 } from 'app/shared/models/poll/base-poll'; +import { 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'; @@ -75,6 +75,16 @@ export class ViewMotionPoll extends ViewBasePoll implements MotionPo public get pollmethodVerbose(): string { return MotionPollMethodsVerbose[this.pollmethod]; } + + /** + * Override from base poll to skip started state in analog poll type + */ + public getNextStates(): { [key: number]: string } { + if (this.poll.type === 'analog' && this.state === PollState.Created) { + return null; + } + return super.getNextStates(); + } } export interface ViewMotionPoll extends MotionPoll { diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.html b/client/src/app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.html index bfbec91b3..924684f3e 100644 --- a/client/src/app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.html +++ b/client/src/app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.html @@ -20,6 +20,7 @@
{{ poll.stateVerbose }} + + {{ poll.stateVerbose }} + @@ -105,7 +114,7 @@ - diff --git a/client/src/app/site/polls/components/base-poll.component.ts b/client/src/app/site/polls/components/base-poll.component.ts index cc72f4bb4..4ae72c268 100644 --- a/client/src/app/site/polls/components/base-poll.component.ts +++ b/client/src/app/site/polls/components/base-poll.component.ts @@ -30,8 +30,15 @@ export abstract class BasePollComponent extends BaseView super(titleService, translate, matSnackBar); } - public changeState(key: PollState): void { - key === PollState.Created ? this.repo.resetPoll(this._poll) : this.repo.changePollState(this._poll); + public async changeState(key: PollState): Promise { + if (key === PollState.Created) { + const title = this.translate.instant('Are you sure you want to reset this poll? All Votes will be lost.'); + if (await this.promptService.open(title)) { + this.repo.resetPoll(this._poll).catch(this.raiseError); + } + } else { + this.repo.changePollState(this._poll).catch(this.raiseError); + } } /** diff --git a/client/src/app/site/polls/components/poll-form/poll-form.component.ts b/client/src/app/site/polls/components/poll-form/poll-form.component.ts index fbc99ccca..d88fd9a83 100644 --- a/client/src/app/site/polls/components/poll-form/poll-form.component.ts +++ b/client/src/app/site/polls/components/poll-form/poll-form.component.ts @@ -7,6 +7,7 @@ import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; +import { ConfigService } from 'app/core/ui-services/config.service'; import { PercentBase } from 'app/shared/models/poll/base-poll'; import { PollType } from 'app/shared/models/poll/base-poll'; import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll'; @@ -14,6 +15,7 @@ import { BaseViewComponent } from 'app/site/base/base-view'; import { MajorityMethodVerbose, PercentBaseVerbose, + PollClassType, PollPropertyVerbose, PollTypeVerbose, ViewBasePoll @@ -85,7 +87,8 @@ export class PollFormComponent extends BaseViewComponent implements OnInit { snackbar: MatSnackBar, private fb: FormBuilder, private groupRepo: GroupRepositoryService, - public pollService: PollService + public pollService: PollService, + private configService: ConfigService ) { super(title, translate, snackbar); this.initContentForm(); @@ -104,6 +107,13 @@ export class PollFormComponent extends BaseViewComponent implements OnInit { } if (this.data) { + if (!this.data.groups_id) { + if (this.data.collectionString === ViewAssignmentPoll.COLLECTIONSTRING) { + this.data.groups_id = this.configService.instant('assignment_poll_default_groups'); + } else { + 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]); @@ -150,12 +160,16 @@ export class PollFormComponent extends BaseViewComponent implements OnInit { * @param data Passing the properties of the poll. */ private updatePollValues(data: { [key: string]: any }): void { - this.pollValues = Object.entries(data) - .filter(([key, _]) => key === 'type' || key === 'pollmethod') - .map(([key, value]) => [ - this.pollService.getVerboseNameForKey(key), - this.pollService.getVerboseNameForValue(key, value as string) + this.pollValues = [ + [this.pollService.getVerboseNameForKey('type'), this.pollService.getVerboseNameForValue('type', data.type)] + ]; + // show pollmethod only for assignment polls + if (this.data.pollClassType === PollClassType.Assignment) { + this.pollValues.push([ + this.pollService.getVerboseNameForKey('pollmethod'), + this.pollService.getVerboseNameForValue('pollmethod', data.pollmethod) ]); + } if (data.type !== 'analog') { this.pollValues.push([ this.pollService.getVerboseNameForKey('groups'), diff --git a/client/src/app/site/polls/models/view-base-poll.ts b/client/src/app/site/polls/models/view-base-poll.ts index ea2275220..6efde883f 100644 --- a/client/src/app/site/polls/models/view-base-poll.ts +++ b/client/src/app/site/polls/models/view-base-poll.ts @@ -8,6 +8,11 @@ import { ViewMotionOption } from 'app/site/motions/models/view-motion-option'; import { ViewGroup } from 'app/site/users/models/view-group'; import { ViewUser } from 'app/site/users/models/view-user'; +export enum PollClassType { + Motion = 'motion', + Assignment = 'assignment' +} + export const PollClassTypeVerbose = { motion: 'Motion poll', assignment: 'Assignment poll' @@ -20,6 +25,13 @@ export const PollStateVerbose = { 4: 'Published' }; +export const PollStateChangeActionVerbose = { + 1: 'Reset', + 2: 'Start voting', + 3: 'End voting', + 4: 'Publish' +}; + export const PollTypeVerbose = { analog: 'Analog voting', named: 'Named voting', @@ -55,8 +67,6 @@ export const PercentBaseVerbose = { }; export abstract class ViewBasePoll = any> extends BaseProjectableViewModel { - private _tableData: {}[] = []; - public get tableData(): {}[] { if (!this._tableData.length) { this._tableData = this.generateTableData(); @@ -91,24 +101,25 @@ export abstract class ViewBasePoll = any> extends Bas public get percentBaseVerbose(): string { return PercentBaseVerbose[this.onehundred_percent_base]; } - - /** - * returns a mapping "verbose_state" -> "state_id" for all valid next states - */ - public get nextStates(): { [key: number]: string } { - const next_state = (this.state % Object.keys(PollStateVerbose).length) + 1; - const states = {}; - states[PollStateVerbose[next_state]] = next_state; - if (this.state === PollState.Finished) { - states[PollStateVerbose[PollState.Created]] = PollState.Created; - } - return states; - } + private _tableData: {}[] = []; public abstract readonly pollClassType: 'motion' | 'assignment'; public canBeVotedFor: () => boolean; + /** + * returns a mapping "verbose_state" -> "state_id" for all valid next states + */ + public getNextStates(): { [key: number]: string } { + const next_state = (this.state % Object.keys(PollStateVerbose).length) + 1; + const states = {}; + states[PollStateChangeActionVerbose[next_state]] = next_state; + if (this.state === PollState.Finished) { + states[PollStateChangeActionVerbose[PollState.Created]] = PollState.Created; + } + return states; + } + public abstract getSlide(): ProjectorElementBuildDeskriptor; public abstract getContentObject(): BaseViewModel; diff --git a/openslides/assignments/config_variables.py b/openslides/assignments/config_variables.py index 5412b47c9..7b9a45a4a 100644 --- a/openslides/assignments/config_variables.py +++ b/openslides/assignments/config_variables.py @@ -5,7 +5,6 @@ from openslides.core.config import ConfigVariable def get_config_variables(): """ Generator which yields all config variables of this app. - They are grouped in 'Ballot and ballot papers' and 'PDF'. The generator has to be evaluated during app loading (see apps.py). """ @@ -50,6 +49,16 @@ def get_config_variables(): subgroup="Elections", ) + yield ConfigVariable( + name="assignment_poll_default_groups", + default_value=[], + input_type="groups", + label="Default groups for named and pseudoanonymous assignment polls", + weight=415, + group="Voting", + subgroup="Elections", + ) + # PDF yield ConfigVariable( name="assignments_pdf_title", diff --git a/openslides/core/config.py b/openslides/core/config.py index cf913e84d..d4f39a271 100644 --- a/openslides/core/config.py +++ b/openslides/core/config.py @@ -23,6 +23,7 @@ INPUT_TYPE_MAPPING = { "datetimepicker": int, "static": dict, "translations": list, + "groups": list, } ALLOWED_NONE = ("datetimepicker",) @@ -143,6 +144,13 @@ class ConfigHandler: ): raise ConfigError("Invalid input. Choice does not match.") + if config_variable.input_type == "groups": + from ..users.models import Group + + groups = set(group.id for group in Group.objects.all()) + if not groups.issuperset(set(value)): + raise ConfigError("Invalid input. Chosen group does not exist.") + for validator in config_variable.validators: try: validator(value) diff --git a/openslides/motions/config_variables.py b/openslides/motions/config_variables.py index abb875625..7105939c2 100644 --- a/openslides/motions/config_variables.py +++ b/openslides/motions/config_variables.py @@ -418,3 +418,13 @@ def get_config_variables(): 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", + )