Add amendments to Motion-PDF-Summary-Box

Adds amendments to motion pdf summary box - only if the state of them accepts the merge into the parent motion.
Adds a new flatMap function to array.prototype (should be safe to use until
Array.flatMap made it into official JS. I expect it in ES 2019.

Refactors some PDF and ChangeReco / Amendment related code
This commit is contained in:
Sean Engelhardt 2019-05-09 12:52:10 +02:00 committed by GabrielMeyer
parent a3b5f083d5
commit bbe966efa9
6 changed files with 167 additions and 100 deletions

View File

@ -18,6 +18,16 @@ import { SpinnerService } from './core/ui-services/spinner.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ViewUser } from './site/users/models/view-user'; import { ViewUser } from './site/users/models/view-user';
/**
* Enhance array with own functions
* TODO: Remove once flatMap made its way into official JS/TS (ES 2019?)
*/
declare global {
interface Array<T> {
flatMap(o: any): Array<any>;
}
}
/** /**
* Angular's global App Component * Angular's global App Component
*/ */
@ -82,6 +92,7 @@ export class AppComponent {
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en'); translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
// change default JS functions // change default JS functions
this.overloadArrayToString(); this.overloadArrayToString();
this.overloadFlatMap();
// Show the spinner initial // Show the spinner initial
spinnerService.setVisibility(true, translate.instant('Loading data. Please wait...')); spinnerService.setVisibility(true, translate.instant('Loading data. Please wait...'));
@ -148,6 +159,18 @@ export class AppComponent {
}; };
} }
/**
* Adds an implementation of flatMap.
* TODO: Remove once flatMap made its way into official JS/TS (ES 2019?)
*/
private overloadFlatMap(): void {
const concat = (x: any, y: any) => x.concat(y);
const flatMap = (f: any, xs: any) => xs.map(f).reduce(concat, []);
Array.prototype.flatMap = function(f: any): any[] {
return flatMap(f, this);
};
}
/** /**
* Function to check if the user is existing and the app is already stable. * Function to check if the user is existing and the app is already stable.
* If both conditions true, hide the spinner. * If both conditions true, hide the spinner.

View File

@ -0,0 +1,21 @@
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
import { ModificationType } from 'app/core/ui-services/diff.service';
/**
* Gets the name of the modification type
*
* @param change
* @returns the name of a recommendation type
*/
export function getRecommendationTypeName(change: ViewMotionChangeRecommendation): string {
switch (change.type) {
case ModificationType.TYPE_REPLACEMENT:
return 'Replacement';
case ModificationType.TYPE_INSERTION:
return 'Insertion';
case ModificationType.TYPE_DELETION:
return 'Deletion';
default:
return change.other_description;
}
}

View File

