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',
ownKey: 'changeRecommendations',
foreignViewModel: ViewMotionChangeRecommendation
},
{
type: 'O2M',
foreignIdKey: 'parent_id',
ownKey: 'amendments',
foreignViewModel: ViewMotion
}
// Personal notes are dynamically added in the repo.
];

View File

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

View File

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

View File

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

View File

@ -62,24 +62,6 @@ export interface MotionTitleInformation extends TitleInformationWithAgendaItem {
*/
export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Motion>
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 {
return this._model;
}
@ -368,10 +350,48 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
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
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
*

View File

@ -46,7 +46,7 @@
<span *ngIf="motion.diffLines && motion.diffLines.length">
<span *ngIf="motion.identifier">&nbsp;&middot;&nbsp;</span>
<span translate>Line</span>
<span>&nbsp;{{ getChangeLines(motion) }}</span>
<span>&nbsp;{{ motion.getChangeLines() }}</span>
</span>
</div>
@ -106,6 +106,10 @@
<mat-icon>archive</mat-icon>
<span translate>Export</span>
</button>
<button mat-menu-item (click)="exportAmendmentListPdf()">
<mat-icon>picture_as_pdf</mat-icon>
<span translate>Amendment list PDF</span>
</button>
</div>
<div *ngIf="isMultiSelect">
<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 { MotionExportDialogComponent } from '../shared-motion/motion-export-dialog/motion-export-dialog.component';
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 { ViewMotion } from '../../models/view-motion';
@ -32,6 +33,11 @@ import { ViewMotion } from '../../models/view-motion';
encapsulation: ViewEncapsulation.None
})
export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> implements OnInit {
/**
* Hold the Id of the parent motion
*/
private parentMotionId: number;
/**
* Hold the parent motion if present
*/
@ -91,7 +97,8 @@ export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> im
private sanitizer: DomSanitizer,
private dialog: MatDialog,
private motionExport: MotionExportService,
private linenumberingService: LinenumberingService
private linenumberingService: LinenumberingService,
private pdfExport: MotionPdfExportService
) {
super(titleService, translate, matSnackBar, storage);
super.setTitle('Amendments');
@ -105,9 +112,9 @@ export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> im
// if there is a subscription to the parent motion
this.parentMotion = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
const parentMotionId = +params.get('id');
this.amendmentFilterService.parentMotionId = parentMotionId;
return this.motionRepo.getViewModelObservable(parentMotionId);
this.parentMotionId = +params.get('id');
this.amendmentFilterService.parentMotionId = this.parentMotionId;
return this.motionRepo.getViewModelObservable(this.parentMotionId);
})
);
} 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
*
@ -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 {
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 { E2EImportsModule } from 'e2e-imports.module';
import { AmendmentSortListService } from './amendment-sort-list.service';
describe('AmendmentSortListService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule]
})
);
it('should be created', () => {
const service: AmendmentSortListService = TestBed.get(AmendmentSortListService);

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/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 { ConfigService } from 'app/core/ui-services/config.service';
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
@ -30,6 +31,7 @@ export class MotionPdfExportService {
private translate: TranslateService,
private configService: ConfigService,
private motionPdfService: MotionPdfService,
private amendmentListPdfService: AmendmentListPdfService,
private pdfCatalogService: MotionPdfCatalogService,
private pdfDocumentService: PdfDocumentService
) {}
@ -116,4 +118,22 @@ export class MotionPdfExportService {
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);
}
}