Merge pull request #4994 from tsiegleauq/amendment-list-pdf-overview

Add amendment overview list as PDF
This commit is contained in:
Emanuel Schütze 2019-09-10 17:19:50 +02:00 committed by GitHub
commit 39b0168714
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 244 additions and 51 deletions

View File

@ -138,6 +138,12 @@ const MotionRelations: RelationDefinition[] = [
ownIdKey: 'change_recommendations_id', ownIdKey: 'change_recommendations_id',
ownKey: 'changeRecommendations', ownKey: 'changeRecommendations',
foreignViewModel: ViewMotionChangeRecommendation foreignViewModel: ViewMotionChangeRecommendation
},
{
type: 'O2M',
foreignIdKey: 'parent_id',
ownKey: 'amendments',
foreignViewModel: ViewMotion
} }
// Personal notes are dynamically added in the repo. // Personal notes are dynamically added in the repo.
]; ];

View File

@ -1,9 +1,15 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { OverlayService } from './overlay.service'; import { OverlayService } from './overlay.service';
describe('OverlayService', () => { describe('OverlayService', () => {
beforeEach(() => TestBed.configureTestingModule({})); beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule]
})
);
it('should be created', () => { it('should be created', () => {
const service: OverlayService = TestBed.get(OverlayService); const service: OverlayService = TestBed.get(OverlayService);

View File

@ -4,7 +4,7 @@ import { E2EImportsModule } from 'e2e-imports.module';
import { PreviewComponent } from './preview.component'; import { PreviewComponent } from './preview.component';
fdescribe('PreviewComponent', () => { describe('PreviewComponent', () => {
let component: PreviewComponent; let component: PreviewComponent;
let fixture: ComponentFixture<PreviewComponent>; let fixture: ComponentFixture<PreviewComponent>;

View File

@ -5,7 +5,7 @@ import { E2EImportsModule } from 'e2e-imports.module';
import { ProgressSnackBarComponent } from './progress-snack-bar.component'; import { ProgressSnackBarComponent } from './progress-snack-bar.component';
fdescribe('ProgressSnackBarComponent', () => { describe('ProgressSnackBarComponent', () => {
let component: ProgressSnackBarComponent; let component: ProgressSnackBarComponent;
let fixture: ComponentFixture<ProgressSnackBarComponent>; let fixture: ComponentFixture<ProgressSnackBarComponent>;

View File

@ -3,7 +3,7 @@ import { async, TestBed } from '@angular/core/testing';
// import { SuperSearchComponent } from './super-search.component'; // import { SuperSearchComponent } from './super-search.component';
import { E2EImportsModule } from 'e2e-imports.module'; import { E2EImportsModule } from 'e2e-imports.module';
fdescribe('SuperSearchComponent', () => { describe('SuperSearchComponent', () => {
// let component: SuperSearchComponent; // let component: SuperSearchComponent;
// let fixture: ComponentFixture<SuperSearchComponent>; // let fixture: ComponentFixture<SuperSearchComponent>;

View File

@ -62,24 +62,6 @@ export interface MotionTitleInformation extends TitleInformationWithAgendaItem {
*/ */
export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Motion> export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Motion>
implements MotionTitleInformation, Searchable { implements MotionTitleInformation, Searchable {
public static COLLECTIONSTRING = Motion.COLLECTIONSTRING;
protected _collectionString = Motion.COLLECTIONSTRING;
protected _category?: ViewCategory;
protected _submitters?: ViewSubmitter[];
protected _supporters?: ViewUser[];
protected _workflow?: ViewWorkflow;
protected _state?: ViewState;
protected _recommendation?: ViewState;
protected _motion_block?: ViewMotionBlock;
protected _attachments?: ViewMediafile[];
protected _tags?: ViewTag[];
protected _parent?: ViewMotion;
protected _amendments?: ViewMotion[];
protected _changeRecommendations?: ViewMotionChangeRecommendation[];
protected _diffLines?: DiffLinesInParagraph[];
public personalNote?: PersonalNoteContent;
public get motion(): Motion { public get motion(): Motion {
return this._model; return this._model;
} }
@ -368,10 +350,48 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
return null; return null;
} }
} }
public static COLLECTIONSTRING = Motion.COLLECTIONSTRING;
protected _collectionString = Motion.COLLECTIONSTRING;
protected _category?: ViewCategory;
protected _submitters?: ViewSubmitter[];
protected _supporters?: ViewUser[];
protected _workflow?: ViewWorkflow;
protected _state?: ViewState;
protected _recommendation?: ViewState;
protected _motion_block?: ViewMotionBlock;
protected _attachments?: ViewMediafile[];
protected _tags?: ViewTag[];
protected _parent?: ViewMotion;
protected _amendments?: ViewMotion[];
protected _changeRecommendations?: ViewMotionChangeRecommendation[];
protected _diffLines?: DiffLinesInParagraph[];
public personalNote?: PersonalNoteContent;
// This is set by the repository // This is set by the repository
public getIdentifierOrTitle: () => string; public getIdentifierOrTitle: () => string;
/**
* Extract the lines of the amendments
* If an amendments has multiple changes, they will be printed like an array of strings
*
* @param amendment the motion to create the amendment to
* @return The lines of the amendment
*/
public getChangeLines(): string {
if (!!this.diffLines) {
return this.diffLines
.map(diffLine => {
if (diffLine.diffLineTo === diffLine.diffLineFrom + 1) {
return '' + diffLine.diffLineFrom;
} else {
return `${diffLine.diffLineFrom} - ${diffLine.diffLineTo - 1}`;
}
})
.toString();
}
}
/** /**
* Formats the category for search * Formats the category for search
* *

View File

@ -46,7 +46,7 @@
<span *ngIf="motion.diffLines && motion.diffLines.length"> <span *ngIf="motion.diffLines && motion.diffLines.length">
<span *ngIf="motion.identifier">&nbsp;&middot;&nbsp;</span> <span *ngIf="motion.identifier">&nbsp;&middot;&nbsp;</span>
<span translate>Line</span> <span translate>Line</span>
<span>&nbsp;{{ getChangeLines(motion) }}</span> <span>&nbsp;{{ motion.getChangeLines() }}</span>
</span> </span>
</div> </div>
@ -106,6 +106,10 @@
<mat-icon>archive</mat-icon> <mat-icon>archive</mat-icon>
<span translate>Export</span> <span translate>Export</span>
</button> </button>
<button mat-menu-item (click)="exportAmendmentListPdf()">
<mat-icon>picture_as_pdf</mat-icon>
<span translate>Amendment list PDF</span>
</button>
</div> </div>
<div *ngIf="isMultiSelect"> <div *ngIf="isMultiSelect">
<button mat-menu-item (click)="selectAll()"> <button mat-menu-item (click)="selectAll()">

View File

@ -18,6 +18,7 @@ import { largeDialogSettings } from 'app/shared/utils/dialog-settings';
import { BaseListViewComponent } from 'app/site/base/base-list-view'; import { BaseListViewComponent } from 'app/site/base/base-list-view';
import { MotionExportDialogComponent } from '../shared-motion/motion-export-dialog/motion-export-dialog.component'; import { MotionExportDialogComponent } from '../shared-motion/motion-export-dialog/motion-export-dialog.component';
import { MotionExportInfo, MotionExportService } from '../../services/motion-export.service'; import { MotionExportInfo, MotionExportService } from '../../services/motion-export.service';
import { MotionPdfExportService } from '../../services/motion-pdf-export.service';
import { MotionSortListService } from '../../services/motion-sort-list.service'; import { MotionSortListService } from '../../services/motion-sort-list.service';
import { ViewMotion } from '../../models/view-motion'; import { ViewMotion } from '../../models/view-motion';
@ -32,6 +33,11 @@ import { ViewMotion } from '../../models/view-motion';
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> implements OnInit { export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> implements OnInit {
/**
* Hold the Id of the parent motion
*/
private parentMotionId: number;
/** /**
* Hold the parent motion if present * Hold the parent motion if present
*/ */
@ -91,7 +97,8 @@ export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> im
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private dialog: MatDialog, private dialog: MatDialog,
private motionExport: MotionExportService, private motionExport: MotionExportService,
private linenumberingService: LinenumberingService private linenumberingService: LinenumberingService,
private pdfExport: MotionPdfExportService
) { ) {
super(titleService, translate, matSnackBar, storage); super(titleService, translate, matSnackBar, storage);
super.setTitle('Amendments'); super.setTitle('Amendments');
@ -105,9 +112,9 @@ export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> im
// if there is a subscription to the parent motion // if there is a subscription to the parent motion
this.parentMotion = this.route.paramMap.pipe( this.parentMotion = this.route.paramMap.pipe(
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
const parentMotionId = +params.get('id'); this.parentMotionId = +params.get('id');
this.amendmentFilterService.parentMotionId = parentMotionId; this.amendmentFilterService.parentMotionId = this.parentMotionId;
return this.motionRepo.getViewModelObservable(parentMotionId); return this.motionRepo.getViewModelObservable(this.parentMotionId);
}) })
); );
} else { } else {
@ -115,29 +122,6 @@ export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> im
} }
} }
/**
* Extract the lines of the amendments
* If an amendments has multiple changes, they will be printed like an array of strings
*
* @param amendment the motion to create the amendment to
* @return The lines of the amendment
*/
public getChangeLines(amendment: ViewMotion): string {
const diffLines = amendment.diffLines;
if (!!diffLines) {
return diffLines
.map(diffLine => {
if (diffLine.diffLineTo === diffLine.diffLineFrom + 1) {
return '' + diffLine.diffLineFrom;
} else {
return `${diffLine.diffLineFrom} - ${diffLine.diffLineTo - 1}`;
}
})
.toString();
}
}
/** /**
* Formulate the amendment summary * Formulate the amendment summary
* *
@ -172,6 +156,14 @@ export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> im
); );
} }
/**
* Export the given motion ist as special PDF
*/
public exportAmendmentListPdf(): void {
const parentMotion = this.parentMotionId ? this.motionRepo.getViewModel(this.parentMotionId) : undefined;
this.pdfExport.exportAmendmentList(this.dataSource.filteredData, parentMotion);
}
public sanitizeText(text: string): SafeHtml { public sanitizeText(text: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(text); return this.sanitizer.bypassSecurityTrustHtml(text);
} }

View File

@ -0,0 +1,18 @@
import { TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { AmendmentListPdfService } from './amendment-list-pdf.service';
describe('AmendmentListPdfService', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule]
})
);
it('should be created', () => {
const service: AmendmentListPdfService = TestBed.get(AmendmentListPdfService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,121 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { HtmlToPdfService } from 'app/core/pdf-services/html-to-pdf.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { ViewMotion } from '../models/view-motion';
/**
* Creates a PDF list for amendments
*/
@Injectable({
providedIn: 'root'
})
export class AmendmentListPdfService {
public constructor(
private motionRepo: MotionRepositoryService,
private translate: TranslateService,
private htmlToPdfService: HtmlToPdfService
) {}
/**
* Also required by amendment-detail. Should be own service
* @param amendment
* @return rendered PDF text
*/
private renderDiffLines(amendment: ViewMotion): object {
if (amendment.diffLines && amendment.diffLines.length) {
const linesHtml = amendment.diffLines.map(line => line.text).join('<br />[...]<br />');
return this.htmlToPdfService.convertHtml(linesHtml);
}
}
/**
* Converts an amendment to a row of the `amendmentRows` table
* @amendment the amendment to convert
* @returns a line in the row as PDF doc definition
*/
private amendmentToTableRow(amendment: ViewMotion): object {
let recommendationText = '';
if (amendment.recommendation) {
if (amendment.recommendation.show_recommendation_extension_field && amendment.recommendationExtension) {
recommendationText += ` ${this.motionRepo.getExtendedRecommendationLabel(amendment)}`;
} else {
recommendationText += this.translate.instant(amendment.recommendation.recommendation_label);
}
}
return [
{
text: amendment.identifierOrTitle
},
{
text: amendment.getChangeLines()
},
{
text: amendment.submittersAsUsers.toString()
},
{
text: this.renderDiffLines(amendment)
},
{
text: recommendationText
}
];
}
/**
* Creates the PDFmake document structure for amendment list overview
* @param docTitle the header
* @param amendments the amendments to render
*/
public overviewToDocDef(docTitle: string, amendments: ViewMotion[]): object {
const title = {
text: docTitle,
style: 'title'
};
const amendmentTableBody: object[] = [
[
{
text: this.translate.instant('Motion'),
style: 'tableHeader'
},
{
text: this.translate.instant('Line'),
style: 'tableHeader'
},
{
text: this.translate.instant('Submitters'),
style: 'tableHeader'
},
{
text: this.translate.instant('Changes'),
style: 'tableHeader'
},
{
text: this.translate.instant('Recommendation'),
style: 'tableHeader'
}
]
];
const amendmentRows: object[] = [];
for (const amendment of amendments) {
amendmentRows.push(this.amendmentToTableRow(amendment));
}
const table: object = {
table: {
widths: ['auto', 'auto', 'auto', '*', 'auto'],
headerRows: 1,
dontBreakRows: true,
body: amendmentTableBody.concat(amendmentRows)
},
layout: 'switchColorTableLayout'
};
return [title, table];
}
}

View File

@ -1,9 +1,15 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { AmendmentSortListService } from './amendment-sort-list.service'; import { AmendmentSortListService } from './amendment-sort-list.service';
describe('AmendmentSortListService', () => { describe('AmendmentSortListService', () => {
beforeEach(() => TestBed.configureTestingModule({})); beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule]
})
);
it('should be created', () => { it('should be created', () => {
const service: AmendmentSortListService = TestBed.get(AmendmentSortListService); const service: AmendmentSortListService = TestBed.get(AmendmentSortListService);

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { AmendmentListPdfService } from './amendment-list-pdf.service';
import { PdfDocumentService } from 'app/core/pdf-services/pdf-document.service'; import { PdfDocumentService } from 'app/core/pdf-services/pdf-document.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { PersonalNoteContent } from 'app/shared/models/users/personal-note'; import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
@ -30,6 +31,7 @@ export class MotionPdfExportService {
private translate: TranslateService, private translate: TranslateService,
private configService: ConfigService, private configService: ConfigService,
private motionPdfService: MotionPdfService, private motionPdfService: MotionPdfService,
private amendmentListPdfService: AmendmentListPdfService,
private pdfCatalogService: MotionPdfCatalogService, private pdfCatalogService: MotionPdfCatalogService,
private pdfDocumentService: PdfDocumentService private pdfDocumentService: PdfDocumentService
) {} ) {}
@ -116,4 +118,22 @@ export class MotionPdfExportService {
this.pdfDocumentService.download(doc, filename, metadata); this.pdfDocumentService.download(doc, filename, metadata);
} }
} }
/**
* Exports the amendments to the given motion as an overview table
* @param parentMotion
*/
public exportAmendmentList(amendments: ViewMotion[], parentMotion?: ViewMotion): void {
let filename: string;
if (parentMotion) {
filename = `${this.translate.instant('Amendments to')} ${parentMotion.getListTitle()}`;
} else {
filename = `${this.translate.instant('Amendments')}`;
}
const doc = this.amendmentListPdfService.overviewToDocDef(filename, amendments);
const metadata = {
title: filename
};
this.pdfDocumentService.downloadLandscape(doc, filename, metadata);
}
} }