diff --git a/client/src/app/core/marked-translations.ts b/client/src/app/core/marked-translations.ts index 916641694..bc772cb2d 100644 --- a/client/src/app/core/marked-translations.ts +++ b/client/src/app/core/marked-translations.ts @@ -115,6 +115,7 @@ _('Default text version for change recommendations'); // subgroup Amendments _('Amendments'); _('Activate amendments'); +_('Activate statutes'); _('Show amendments together with motions'); _('Prefix for the identifier for amendments'); _('The title of the motion is always applied.'); diff --git a/client/src/app/shared/models/motions/motion.ts b/client/src/app/shared/models/motions/motion.ts index 82a891045..f44e4523d 100644 --- a/client/src/app/shared/models/motions/motion.ts +++ b/client/src/app/shared/models/motions/motion.ts @@ -30,6 +30,7 @@ export class Motion extends AgendaBaseModel { public state_id: number; public state_extension: string; public state_required_permission_to_see: string; + public statute_paragraph_id: number; public recommendation_id: number; public recommendation_extension: string; public tags_id: number[]; 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 66d63cbeb..3be254624 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 @@ -259,7 +259,7 @@
-
+
@@ -268,11 +268,29 @@
+ +
+ + Statute amendment + + + + + + {{ paragraph.title }} + + + +
+

{{motion.title}}

