Merge pull request #3957 from CatoTH/StatuteParagraphAmendments

Creating / Editing / Showing statute paragraph amendments
This commit is contained in:
Sean 2018-11-20 16:54:16 +01:00 committed by GitHub
commit 01c593e9be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 140 additions and 8 deletions

View File

@ -115,6 +115,7 @@ _('Default text version for change recommendations');
// subgroup Amendments // subgroup Amendments
_('Amendments'); _('Amendments');
_('Activate amendments'); _('Activate amendments');
_('Activate statutes');
_('Show amendments together with motions'); _('Show amendments together with motions');
_('Prefix for the identifier for amendments'); _('Prefix for the identifier for amendments');
_('The title of the motion is always applied.'); _('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_id: number;
public state_extension: string; public state_extension: string;
public state_required_permission_to_see: string; public state_required_permission_to_see: string;
public statute_paragraph_id: number;
public recommendation_id: number; public recommendation_id: number;
public recommendation_extension: string; public recommendation_extension: string;
public tags_id: number[]; public tags_id: number[];

View File

@ -259,7 +259,7 @@
<form class="motion-content" [formGroup]='contentForm' (ngSubmit)='saveMotion()'> <form class="motion-content" [formGroup]='contentForm' (ngSubmit)='saveMotion()'>
<!-- Line Number and Diff buttons--> <!-- 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 }}"> <button type="button" mat-icon-button [matMenuTriggerFor]="lineNumberingMenu" matTooltip="{{ 'Line numbering' | translate }}">
<mat-icon>format_list_numbered</mat-icon> <mat-icon>format_list_numbered</mat-icon>
</button> </button>
@ -268,11 +268,29 @@
</button> </button>
</div> </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 --> <!-- Title -->
<div *ngIf="motion && motion.title || editMotion"> <div *ngIf="motion && motion.title || editMotion">
<div *ngIf='!editMotion'> <div *ngIf='!editMotion'>
<h4>{{motion.title}}</h4> <h4>{{motion.title}}</h4>
</div> </div>
<mat-form-field *ngIf="editMotion" class="wide-form"> <mat-form-field *ngIf="editMotion" class="wide-form">
<input matInput osAutofocus placeholder="{{ 'Title' | translate }}" formControlName='title' [value]='motionCopy.title' <input matInput osAutofocus placeholder="{{ 'Title' | translate }}" formControlName='title' [value]='motionCopy.title'
required> required>
@ -283,7 +301,7 @@
<!-- Text --> <!-- Text -->
<!-- TODO: this is a config variable. Read it out --> <!-- TODO: this is a config variable. Read it out -->
<span class="text-prefix-label" translate>The assembly may decide:</span> <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()" <div *ngIf="!isRecoModeDiff()" class="motion-text" [class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-outside]="isLineNumberingOutside()"> [class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-outside]="isLineNumberingOutside()">
<os-motion-detail-original-change-recommendations *ngIf="isLineNumberingOutside() && isRecoModeOriginal()" <os-motion-detail-original-change-recommendations *ngIf="isLineNumberingOutside() && isRecoModeOriginal()"
@ -294,6 +312,10 @@
<os-motion-detail-diff *ngIf="isRecoModeDiff()" [motion]="motion" [changes]="allChangingObjects" <os-motion-detail-diff *ngIf="isRecoModeDiff()" [motion]="motion" [changes]="allChangingObjects"
[scrollToChange]="scrollToChange" (createChangeRecommendation)="createChangeRecommendation($event)"></os-motion-detail-diff> [scrollToChange]="scrollToChange" (createChangeRecommendation)="createChangeRecommendation($event)"></os-motion-detail-diff>
</ng-container> </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"> <mat-form-field *ngIf="motion && editMotion" class="wide-form">
<textarea matInput placeholder="{{ 'Motion text' | translate }}" formControlName='text' [value]='motionCopy.text' required></textarea> <textarea matInput placeholder="{{ 'Motion text' | translate }}" formControlName='text' [value]='motionCopy.text' required></textarea>
</mat-form-field> </mat-form-field>

View File

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

View File