@ -10,6 +10,10 @@ import { MergeAmendment } from 'app/shared/models/motions/workflow-state';
* Amendments <-> ViewMotionAmendedParagraph is potentially a 1:n-relation * Amendments <-> ViewMotionAmendedParagraph is potentially a 1:n-relation
*/ */
export class ViewMotionAmendedParagraph implements ViewUnifiedChange { export class ViewMotionAmendedParagraph implements ViewUnifiedChange {
public get stateName(): string {
return this.amendment.state.name;
}
public constructor( public constructor(
private amendment: ViewMotion, private amendment: ViewMotion,
private paragraphNo: number, private paragraphNo: number,

View File

@ -18,8 +18,8 @@
</span> </span>
<span *ngIf="isChangeRecommendation(change)"> ({{ 'Change recommendation' | translate }})</span> <span *ngIf="isChangeRecommendation(change)"> ({{ 'Change recommendation' | translate }})</span>
<span *ngIf="isAmendment(change)"> ({{ 'Amendment' | translate }} {{ change.getIdentifier() }})</span> <span *ngIf="isAmendment(change)"> ({{ 'Amendment' | translate }} {{ change.getIdentifier() }})</span>
<span class="operation" *ngIf="isChangeRecommendation(change)" <span class="operation" *ngIf="isChangeRecommendation(change)">
> {{ getRecommendationTypeName(change) | translate }} {{ getRecommendationTypeName(change) | translate }}
<!-- <!--
@TODO @TODO
<span ng-if="change.original.getType(motion.getVersion(version).text) == 3"> <span ng-if="change.original.getType(motion.getVersion(version).text) == 3">
@ -29,7 +29,9 @@
</span> </span>
<span class="status"> <span class="status">
<ng-container *ngIf="change.isRejected()"> <span translate>Rejected</span></ng-container> <ng-container *ngIf="change.isRejected()"> <span translate>Rejected</span></ng-container>
<ng-container *ngIf="change.isAccepted() && isAmendment(change)"> {{ change.amendment.state.name | translate }}</ng-container> <ng-container *ngIf="change.isAccepted() && isAmendment(change)">
{{ change.stateName | translate }}</ng-container
>
</span> </span>
</a> </a>
</li> </li>
@ -43,10 +45,11 @@
<!-- The actual diff view --> <!-- The actual diff view -->
<div class="motion-text-with-diffs"> <div class="motion-text-with-diffs">
<div *ngFor="let change of changes; let i = index"> <div *ngFor="let change of changes; let i = index">
<div class="motion-text" <div
[class.line-numbers-none]="isLineNumberingNone()" class="motion-text"
[class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-outside]="isLineNumberingOutside()" [class.line-numbers-inline]="isLineNumberingInline()"
[class.line-numbers-outside]="isLineNumberingOutside()"
> >
<os-motion-detail-original-change-recommendations <os-motion-detail-original-change-recommendations
[html]="getTextBetweenChanges(changes[i - 1], change)" [html]="getTextBetweenChanges(changes[i - 1], change)"
@ -97,10 +100,11 @@
</div> </div>
</div> </div>
<div class="motion-text" <div
[class.line-numbers-none]="isLineNumberingNone()" class="motion-text"
[class.line-numbers-inline]="isLineNumberingInline()" [class.line-numbers-none]="isLineNumberingNone()"
[class.line-numbers-outside]="isLineNumberingOutside()" [class.line-numbers-inline]="isLineNumberingInline()"
[class.line-numbers-outside]="isLineNumberingOutside()"
> >
<os-motion-detail-original-change-recommendations <os-motion-detail-original-change-recommendations
[html]="getTextRemainderAfterLastChange()" [html]="getTextRemainderAfterLastChange()"

View File

@ -7,7 +7,7 @@ import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseViewComponent } from 'app/site/base/base-view';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service'; import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { DiffService, LineRange, ModificationType } from 'app/core/ui-services/diff.service'; import { DiffService, LineRange } from 'app/core/ui-services/diff.service';
import { import {
MotionChangeRecommendationComponent, MotionChangeRecommendationComponent,
MotionChangeRecommendationComponentData MotionChangeRecommendationComponentData
@ -17,6 +17,7 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewMotion, LineNumberingMode } from 'app/site/motions/models/view-motion'; import { ViewMotion, LineNumberingMode } from 'app/site/motions/models/view-motion';
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change'; import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation'; import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
import { getRecommendationTypeName } from 'app/shared/utils/recommendation-type-names';
/** /**
* This component displays the original motion text with the change blocks inside. * This component displays the original motion text with the change blocks inside.
@ -45,6 +46,11 @@ import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-mot
styleUrls: ['./motion-detail-diff.component.scss'] styleUrls: ['./motion-detail-diff.component.scss']
}) })
export class MotionDetailDiffComponent extends BaseViewComponent implements AfterViewInit { export class MotionDetailDiffComponent extends BaseViewComponent implements AfterViewInit {
/**
* Get the {@link getRecommendationTypeName}-Function from Utils
*/
public getRecommendationTypeName = getRecommendationTypeName;
@Input() @Input()
public motion: ViewMotion; public motion: ViewMotion;
@Input() @Input()
@ -222,24 +228,6 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
return change.getChangeType() === ViewUnifiedChangeType.TYPE_CHANGE_RECOMMENDATION; return change.getChangeType() === ViewUnifiedChangeType.TYPE_CHANGE_RECOMMENDATION;
} }
/**
* Gets the name of the modification type
*
* @param change
*/
public getRecommendationTypeName(change: ViewMotionChangeRecommendation): string {
switch (change.type) {
case ModificationType.TYPE_REPLACEMENT:
return 'Replacement';
case ModificationType.TYPE_INSERTION:
return 'Insertion';
case ModificationType.TYPE_DELETION:
return 'Deletion';
default:
return '@UNKNOWN@';
}
}
/** /**
* Sets a change recommendation to accepted or rejected. * Sets a change recommendation to accepted or rejected.
* The template has to make sure only to pass change recommendations to this method. * The template has to make sure only to pass change recommendations to this method.

View File

@ -5,6 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CalculablePollKey } from 'app/core/ui-services/poll.service'; import { CalculablePollKey } from 'app/core/ui-services/poll.service';
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service'; import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { getRecommendationTypeName } from 'app/shared/utils/recommendation-type-names';
import { HtmlToPdfService } from 'app/core/ui-services/html-to-pdf.service'; import { HtmlToPdfService } from 'app/core/ui-services/html-to-pdf.service';
import { MotionPollService } from './motion-poll.service'; import { MotionPollService } from './motion-poll.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service'; import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
@ -12,8 +13,10 @@ import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions
import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-motion'; import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-motion';
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service'; import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service'; import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service';
import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change';
import { PdfDocumentService } from 'app/core/ui-services/pdf-document.service'; import { PdfDocumentService } from 'app/core/ui-services/pdf-document.service';
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
import { ViewMotionChangeRecommendation } from '../models/view-motion-change-recommendation';
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
/** /**
* Type declaring which strings are valid options for metainfos to be exported into a pdf * Type declaring which strings are valid options for metainfos to be exported into a pdf
@ -45,6 +48,11 @@ export type InfoToExport =
providedIn: 'root' providedIn: 'root'
}) })
export class MotionPdfService { export class MotionPdfService {
/**
* Get the {@link getRecommendationTypeName}-Function from Utils
*/
public getRecommendationTypeName = getRecommendationTypeName;
/** /**
* Constructor * Constructor
* *
@ -91,6 +99,8 @@ export class MotionPdfService {
infoToExport?: InfoToExport[], infoToExport?: InfoToExport[],
commentsToExport?: number[] commentsToExport?: number[]
): object { ): object {
// get the line length from the config
const lineLength = this.configService.instant<number>('motions_line_length');
let motionPdfContent = []; let motionPdfContent = [];
// Enforces that statutes should always have Diff Mode and no line numbers // Enforces that statutes should always have Diff Mode and no line numbers
@ -116,14 +126,14 @@ export class MotionPdfService {
motionPdfContent = [title, subtitle]; motionPdfContent = [title, subtitle];
if ((infoToExport && infoToExport.length > 0) || !infoToExport) { if ((infoToExport && infoToExport.length > 0) || !infoToExport) {
const metaInfo = this.createMetaInfoTable(motion, crMode, infoToExport); const metaInfo = this.createMetaInfoTable(motion, lineLength, crMode, infoToExport);
motionPdfContent.push(metaInfo); motionPdfContent.push(metaInfo);
} }
if (!contentToExport || contentToExport.includes('text')) { if (!contentToExport || contentToExport.includes('text')) {
const preamble = this.createPreamble(motion); const preamble = this.createPreamble(motion);
motionPdfContent.push(preamble); motionPdfContent.push(preamble);
const text = this.createText(motion, lnMode, crMode); const text = this.createText(motion, lineLength, lnMode, crMode);
motionPdfContent.push(text); motionPdfContent.push(text);
} }
@ -192,7 +202,12 @@ export class MotionPdfService {
* @param motion the target motion * @param motion the target motion
* @returns doc def for the meta infos * @returns doc def for the meta infos
*/ */
private createMetaInfoTable(motion: ViewMotion, crMode: ChangeRecoMode, infoToExport?: InfoToExport[]): object { private createMetaInfoTable(
motion: ViewMotion,
lineLength: number,
crMode: ChangeRecoMode,
infoToExport?: InfoToExport[]
): object {
const metaTableBody = []; const metaTableBody = [];
// submitters // submitters
@ -367,23 +382,13 @@ export class MotionPdfService {
} }
// summary of change recommendations (for motion diff version only) // summary of change recommendations (for motion diff version only)
const changeRecos = this.changeRecoRepo const changes = this.getUnifiedChanges(motion, lineLength);
.getChangeRecoOfMotion(motion.id)
.sort((a: ViewUnifiedChange, b: ViewUnifiedChange) => {
if (a.getLineFrom() < b.getLineFrom()) {
return -1;
} else if (a.getLineFrom() > b.getLineFrom()) {
return 1;
} else {
return 0;
}
});
if (crMode === ChangeRecoMode.Diff && changeRecos.length > 0) { if (crMode === ChangeRecoMode.Diff && changes.length > 0) {
const columnLineNumbers = []; const columnLineNumbers = [];
const columnChangeType = []; const columnChangeType = [];
changeRecos.forEach(changeReco => { changes.forEach(change => {
// TODO: the function isTitleRecommendation() does not exist anymore. // TODO: the function isTitleRecommendation() does not exist anymore.
// Not sure if required or not // Not sure if required or not
// if (changeReco.isTitleRecommendation()) { // if (changeReco.isTitleRecommendation()) {
@ -392,44 +397,56 @@ export class MotionPdfService {
// line numbers column // line numbers column
let line; let line;
if (changeReco.line_from >= changeReco.line_to - 1) { if (change.getLineFrom() >= change.getLineTo() - 1) {
line = changeReco.line_from; line = change.getLineFrom();
} else { } else {
line = changeReco.line_from + ' - ' + (changeReco.line_to - 1); line = change.getLineFrom() + ' - ' + (change.getLineTo() - 1);
} }
columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `);
// change type column // change type column
if (changeReco.type === 0) { if (change.getChangeType() === ViewUnifiedChangeType.TYPE_CHANGE_RECOMMENDATION) {
columnChangeType.push(this.translate.instant('Replacement')); const changeReco = change as ViewMotionChangeRecommendation;
} else if (changeReco.type === 1) { columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `);
columnChangeType.push(this.translate.instant('Insertion')); columnChangeType.push(
} else if (changeReco.type === 2) { `(${this.translate.instant('Change recommendation')}) - ${this.translate.instant(
columnChangeType.push(this.translate.instant('Deletion')); this.getRecommendationTypeName(changeReco)
} else if (changeReco.type === 3) { )}`
columnChangeType.push(changeReco.other_description); );
} else if (change.getChangeType() === ViewUnifiedChangeType.TYPE_AMENDMENT) {
const amendment = change as ViewMotionAmendedParagraph;
let summaryText = `(${this.translate.instant('Amendment')} ${amendment.getIdentifier()}) -`;
if (amendment.isRejected()) {
summaryText += ` ${this.translate.instant('Rejected')}`;
} else if (amendment.isAccepted()) {
summaryText += ` ${this.translate.instant(amendment.stateName)}`;
// only append line and change, if the merge of the state of the amendment is accepted.
columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `);
columnChangeType.push(summaryText);
}
} }
}); });
metaTableBody.push([ if (columnChangeType.length > 0) {
{ metaTableBody.push([
text: this.translate.instant('Summary of changes'), {
style: 'boldText' text: this.translate.instant('Summary of changes'),
}, style: 'boldText'
{ },
columns: [ {
{ columns: [
text: columnLineNumbers.join('\n'), {
width: 'auto' text: columnLineNumbers.join('\n'),
}, width: 'auto'
{ },
text: columnChangeType.join('\n'), {
width: 'auto' text: columnChangeType.join('\n'),
} width: 'auto'
], }
columnGap: 7 ],
} columnGap: 7
]); }
]);
}
} }
if (metaTableBody.length > 0) { if (metaTableBody.length > 0) {
@ -479,10 +496,13 @@ export class MotionPdfService {
* @param crMode determine the used change Recommendation mode * @param crMode determine the used change Recommendation mode
* @returns doc def for the "the assembly may decide" preamble * @returns doc def for the "the assembly may decide" preamble
*/ */
private createText(motion: ViewMotion, lnMode: LineNumberingMode, crMode: ChangeRecoMode): object { private createText(
motion: ViewMotion,
lineLength: number,
lnMode: LineNumberingMode,
crMode: ChangeRecoMode
): object {
let motionText: string; let motionText: string;
// get the line length from the config
const lineLength = this.configService.instant<number>('motions_line_length');
if (motion.isParagraphBasedAmendment()) { if (motion.isParagraphBasedAmendment()) {
motionText = ''; motionText = '';
@ -510,26 +530,8 @@ export class MotionPdfService {
} else { } else {
// lead motion or normal amendments // lead motion or normal amendments
// TODO: Consider tile change recommendation // TODO: Consider tile change recommendation
const changes: ViewUnifiedChange[] = Object.assign(
[],
this.changeRecoRepo.getChangeRecoOfMotion(motion.id)
);
// TODO: Cleanup, everything change reco and amendment based needs a unified structure. const changes = this.getUnifiedChanges(motion, lineLength);
const amendments = this.motionRepo.getAmendmentsInstantly(motion.id);
if (amendments) {
for (const amendment of amendments) {
const changedParagraphs = this.motionRepo.getAmendmentAmendedParagraphs(amendment, lineLength);
for (const change of changedParagraphs) {
changes.push(change as ViewUnifiedChange);
}
}
}
// changes need to be sorted, by "line from".
// otherwise, formatMotion will make unexpected results by messing up the
// order of changes applied to the motion
changes.sort((a, b) => a.getLineFrom() - b.getLineFrom());
motionText = this.motionRepo.formatMotion(motion.id, crMode, changes, lineLength); motionText = this.motionRepo.formatMotion(motion.id, crMode, changes, lineLength);
// reformat motion text to split long HTML elements to easier convert into PDF // reformat motion text to split long HTML elements to easier convert into PDF
motionText = this.linenumberingService.splitInlineElementsAtLineBreaks(motionText); motionText = this.linenumberingService.splitInlineElementsAtLineBreaks(motionText);
@ -538,6 +540,30 @@ export class MotionPdfService {
return this.htmlToPdfService.convertHtml(motionText, lnMode); return this.htmlToPdfService.convertHtml(motionText, lnMode);
} }
/**
* changes need to be sorted, by "line from".
* otherwise, formatMotion will make unexpected results by messing up the
* order of changes applied to the motion
*
* TODO: Cleanup, everything change reco and amendment based needs a unified structure.
*
* @param motion
* @param lineLength
* @returns
*/
private getUnifiedChanges(motion: ViewMotion, lineLength: number): ViewUnifiedChange[] {
return this.changeRecoRepo
.getChangeRecoOfMotion(motion.id)
.concat(
this.motionRepo
.getAmendmentsInstantly(motion.id)
.flatMap((amendment: ViewMotion) =>
this.motionRepo.getAmendmentAmendedParagraphs(amendment, lineLength)
)
)
.sort((a, b) => a.getLineFrom() - b.getLineFrom()) as ViewUnifiedChange[];
}
/** /**
* Creates the motion reason - uses HTML to PDF * Creates the motion reason - uses HTML to PDF
* *
@ -688,6 +714,7 @@ export class MotionPdfService {
const subtitle = this.createSubtitle(motion); const subtitle = this.createSubtitle(motion);
const metaInfo = this.createMetaInfoTable( const metaInfo = this.createMetaInfoTable(
motion, motion,
this.configService.instant<number>('motions_line_length'),
this.configService.instant('motions_recommendation_text_mode'), this.configService.instant('motions_recommendation_text_mode'),
['submitters', 'state', 'category'] ['submitters', 'state', 'category']
); );