+ @@ -283,7 +301,7 @@ The assembly may decide: - +
+
+
+ diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss b/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss index 7e4ac8114..e5a7e6ce3 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss @@ -113,6 +113,12 @@ span { } } +.statute-amendment-selector { + mat-form-field { + margin-left: 20px; + } +} + .motion-content { h4 { margin: 10px 10px 15px 0; 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 e2c96d5d3..6b26ebfcb 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 @@ -1,7 +1,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { MatDialog, MatExpansionPanel, MatSnackBar, MatSelectChange } from '@angular/material'; +import { MatDialog, MatExpansionPanel, MatSnackBar, MatSelectChange, MatCheckboxChange } from '@angular/material'; import { Category } from '../../../../shared/models/motions/category'; import { ViewportService } from '../../../../core/services/viewport.service'; @@ -23,6 +23,9 @@ import { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser'; import { ViewUnifiedChange } from '../../models/view-unified-change'; import { OperatorService } from '../../../../core/services/operator.service'; 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"; /** * Component for the motion detail view @@ -72,6 +75,12 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { */ public motion: ViewMotion; + /** + * Value of the configuration variable `motions_statutes_enabled` - are statutes enabled? + * @TODO replace by direct access to config variable, once it's available from the templates + */ + public statutesEnabled: boolean; + /** * Copy of the motion that the user might edit */ @@ -102,6 +111,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { */ public previousMotion: ViewMotion; + /** + * statute paragraphs, necessary for amendments + */ + public statuteParagraphs: ViewStatuteParagraph[] = []; + /** * Subject for the Categories */ @@ -134,14 +148,16 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { * @param translate * @param matSnackBar * @param vp the viewport service - * @param op + * @param op Operator Service * @param router to navigate back to the motion list and to an existing motion * @param route determine if this is a new or an existing motion * @param formBuilder For reactive forms. Form Group and Form Control * @param dialogService For opening dialogs * @param repo Motion Repository * @param changeRecoRepo Change Recommendation Repository + * @param statuteRepo: Statute Paragraph Repository * @param DS The DataStoreService + * @param configService The configuration provider * @param sanitizer For making HTML SafeHTML */ public constructor( @@ -156,7 +172,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { private dialogService: MatDialog, private repo: MotionRepositoryService, private changeRecoRepo: ChangeRecommendationRepositoryService, + private statuteRepo: StatuteParagraphRepositoryService, private DS: DataStoreService, + private configService: ConfigService, private sanitizer: DomSanitizer ) { super(title, translate, matSnackBar); @@ -178,6 +196,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { this.categoryObserver.next(DS.getAll(Category)); } }); + this.configService.get('motions_statutes_enabled').subscribe((enabled: boolean): void => { + this.statutesEnabled = enabled; + }); } /** @@ -237,10 +258,12 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { }); this.metaInfoForm.patchValue(metaInfoPatch); - const contentPatch = {}; + const contentPatch: {[key: string]: any} = {}; Object.keys(this.contentForm.controls).forEach(ctrl => { contentPatch[ctrl] = formMotion[ctrl]; }); + const statuteAmendmentFieldName = 'statute_amendment'; + contentPatch[statuteAmendmentFieldName] = formMotion.isStatuteAmendment(); this.contentForm.patchValue(contentPatch); } @@ -262,7 +285,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { this.contentForm = this.formBuilder.group({ title: ['', Validators.required], text: ['', Validators.required], - reason: [''] + reason: [''], + statute_amendment: [''], // Internal value for the checkbox, not saved to the model + statute_paragraph_id: [''] }); } @@ -311,12 +336,22 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { } /** - * get the formated motion text from the repository, as SafeHTML for [innerHTML] + * get the formatted motion text from the repository, as SafeHTML for [innerHTML] + * @returns {SafeHtml} */ public getFormattedText(): SafeHtml { return this.sanitizer.bypassSecurityTrustHtml(this.getFormattedTextPlain()); } + /** + * get the diff html from the statute amendment, as SafeHTML for [innerHTML] + * @returns {SafeHtml} + */ + public getFormattedStatuteAmendment(): SafeHtml { + const diffHtml = this.repo.formatStatuteAmendment(this.statuteParagraphs, this.motion, this.motion.lineLength); + return this.sanitizer.bypassSecurityTrustHtml(diffHtml); + } + /** * Trigger to delete the motion. * Sends a delete request over the repository and @@ -426,6 +461,28 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { } } + /** + * If the checkbox is deactivated, the statute_paragraph_id-field needs to be reset, as only that field is saved + * @param {MatCheckboxChange} $event + */ + public onStatuteAmendmentChange($event: MatCheckboxChange): void { + this.contentForm.patchValue({ + statute_paragraph_id: null + }); + } + + /** + * The paragraph of the statute to amend was changed -> change the input fields below + * @param {number} newValue + */ + public onStatuteParagraphChange(newValue: number): void { + const selectedParagraph = this.statuteParagraphs.find(par => par.id === newValue); + this.contentForm.patchValue({ + title: this.translate.instant('Statute amendment for') + ` ${selectedParagraph.title}`, + text: selectedParagraph.text + }); + } + /** * Navigates the user to the given ViewMotion * @param motion target @@ -511,5 +568,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { this.setSurroundingMotions(); } }); + this.statuteRepo.getViewModelListObservable().subscribe(newViewStatuteParagraphs => { + this.statuteParagraphs = newViewStatuteParagraphs; + }); } } diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.html b/client/src/app/site/motions/components/motion-list/motion-list.component.html index e2adaabe2..ee9632059 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.html +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.html @@ -96,7 +96,7 @@ Comment sections - diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.ts b/client/src/app/site/motions/components/motion-list/motion-list.component.ts index 1e260b8bc..128af41c4 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.ts +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.ts @@ -9,6 +9,7 @@ import { ViewMotion } from '../../models/view-motion'; import { WorkflowState } from '../../../../shared/models/motions/workflow-state'; import { ListViewBaseComponent } from '../../../base/list-view-base'; import { MatSnackBar } from '@angular/material'; +import { ConfigService } from "../../../../core/services/config.service"; /** * Component that displays all the motions in a Table using DataSource. @@ -31,13 +32,21 @@ export class MotionListComponent extends ListViewBaseComponent imple */ public columnsToDisplayFullWidth = ['identifier', 'title', 'state', 'speakers']; + /** + * Value of the configuration variable `motions_statutes_enabled` - are statutes enabled? + * @TODO replace by direct access to config variable, once it's available from the templates + */ + public statutesEnabled: boolean; + /** * Constructor implements title and translation Module. * * @param titleService Title * @param translate Translation + * @param matSnackBar * @param router Router * @param route Current route + * @param configService The configuration provider * @param repo Motion Repository */ public constructor( @@ -46,6 +55,7 @@ export class MotionListComponent extends ListViewBaseComponent imple matSnackBar: MatSnackBar, private router: Router, private route: ActivatedRoute, + private configService: ConfigService, private repo: MotionRepositoryService ) { super(titleService, translate, matSnackBar); @@ -69,6 +79,9 @@ export class MotionListComponent extends ListViewBaseComponent imple } }); }); + this.configService.get('motions_statutes_enabled').subscribe((enabled: boolean): void => { + this.statutesEnabled = enabled; + }); } /** diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index 05f07f036..320f429ba 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -138,6 +138,10 @@ export class ViewMotion extends BaseViewModel { return this.motion && this.motion.recommendation_id ? this.motion.recommendation_id : null; } + public get statute_paragraph_id(): number { + return this.motion && this.motion.statute_paragraph_id ? this.motion.statute_paragraph_id : null; + } + public get recommendation(): WorkflowState { return this.recommendation_id && this.workflow ? this.workflow.getStateById(this.recommendation_id) : null; } @@ -269,6 +273,10 @@ export class ViewMotion extends BaseViewModel { return !!(this.supporters && this.supporters.length > 0); } + public isStatuteAmendment(): boolean { + return !!this.statute_paragraph_id; + } + /** * Duplicate this motion into a copy of itself */ 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 afc2fb7b6..7e93c314b 100644 --- a/client/src/app/site/motions/services/motion-repository.service.ts +++ b/client/src/app/site/motions/services/motion-repository.service.ts @@ -14,6 +14,7 @@ import { DiffService, LineRange, ModificationType } from './diff.service'; import { ViewChangeReco } from '../models/view-change-reco'; import { MotionChangeReco } from '../../../shared/models/motions/motion-change-reco'; import { ViewUnifiedChange } from '../models/view-unified-change'; +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'; @@ -225,6 +226,13 @@ export class MotionRepositoryService extends BaseRepository } } + public formatStatuteAmendment(paragraphs: ViewStatuteParagraph[], amendment: ViewMotion, lineLength: number): string { + const origParagraph = paragraphs.find(paragraph => paragraph.id === amendment.statute_paragraph_id); + let diffHtml = this.diff.diff(origParagraph.text, amendment.text); + diffHtml = this.lineNumbering.insertLineBreaksWithoutNumbers(diffHtml, lineLength, true); + return diffHtml; + } + /** * Extracts a renderable HTML string representing the given line number range of this motion * diff --git a/openslides/motions/config_variables.py b/openslides/motions/config_variables.py index 6f4e2a89c..df532a077 100644 --- a/openslides/motions/config_variables.py +++ b/openslides/motions/config_variables.py @@ -138,6 +138,17 @@ def get_config_variables(): group='Motions', subgroup='General') + # Statutes + + yield ConfigVariable( + name='motions_statutes_enabled', + default_value=False, + input_type='boolean', + label='Activate statutes', + weight=334, + group='Motions', + subgroup='General') + # Amendments yield ConfigVariable( name='motions_amendments_enabled', diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index c21222ba0..3d1d8f50b 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -406,6 +406,7 @@ class MotionSerializer(ModelSerializer): 'state', 'state_extension', 'state_required_permission_to_see', + 'statute_paragraph', 'workflow_id', 'recommendation', 'recommendation_extension', @@ -461,6 +462,7 @@ class MotionSerializer(ModelSerializer): motion.motion_block = validated_data.get('motion_block') motion.origin = validated_data.get('origin', '') motion.parent = validated_data.get('parent') + motion.statute_paragraph = validated_data.get('statute_paragraph') motion.reset_state(validated_data.get('workflow_id')) motion.agenda_item_update_information['type'] = validated_data.get('agenda_type') motion.agenda_item_update_information['parent_id'] = validated_data.get('agenda_parent_id')