@ -1,7 +1,7 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 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 { Category } from '../../../../shared/models/motions/category';
import { ViewportService } from '../../../../core/services/viewport.service'; 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 { ViewUnifiedChange } from '../../models/view-unified-change';
import { OperatorService } from '../../../../core/services/operator.service'; import { OperatorService } from '../../../../core/services/operator.service';
import { BaseViewComponent } from '../../../base/base-view'; 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 * Component for the motion detail view
@ -72,6 +75,12 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
*/ */
public motion: ViewMotion; 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 * Copy of the motion that the user might edit
*/ */
@ -102,6 +111,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
*/ */
public previousMotion: ViewMotion; public previousMotion: ViewMotion;
/**
* statute paragraphs, necessary for amendments
*/
public statuteParagraphs: ViewStatuteParagraph[] = [];
/** /**
* Subject for the Categories * Subject for the Categories
*/ */
@ -134,14 +148,16 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
* @param translate * @param translate
* @param matSnackBar * @param matSnackBar
* @param vp the viewport service * @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 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 route determine if this is a new or an existing motion
* @param formBuilder For reactive forms. Form Group and Form Control * @param formBuilder For reactive forms. Form Group and Form Control
* @param dialogService For opening dialogs * @param dialogService For opening dialogs
* @param repo Motion Repository * @param repo Motion Repository
* @param changeRecoRepo Change Recommendation Repository * @param changeRecoRepo Change Recommendation Repository
* @param statuteRepo: Statute Paragraph Repository
* @param DS The DataStoreService * @param DS The DataStoreService
* @param configService The configuration provider
* @param sanitizer For making HTML SafeHTML * @param sanitizer For making HTML SafeHTML
*/ */
public constructor( public constructor(
@ -156,7 +172,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
private dialogService: MatDialog, private dialogService: MatDialog,
private repo: MotionRepositoryService, private repo: MotionRepositoryService,
private changeRecoRepo: ChangeRecommendationRepositoryService, private changeRecoRepo: ChangeRecommendationRepositoryService,
private statuteRepo: StatuteParagraphRepositoryService,
private DS: DataStoreService, private DS: DataStoreService,
private configService: ConfigService,
private sanitizer: DomSanitizer private sanitizer: DomSanitizer
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
@ -178,6 +196,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
this.categoryObserver.next(DS.getAll(Category)); 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); this.metaInfoForm.patchValue(metaInfoPatch);
const contentPatch = {}; const contentPatch: {[key: string]: any} = {};
Object.keys(this.contentForm.controls).forEach(ctrl => { Object.keys(this.contentForm.controls).forEach(ctrl => {
contentPatch[ctrl] = formMotion[ctrl]; contentPatch[ctrl] = formMotion[ctrl];
}); });
const statuteAmendmentFieldName = 'statute_amendment';
contentPatch[statuteAmendmentFieldName] = formMotion.isStatuteAmendment();
this.contentForm.patchValue(contentPatch); this.contentForm.patchValue(contentPatch);
} }
@ -262,7 +285,9 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
this.contentForm = this.formBuilder.group({ this.contentForm = this.formBuilder.group({
title: ['', Validators.required], title: ['', Validators.required],
text: ['', 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 { public getFormattedText(): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(this.getFormattedTextPlain()); 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. * Trigger to delete the motion.
* Sends a delete request over the repository and * 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 * Navigates the user to the given ViewMotion
* @param motion target * @param motion target
@ -511,5 +568,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
this.setSurroundingMotions(); this.setSurroundingMotions();
} }
}); });
this.statuteRepo.getViewModelListObservable().subscribe(newViewStatuteParagraphs => {
this.statuteParagraphs = newViewStatuteParagraphs;
});
} }
} }

View File

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

View File

@ -9,6 +9,7 @@ import { ViewMotion } from '../../models/view-motion';
import { WorkflowState } from '../../../../shared/models/motions/workflow-state'; import { WorkflowState } from '../../../../shared/models/motions/workflow-state';
import { ListViewBaseComponent } from '../../../base/list-view-base'; import { ListViewBaseComponent } from '../../../base/list-view-base';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { ConfigService } from "../../../../core/services/config.service";
/** /**
* Component that displays all the motions in a Table using DataSource. * 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']; 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. * Constructor implements title and translation Module.
* *
* @param titleService Title * @param titleService Title
* @param translate Translation * @param translate Translation
* @param matSnackBar
* @param router Router * @param router Router
* @param route Current route * @param route Current route
* @param configService The configuration provider
* @param repo Motion Repository * @param repo Motion Repository
*/ */
public constructor( public constructor(
@ -46,6 +55,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private configService: ConfigService,
private repo: MotionRepositoryService private repo: MotionRepositoryService
) { ) {
super(titleService, translate, matSnackBar); 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; 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 { public get recommendation(): WorkflowState {
return this.recommendation_id && this.workflow ? this.workflow.getStateById(this.recommendation_id) : null; 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); return !!(this.supporters && this.supporters.length > 0);
} }
public isStatuteAmendment(): boolean {
return !!this.statute_paragraph_id;
}
/** /**
* Duplicate this motion into a copy of itself * 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 { ViewChangeReco } from '../models/view-change-reco';
import { MotionChangeReco } from '../../../shared/models/motions/motion-change-reco'; import { MotionChangeReco } from '../../../shared/models/motions/motion-change-reco';
import { ViewUnifiedChange } from '../models/view-unified-change'; import { ViewUnifiedChange } from '../models/view-unified-change';
import { ViewStatuteParagraph } from '../models/view-statute-paragraph';
import { Identifiable } from '../../../shared/models/base/identifiable'; import { Identifiable } from '../../../shared/models/base/identifiable';
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service'; import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
import { HttpService } from 'app/core/services/http.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 * 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', group='Motions',
subgroup='General') 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 # Amendments
yield ConfigVariable( yield ConfigVariable(
name='motions_amendments_enabled', name='motions_amendments_enabled',

View File

@ -406,6 +406,7 @@ class MotionSerializer(ModelSerializer):
'state', 'state',
'state_extension', 'state_extension',
'state_required_permission_to_see', 'state_required_permission_to_see',
'statute_paragraph',
'workflow_id', 'workflow_id',
'recommendation', 'recommendation',
'recommendation_extension', 'recommendation_extension',
@ -461,6 +462,7 @@ class MotionSerializer(ModelSerializer):
motion.motion_block = validated_data.get('motion_block') motion.motion_block = validated_data.get('motion_block')
motion.origin = validated_data.get('origin', '') motion.origin = validated_data.get('origin', '')
motion.parent = validated_data.get('parent') motion.parent = validated_data.get('parent')
motion.statute_paragraph = validated_data.get('statute_paragraph')
motion.reset_state(validated_data.get('workflow_id')) motion.reset_state(validated_data.get('workflow_id'))
motion.agenda_item_update_information['type'] = validated_data.get('agenda_type') motion.agenda_item_update_information['type'] = validated_data.get('agenda_type')
motion.agenda_item_update_information['parent_id'] = validated_data.get('agenda_parent_id') motion.agenda_item_update_information['parent_id'] = validated_data.get('agenda_parent_id')