From 3d4bd67980e7d3e7dddec707bb22d4b33a9d46b7 Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Thu, 22 Nov 2018 12:33:40 +0100 Subject: [PATCH] new configs for statute amendments; improved the majorityMethod config variable Removed none from os-search-value-selector; improved list of speakers --- .../src/app/core/services/config.service.ts | 4 +- .../search-value-selector.component.html | 13 ++-- .../search-value-selector.component.ts | 10 ++- .../speaker-list/speaker-list.component.ts | 28 +++++--- .../src/app/site/config/models/view-config.ts | 1 - .../services/config-repository.service.ts | 3 +- .../motion-detail.component.html | 23 +++--- .../motion-detail/motion-detail.component.ts | 71 ++++++++++++++++--- .../app/site/motions/models/view-motion.ts | 4 ++ .../services/motion-repository.service.ts | 12 ---- openslides/assignments/config_variables.py | 6 +- openslides/core/config.py | 1 - openslides/motions/config_variables.py | 32 +++++++-- openslides/poll/majority.py | 7 ++ 14 files changed, 156 insertions(+), 59 deletions(-) create mode 100644 openslides/poll/majority.py diff --git a/client/src/app/core/services/config.service.ts b/client/src/app/core/services/config.service.ts index b47b8de65..eadc39042 100644 --- a/client/src/app/core/services/config.service.ts +++ b/client/src/app/core/services/config.service.ts @@ -67,10 +67,10 @@ export class ConfigService extends OpenSlidesComponent { * * @param key The config value to get from. */ - public get(key: string): Observable { + public get(key: string): Observable { if (!this.configSubjects[key]) { this.configSubjects[key] = new BehaviorSubject(this.instant(key)); } - return this.configSubjects[key].asObservable(); + return this.configSubjects[key].asObservable() as Observable; } } 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 b7a6c3b78..2e7fabf9b 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,12 +1,14 @@ - + -
- None +
+ + None +
- {{selectedItem.getTitle(translate)}} + {{ selectedItem.getTitle(translate) }} @@ -15,7 +17,8 @@ Selected values:

- {{selectedItem.name}} + + {{ selectedItem.name }} cancel 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 476b7d605..a668d3999 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 @@ -22,8 +22,8 @@ import { Selectable } from '../selectable'; * ngDefaultControl * [multiple]="true" * placeholder="Placeholder" - * [InputListValues]="myListValues", - * [form]="myform_name", + * [InputListValues]="myListValues" + * [form]="myform_name" * [formControl]="myformcontrol"> * * ``` @@ -68,6 +68,12 @@ export class SearchValueSelectorComponent implements OnInit, OnDestroy { @Input() public multiple: boolean; + /** + * Decide, if none should be included, if multiple is false. + */ + @Input() + public includeNone = false; + /** * The inputlist subject. Subscribes to it and updates the selector, if the subject * changes its values. diff --git a/client/src/app/site/agenda/components/speaker-list/speaker-list.component.ts b/client/src/app/site/agenda/components/speaker-list/speaker-list.component.ts index e21b56268..6bb74be2e 100644 --- a/client/src/app/site/agenda/components/speaker-list/speaker-list.component.ts +++ b/client/src/app/site/agenda/components/speaker-list/speaker-list.component.ts @@ -10,6 +10,10 @@ import { DataStoreService } from 'app/core/services/data-store.service'; import { AgendaRepositoryService } from '../../services/agenda-repository.service'; import { ViewItem } from '../../models/view-item'; import { OperatorService } from 'app/core/services/operator.service'; +import { BaseViewComponent } from 'app/site/base/base-view'; +import { Title } from '@angular/platform-browser'; +import { TranslateService } from '@ngx-translate/core'; +import { MatSnackBar } from '@angular/material'; /** * The list of speakers for agenda items. @@ -19,7 +23,7 @@ import { OperatorService } from 'app/core/services/operator.service'; templateUrl: './speaker-list.component.html', styleUrls: ['./speaker-list.component.scss'] }) -export class SpeakerListComponent implements OnInit { +export class SpeakerListComponent extends BaseViewComponent implements OnInit { /** * Holds the view item to the given topic */ @@ -52,17 +56,24 @@ export class SpeakerListComponent implements OnInit { /** * Constructor for speaker list component + * @param title + * @param translate + * @param snackBar * @param route Angulars ActivatedRoute * @param DS the DataStore * @param itemRepo Repository fpr agenda items * @param op the current operator */ public constructor( + title: Title, + translate: TranslateService, + snackBar: MatSnackBar, private route: ActivatedRoute, private DS: DataStoreService, private itemRepo: AgendaRepositoryService, private op: OperatorService ) { + super(title, translate, snackBar) this.addSpeakerForm = new FormGroup({ user_id: new FormControl([]) }); this.getAgendaItemByUrl(); } @@ -115,9 +126,8 @@ export class SpeakerListComponent implements OnInit { * Create a speaker out of an id * @param userId the user id to add to the list. No parameter adds the operators user as speaker. */ - public async addNewSpeaker(userId?: number): Promise { - await this.itemRepo.addSpeaker(userId, this.viewItem.item); - this.addSpeakerForm.reset(); + public addNewSpeaker(userId?: number): void { + this.itemRepo.addSpeaker(userId, this.viewItem.item).then(() => this.addSpeakerForm.reset(), this.raiseError); } /** @@ -128,7 +138,7 @@ export class SpeakerListComponent implements OnInit { public onSortingChange(listInNewOrder: ViewSpeaker[]): void { // extract the ids from the ViewSpeaker array const userIds = listInNewOrder.map(speaker => speaker.id); - this.itemRepo.sortSpeakers(userIds, this.viewItem.item); + this.itemRepo.sortSpeakers(userIds, this.viewItem.item).then(null, this.raiseError); } /** @@ -136,14 +146,14 @@ export class SpeakerListComponent implements OnInit { * @param item the speaker marked in the list */ public onStartButton(item: ViewSpeaker): void { - this.itemRepo.startSpeaker(item.id, this.viewItem.item); + this.itemRepo.startSpeaker(item.id, this.viewItem.item).then(null, this.raiseError); } /** * Click on the mic-cross button */ public onStopButton(): void { - this.itemRepo.stopSpeaker(this.viewItem.item); + this.itemRepo.stopSpeaker(this.viewItem.item).then(null, this.raiseError); } /** @@ -151,7 +161,7 @@ export class SpeakerListComponent implements OnInit { * @param item */ public onMarkButton(item: ViewSpeaker): void { - this.itemRepo.markSpeaker(item.user.id, !item.marked, this.viewItem.item); + this.itemRepo.markSpeaker(item.user.id, !item.marked, this.viewItem.item).then(null, this.raiseError); } /** @@ -159,7 +169,7 @@ export class SpeakerListComponent implements OnInit { * @param item */ public onDeleteButton(item?: ViewSpeaker): void { - this.itemRepo.deleteSpeaker(this.viewItem.item, item ? item.id : null); + this.itemRepo.deleteSpeaker(this.viewItem.item, item ? item.id : null).then(null, this.raiseError); } /** diff --git a/client/src/app/site/config/models/view-config.ts b/client/src/app/site/config/models/view-config.ts index 271f12a77..0b863a99e 100644 --- a/client/src/app/site/config/models/view-config.ts +++ b/client/src/app/site/config/models/view-config.ts @@ -15,7 +15,6 @@ type ConfigInputType = | 'boolean' | 'markupText' | 'integer' - | 'majorityMethod' | 'choice' | 'datetimepicker' | 'colorpicker' diff --git a/client/src/app/site/config/services/config-repository.service.ts b/client/src/app/site/config/services/config-repository.service.ts index 25fbcb10e..c7acb7a58 100644 --- a/client/src/app/site/config/services/config-repository.service.ts +++ b/client/src/app/site/config/services/config-repository.service.ts @@ -220,8 +220,7 @@ export class ConfigRepositoryService extends BaseRepository * @param config */ public createViewModel(config: Config): ViewConfig { - const vm = new ViewConfig(config); - return vm; + return new ViewConfig(config); } /** diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.html b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html index 558f6665e..5fb2edcaa 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.html +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html @@ -79,8 +79,7 @@ - - + info @@ -155,8 +154,8 @@
- +
@@ -172,8 +171,8 @@
- +
@@ -230,8 +229,16 @@ {{ motion.category }}
- + +
+
+ + +
+
+
diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts index d503b7833..ff5341da2 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts @@ -11,7 +11,7 @@ import { User } from '../../../../shared/models/users/user'; import { DataStoreService } from '../../../../core/services/data-store.service'; import { TranslateService } from '@ngx-translate/core'; import { Motion } from '../../../../shared/models/motions/motion'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Subscription, ReplaySubject, concat } from 'rxjs'; import { LineRange } from '../../services/diff.service'; import { MotionChangeRecommendationComponent, @@ -26,6 +26,8 @@ import { BaseViewComponent } from '../../../base/base-view'; import { ViewStatuteParagraph } from '../../models/view-statute-paragraph'; import { StatuteParagraphRepositoryService } from '../../services/statute-paragraph-repository.service'; import { ConfigService } from '../../../../core/services/config.service'; +import { Workflow } from 'app/shared/models/motions/workflow'; +import { take, takeWhile, multicast, skipWhile } from 'rxjs/operators'; /** * Component for the motion detail view @@ -71,9 +73,25 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { public newMotion = false; /** - * Target motion. Might be new or old + * Sets the motions, e.g. via an autoupdate. Reload important things here: + * - Reload the recommendation. Not changed with autoupdates, but if the motion is loaded this needs to run. */ - public motion: ViewMotion; + public set motion(value: ViewMotion) { + this._motion = value; + this.setupRecommender(); + } + + /** + * Returns the target motion. Might be the new one or old. + */ + public get motion(): ViewMotion { + return this._motion; + } + + /** + * Saves the target motion. Accessed via the getter and setter. + */ + private _motion: ViewMotion; /** * Value of the configuration variable `motions_statutes_enabled` - are statutes enabled? @@ -121,6 +139,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { */ public categoryObserver: BehaviorSubject; + /** + * Subject for the Categories + */ + public workflowObserver: BehaviorSubject; + /** * Subject for the Submitters */ @@ -141,6 +164,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { */ public recommender: string; + /** + * The subscription to the recommender config variable. + */ + private recommenderSubscription: Subscription; + /** * Constuct the detail view. * @@ -185,15 +213,17 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { this.submitterObserver = new BehaviorSubject(DS.getAll(User)); this.supporterObserver = new BehaviorSubject(DS.getAll(User)); this.categoryObserver = new BehaviorSubject(DS.getAll(Category)); + this.workflowObserver = new BehaviorSubject(DS.getAll(Workflow)); // Make sure the subjects are updated, when a new Model for the type arrives this.DS.changeObservable.subscribe(newModel => { if (newModel instanceof User) { this.submitterObserver.next(DS.getAll(User)); this.supporterObserver.next(DS.getAll(User)); - } - if (newModel instanceof Category) { + } else if (newModel instanceof Category) { this.categoryObserver.next(DS.getAll(Category)); + } else if (newModel instanceof Workflow) { + this.workflowObserver.next(DS.getAll(Workflow)); } }); this.configService.get('motions_statutes_enabled').subscribe( @@ -282,6 +312,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { recommendation_id: [''], submitters_id: [], supporters_id: [], + workflow_id: [], origin: [''] }); this.contentForm = this.formBuilder.group({ @@ -291,6 +322,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { statute_amendment: [''], // Internal value for the checkbox, not saved to the model statute_paragraph_id: [''] }); + this.updateWorkflowIdForCreateForm(); } /** @@ -474,6 +506,21 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { } } + public updateWorkflowIdForCreateForm(): void { + const isStatuteAmendment = !!this.contentForm.get('statute_amendment').value; + const configKey = isStatuteAmendment ? 'motions_statute_amendments_workflow' : 'motions_workflow'; + // TODO: This should just be a takeWhile(id => !id), but should include the last one where the id is OK. + // takeWhile will get a inclusive parameter, see https://github.com/ReactiveX/rxjs/pull/4115 + this.configService.get(configKey).pipe(multicast( + () => new ReplaySubject(1), + (ids) => ids.pipe(takeWhile(id => !id), o => concat(o, ids.pipe(take(1)))) + ), skipWhile(id => !id)).subscribe(id => { + this.metaInfoForm.patchValue({ + workflow_id: parseInt(id, 10), + }); + }); + } + /** * If the checkbox is deactivated, the statute_paragraph_id-field needs to be reset, as only that field is saved * @param {MatCheckboxChange} $event @@ -482,6 +529,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { this.contentForm.patchValue({ statute_paragraph_id: null }); + this.updateWorkflowIdForCreateForm(); } /** @@ -548,9 +596,13 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { /** * Observes the repository for changes in the motion recommender */ - public getRecommender(): void { - this.repo.getRecommenderObservable().subscribe(newRecommender => { - this.recommender = newRecommender; + public setupRecommender(): void { + const configKey = this.motion.isStatuteAmendment() ? 'motions_statute_recommendations_by' : 'motions_recommendations_by'; + if (this.recommenderSubscription) { + this.recommenderSubscription.unsubscribe(); + } + this.recommenderSubscription = this.configService.get(configKey).subscribe(recommender => { + this.recommender = recommender; }); } @@ -571,10 +623,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { /** * Init. - * Calls getRecommender and sets the surrounding motions to navigate back and forth + * Sets the surrounding motions to navigate back and forth */ public ngOnInit(): void { - this.getRecommender(); this.repo.getViewModelListObservable().subscribe(newMotionList => { if (newMotionList) { this.allMotions = newMotionList; diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index 320f429ba..e9c2f64ec 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -126,6 +126,10 @@ export class ViewMotion extends BaseViewModel { return this._workflow; } + public get workflow_id(): number { + return this.motion ? this.motion.workflow_id : null; + } + public get state(): WorkflowState { return this._state; } diff --git a/client/src/app/site/motions/services/motion-repository.service.ts b/client/src/app/site/motions/services/motion-repository.service.ts index e0d920482..b6f7c77d0 100644 --- a/client/src/app/site/motions/services/motion-repository.service.ts +++ b/client/src/app/site/motions/services/motion-repository.service.ts @@ -18,8 +18,6 @@ import { ViewStatuteParagraph } from '../models/view-statute-paragraph'; import { Identifiable } from '../../../shared/models/base/identifiable'; import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service'; import { HttpService } from 'app/core/services/http.service'; -import { ConfigService } from 'app/core/services/config.service'; -import { Observable } from 'rxjs'; import { Item } from 'app/shared/models/agenda/item'; import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component'; @@ -52,7 +50,6 @@ export class MotionRepositoryService extends BaseRepository mapperService: CollectionStringModelMapperService, private dataSend: DataSendService, private httpService: HttpService, - private configService: ConfigService, private readonly lineNumbering: LinenumberingService, private readonly diff: DiffService ) { @@ -144,15 +141,6 @@ export class MotionRepositoryService extends BaseRepository await this.httpService.put(restPath, { recommendation: stateId }); } - /** - * Returns the motions_recommendations_by observable from the config service - * - * @return an observable that contains the motions "Recommended by" string - */ - public getRecommenderObservable(): Observable { - return this.configService.get('motions_recommendations_by'); - } - /** * Sends the changed nodes to the server. * diff --git a/openslides/assignments/config_variables.py b/openslides/assignments/config_variables.py index 48938cb5a..778c70f72 100644 --- a/openslides/assignments/config_variables.py +++ b/openslides/assignments/config_variables.py @@ -1,6 +1,7 @@ from django.core.validators import MinValueValidator from openslides.core.config import ConfigVariable +from openslides.poll.majority import majorityMethods def get_config_variables(): @@ -47,8 +48,9 @@ def get_config_variables(): # TODO: Add server side validation of the choices. yield ConfigVariable( name='assignments_poll_default_majority_method', - default_value='simple_majority', - input_type='majorityMethod', + default_value=majorityMethods[0]['value'], + input_type='choice', + choices=majorityMethods, label='Required majority', help_text='Default method to check whether a candidate has reached the required majority.', weight=425, diff --git a/openslides/core/config.py b/openslides/core/config.py index f4ae2796f..de5946ff2 100644 --- a/openslides/core/config.py +++ b/openslides/core/config.py @@ -29,7 +29,6 @@ INPUT_TYPE_MAPPING = { 'choice': str, 'colorpicker': str, 'datetimepicker': int, - 'majorityMethod': str, 'static': dict, 'translations': list, } diff --git a/openslides/motions/config_variables.py b/openslides/motions/config_variables.py index df532a077..4a539fd7c 100644 --- a/openslides/motions/config_variables.py +++ b/openslides/motions/config_variables.py @@ -1,6 +1,7 @@ from django.core.validators import MinValueValidator from openslides.core.config import ConfigVariable +from openslides.poll.majority import majorityMethods from .models import Workflow @@ -34,6 +35,16 @@ def get_config_variables(): group='Motions', subgroup='General') + yield ConfigVariable( + name='motions_statute_amendments_workflow', + default_value='1', + input_type='choice', + label='Workflow of new statute amendments', + choices=get_workflow_choices, + weight=312, + group='Motions', + subgroup='General') + yield ConfigVariable( name='motions_identifier', default_value='per_category', @@ -124,6 +135,16 @@ def get_config_variables(): group='Motions', subgroup='General') + yield ConfigVariable( + name='motions_statute_recommendations_by', + default_value='', + label='Name of statute recommender', + help_text='Will be displayed as label before selected statute recommendation. ' + + 'Use an empty value to disable the statute recommendation system.', + weight=333, + group='Motions', + subgroup='General') + yield ConfigVariable( name='motions_recommendation_text_mode', default_value='original', @@ -134,7 +155,7 @@ def get_config_variables(): {'value': 'changed', 'display_name': 'Changed version'}, {'value': 'diff', 'display_name': 'Diff version'}, {'value': 'agreed', 'display_name': 'Final version'}), - weight=333, + weight=334, group='Motions', subgroup='General') @@ -145,7 +166,7 @@ def get_config_variables(): default_value=False, input_type='boolean', label='Activate statutes', - weight=334, + weight=335, group='Motions', subgroup='General') @@ -155,7 +176,7 @@ def get_config_variables(): default_value=False, input_type='boolean', label='Activate amendments', - weight=335, + weight=336, group='Motions', subgroup='Amendments') @@ -233,8 +254,9 @@ def get_config_variables(): # TODO: Add server side validation of the choices. yield ConfigVariable( name='motions_poll_default_majority_method', - default_value='simple_majority', - input_type='majorityMethod', + default_value=majorityMethods[0]['value'], + input_type='choice', + choices=majorityMethods, label='Required majority', help_text='Default method to check whether a motion has reached the required majority.', weight=357, diff --git a/openslides/poll/majority.py b/openslides/poll/majority.py new file mode 100644 index 000000000..34643390d --- /dev/null +++ b/openslides/poll/majority.py @@ -0,0 +1,7 @@ +# Common majority methods for all apps using polls. The first one should be the default. +majorityMethods = ( + {'value': 'simple_majority', 'display_name': 'Simple majority'}, + {'value': 'two-thirds_majority', 'display_name': 'Two-thirds majority'}, + {'value': 'three-quarters_majority', 'display_name': 'Three-quarters majority'}, + {'value': 'disabled', 'display_name': 'Disabled'}, +)