Creating / Editing statute paragraph amendments

This commit is contained in:
Tobias Hößl 2018-10-28 11:06:36 +01:00 committed by Emanuel Schütze
parent 734c7c872b
commit d3fc006ddc
11 changed files with 140 additions and 8 deletions

View File

@ -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.');

View File

@ -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[];

View File

@ -259,7 +259,7 @@
<form class="motion-content" [formGroup]='contentForm' (ngSubmit)='saveMotion()'>
<!-- Line Number and Diff buttons-->
<div *ngIf="!editMotion" class="motion-text-controls">
<div *ngIf="motion && !editMotion && !motion.isStatuteAmendment()" class="motion-text-controls">
<button type="button" mat-icon-button [matMenuTriggerFor]="lineNumberingMenu" matTooltip="{{ 'Line numbering' | translate }}">
<mat-icon>format_list_numbered</mat-icon>
</button>
@ -268,11 +268,29 @@
</button>
</div>
<!-- Selecting statute paragraphs for amendment -->
<div class="statute-amendment-selector" *ngIf="editMotion && statuteParagraphs.length > 0 && statutesEnabled">
<mat-checkbox formControlName='statute_amendment' translate (change)="onStatuteAmendmentChange($event)">
Statute amendment
</mat-checkbox>
<mat-form-field *ngIf="contentForm.value.statute_amendment">
<mat-select [placeholder]="'Select paragraph to amend' | translate"
formControlName='statute_paragraph_id'
(valueChange)="onStatuteParagraphChange($event)">
<mat-option *ngFor="let paragraph of statuteParagraphs" [value]="paragraph.id">
{{ paragraph.title }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- Title -->
<div *ngIf="motion && motion.title || editMotion">
<div *ngIf='!editMotion'>
<h4>{{motion.title}}</h4>
</div>
<mat-form-field *ngIf="editMotion" class="wide-form">
<input matInput osAutofocus placeholder="{{ 'Title' | translate }}" formControlName='title' [value]='motionCopy.title'
required>
@ -283,7 +301,7 @@
<!-- Text -->
<!-- TODO: this is a config variable. Read it out -->
<span class="text-prefix-label" translate>The assembly may decide:</span>
<ng-container *ngIf='motion && !editMotion'>
<ng-container *ngIf='motion && !editMotion && !motion.isStatuteAmendment()'>
<div *ngIf="!isRecoModeDiff()" class="motion-text" [class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-outside]="isLineNumberingOutside()">
<os-motion-detail-original-change-recommendations *ngIf="isLineNumberingOutside() && isRecoModeOriginal()"
@ -294,6 +312,10 @@
<os-motion-detail-diff *ngIf="isRecoModeDiff()" [motion]="motion" [changes]="allChangingObjects"
[scrollToChange]="scrollToChange" (createChangeRecommendation)="createChangeRecommendation($event)"></os-motion-detail-diff>
</ng-container>
<div class="motion-text line-numbers-none" *ngIf="motion && !editMotion && motion.isStatuteAmendment()"
[innerHTML]="getFormattedStatuteAmendment()">
</div>
<mat-form-field *ngIf="motion && editMotion" class="wide-form">
<textarea matInput placeholder="{{ 'Motion text' | translate }}" formControlName='text' [value]='motionCopy.text' required></textarea>
</mat-form-field>

View File

@ -113,6 +113,12 @@ span {
}
}
.statute-amendment-selector {
mat-form-field {
margin-left: 20px;
}
}
.motion-content {
h4 {
margin: 10px 10px 15px 0;

View File

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

View File

@ -96,7 +96,7 @@
<span translate>Comment sections</span>
</button>
<button mat-menu-item routerLink="statute-paragraphs">
<button mat-menu-item routerLink="statute-paragraphs" *ngIf="statutesEnabled">
<mat-icon>account_balance</mat-icon>
<span translate>Statute paragraphs</span>
</button>

View File

@ -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<ViewMotion> 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<ViewMotion> 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<ViewMotion> imple
}
});
});
this.configService.get('motions_statutes_enabled').subscribe((enabled: boolean): void => {
this.statutesEnabled = enabled;
});
}
/**

View File

@ -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
*/

View File

@ -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<ViewMotion, Motion>
}
}
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
*

View File

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

View File

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