Merge pull request #4032 from FinnStutzenstein/new_configs

new configs for statute amendments; improved the majorityMethod confi…
This commit is contained in:
Sean 2018-11-23 11:15:07 +01:00 committed by GitHub
commit 10714bbbe4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 156 additions and 59 deletions

View File

@ -67,10 +67,10 @@ export class ConfigService extends OpenSlidesComponent {
*
* @param key The config value to get from.
*/
public get(key: string): Observable<any> {
public get<T = any>(key: string): Observable<T> {
if (!this.configSubjects[key]) {
this.configSubjects[key] = new BehaviorSubject<any>(this.instant(key));
}
return this.configSubjects[key].asObservable();
return this.configSubjects[key].asObservable() as Observable<T>;
}
}

View File

@ -1,12 +1,14 @@
<mat-form-field [formGroup]="form">
<mat-select [formControl]="formControl" [placeholder]="listname" [multiple]="multiple" #thisSelector>
<mat-select [formControl]="formControl" placeholder="{{ listname | translate }}" [multiple]="multiple" #thisSelector>
<ngx-mat-select-search [formControl]="filterControl"></ngx-mat-select-search>
<div *ngIf="!multiple">
<mat-option [value]="null"><span translate>None</span></mat-option>
<div *ngIf="!multiple && includeNone">
<mat-option [value]="null">
<span translate>None</span>
</mat-option>
<mat-divider></mat-divider>
</div>
<mat-option *ngFor="let selectedItem of filteredItems | async" [value]="selectedItem.id">
{{selectedItem.getTitle(translate)}}
{{ selectedItem.getTitle(translate) }}
</mat-option>
</mat-select>
</mat-form-field>
@ -15,7 +17,8 @@
<span translate>Selected values</span>:
</p>
<mat-chip-list #chipList>
<mat-chip *ngFor="let selectedItem of thisSelector?.value" (removed)="remove(selectedItem)">{{selectedItem.name}}
<mat-chip *ngFor="let selectedItem of thisSelector?.value" (removed)="remove(selectedItem)">
{{ selectedItem.name }}
<mat-icon (click)="remove(selectedItem)">cancel</mat-icon>
</mat-chip>
</mat-chip-list>

View File

@ -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">
* </os-search-value-selector>
* ```
@ -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.

View File

@ -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<void> {
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);
}
/**

View File

@ -15,7 +15,6 @@ type ConfigInputType =
| 'boolean'
| 'markupText'
| 'integer'
| 'majorityMethod'
| 'choice'
| 'datetimepicker'
| 'colorpicker'

View File

@ -220,8 +220,7 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config>
* @param config
*/
public createViewModel(config: Config): ViewConfig {
const vm = new ViewConfig(config);
return vm;
return new ViewConfig(config);
}
/**

View File

@ -79,8 +79,7 @@
<mat-accordion multi='true' class='on-transition-fade'>
<!-- MetaInfo Panel-->
<mat-expansion-panel #metaInfoPanel [expanded]="this.editMotion" class='meta-info-block meta-info-panel'>
<!-- <mat-expansion-panel #metaInfoPanel [expanded]="this.editReco && this.newReco" class='meta-info-block meta-info-panel'> -->
<mat-expansion-panel #metaInfoPanel [expanded]="editMotion" class='meta-info-block meta-info-panel'>
<mat-expansion-panel-header>
<mat-panel-title>
<mat-icon>info</mat-icon>
@ -155,8 +154,8 @@
<div *ngIf="motion && motion.submitters || newMotion">
<div *ngIf="newMotion">
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="this.metaInfoForm.get('submitters_id')"
[multiple]="true" listname="{{ 'Submitters' | translate }}" [InputListValues]="this.submitterObserver"></os-search-value-selector>
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('submitters_id')"
[multiple]="true" listname="{{ 'Submitters' | translate }}" [InputListValues]="submitterObserver"></os-search-value-selector>
</div>
</div>
<div *ngIf="!editMotion && !newMotion">
@ -172,8 +171,8 @@
<!-- print all motion supporters -->
<div *ngIf="editMotion">
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="this.metaInfoForm.get('supporters_id')"
[multiple]="true" listname="{{ 'Supporters' | translate }}" [InputListValues]="this.supporterObserver"></os-search-value-selector>
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('supporters_id')"
[multiple]="true" listname="{{ 'Supporters' | translate }}" [InputListValues]="supporterObserver"></os-search-value-selector>
</div>
</div>
<div *ngIf="!editMotion && motion.hasSupporters()">
@ -230,8 +229,16 @@
{{ motion.category }}
</div>
<div *ngIf="editMotion || newMotion">
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="this.metaInfoForm.get('category_id')"
[multiple]="false" listname="{{ 'Category' | translate }}" [InputListValues]="this.categoryObserver"></os-search-value-selector>
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('category_id')"
[multiple]="false" listname="{{ 'Category' | translate }}" [InputListValues]="categoryObserver" includeNone="true"></os-search-value-selector>
</div>
</div>
<!-- Workflow (just during creation) -->
<div *ngIf="editMotion">
<div *osPerms="['motions.can_manage', 'motions.can_manage_metadata']">
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="metaInfoForm.get('workflow_id')"
[multiple]="false" listname="{{ 'Workflow' | translate }}" [InputListValues]="workflowObserver"></os-search-value-selector>
</div>
</div>

View File

@ -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<Category[]>;
/**
* Subject for the Categories
*/
public workflowObserver: BehaviorSubject<Workflow[]>;
/**
* 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<string>(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;

View File

@ -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;
}

View File

@ -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<ViewMotion, Motion>
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<ViewMotion, Motion>
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<string> {
return this.configService.get('motions_recommendations_by');
}
/**
* Sends the changed nodes to the server.
*

View File

@ -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,

View File

@ -29,7 +29,6 @@ INPUT_TYPE_MAPPING = {
'choice': str,
'colorpicker': str,
'datetimepicker': int,
'majorityMethod': str,
'static': dict,
'translations': list,
}

View File

@ -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,

View File

@ -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'},
)