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 { 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
*/
@ -82,6 +92,7 @@ export class AppComponent {
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
// change default JS functions
this.overloadArrayToString();
this.overloadFlatMap();
// Show the spinner initial
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.
* 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
*/
export class ViewMotionAmendedParagraph implements ViewUnifiedChange {
public get stateName(): string {
return this.amendment.state.name;
}
public constructor(
private amendment: ViewMotion,
private paragraphNo: number,

View File

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

View File

@ -7,7 +7,7 @@ import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ConfigService } from 'app/core/ui-services/config.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 {
MotionChangeRecommendationComponent,
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 { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
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.
@ -45,6 +46,11 @@ import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-mot
styleUrls: ['./motion-detail-diff.component.scss']
})
export class MotionDetailDiffComponent extends BaseViewComponent implements AfterViewInit {
/**
* Get the {@link getRecommendationTypeName}-Function from Utils
*/
public getRecommendationTypeName = getRecommendationTypeName;
@Input()
public motion: ViewMotion;
@Input()
@ -222,24 +228,6 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
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.
* 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 { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.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 { MotionPollService } from './motion-poll.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 { LinenumberingService } from 'app/core/ui-services/linenumbering.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 { 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
@ -45,6 +48,11 @@ export type InfoToExport =
providedIn: 'root'
})
export class MotionPdfService {
/**
* Get the {@link getRecommendationTypeName}-Function from Utils
*/
public getRecommendationTypeName = getRecommendationTypeName;
/**
* Constructor
*
@ -91,6 +99,8 @@ export class MotionPdfService {
infoToExport?: InfoToExport[],
commentsToExport?: number[]
): object {
// get the line length from the config
const lineLength = this.configService.instant<number>('motions_line_length');
let motionPdfContent = [];
// Enforces that statutes should always have Diff Mode and no line numbers
@ -116,14 +126,14 @@ export class MotionPdfService {
motionPdfContent = [title, subtitle];
if ((infoToExport && infoToExport.length > 0) || !infoToExport) {
const metaInfo = this.createMetaInfoTable(motion, crMode, infoToExport);
const metaInfo = this.createMetaInfoTable(motion, lineLength, crMode, infoToExport);
motionPdfContent.push(metaInfo);
}
if (!contentToExport || contentToExport.includes('text')) {
const preamble = this.createPreamble(motion);
motionPdfContent.push(preamble);
const text = this.createText(motion, lnMode, crMode);
const text = this.createText(motion, lineLength, lnMode, crMode);
motionPdfContent.push(text);
}
@ -192,7 +202,12 @@ export class MotionPdfService {
* @param motion the target motion
* @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 = [];
// submitters
@ -367,23 +382,13 @@ export class MotionPdfService {
}
// summary of change recommendations (for motion diff version only)
const changeRecos = this.changeRecoRepo
.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;
}
});
const changes = this.getUnifiedChanges(motion, lineLength);
if (crMode === ChangeRecoMode.Diff && changeRecos.length > 0) {
if (crMode === ChangeRecoMode.Diff && changes.length > 0) {
const columnLineNumbers = [];
const columnChangeType = [];
changeRecos.forEach(changeReco => {
changes.forEach(change => {
// TODO: the function isTitleRecommendation() does not exist anymore.
// Not sure if required or not
// if (changeReco.isTitleRecommendation()) {
@ -392,44 +397,56 @@ export class MotionPdfService {
// line numbers column
let line;
if (changeReco.line_from >= changeReco.line_to - 1) {
line = changeReco.line_from;
if (change.getLineFrom() >= change.getLineTo() - 1) {
line = change.getLineFrom();
} 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
if (changeReco.type === 0) {
columnChangeType.push(this.translate.instant('Replacement'));
} else if (changeReco.type === 1) {
columnChangeType.push(this.translate.instant('Insertion'));
} else if (changeReco.type === 2) {
columnChangeType.push(this.translate.instant('Deletion'));
} else if (changeReco.type === 3) {
columnChangeType.push(changeReco.other_description);
if (change.getChangeType() === ViewUnifiedChangeType.TYPE_CHANGE_RECOMMENDATION) {
const changeReco = change as ViewMotionChangeRecommendation;
columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `);
columnChangeType.push(
`(${this.translate.instant('Change recommendation')}) - ${this.translate.instant(
this.getRecommendationTypeName(changeReco)
)}`
);
} 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([
{
text: this.translate.instant('Summary of changes'),
style: 'boldText'
},
{
columns: [
{
text: columnLineNumbers.join('\n'),
width: 'auto'
},
{
text: columnChangeType.join('\n'),
width: 'auto'
}
],
columnGap: 7
}
]);
if (columnChangeType.length > 0) {
metaTableBody.push([
{
text: this.translate.instant('Summary of changes'),
style: 'boldText'
},
{
columns: [
{
text: columnLineNumbers.join('\n'),
width: 'auto'
},
{
text: columnChangeType.join('\n'),
width: 'auto'
}
],
columnGap: 7
}
]);
}
}
if (metaTableBody.length > 0) {
@ -479,10 +496,13 @@ export class MotionPdfService {
* @param crMode determine the used change Recommendation mode
* @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;
// get the line length from the config
const lineLength = this.configService.instant<number>('motions_line_length');
if (motion.isParagraphBasedAmendment()) {
motionText = '';
@ -510,26 +530,8 @@ export class MotionPdfService {
} else {
// lead motion or normal amendments
// 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 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());
const changes = this.getUnifiedChanges(motion, lineLength);
motionText = this.motionRepo.formatMotion(motion.id, crMode, changes, lineLength);
// reformat motion text to split long HTML elements to easier convert into PDF
motionText = this.linenumberingService.splitInlineElementsAtLineBreaks(motionText);
@ -538,6 +540,30 @@ export class MotionPdfService {
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
*
@ -688,6 +714,7 @@ export class MotionPdfService {
const subtitle = this.createSubtitle(motion);
const metaInfo = this.createMetaInfoTable(
motion,
this.configService.instant<number>('motions_line_length'),
this.configService.instant('motions_recommendation_text_mode'),
['submitters', 'state', 'category']
);