From 86b629620535524e257cff06e9fb583b83f23c26 Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Wed, 17 Oct 2018 16:37:01 +0200 Subject: [PATCH] saving as CSV implemented (user list) --- client/package.json | 1 + client/src/app/app.component.ts | 1 + .../core/services/csv-export.service.spec.ts | 17 +++ .../app/core/services/csv-export.service.ts | 119 ++++++++++++++++++ .../core/services/file-export.service.spec.ts | 17 +++ .../app/core/services/file-export.service.ts | 33 +++++ .../user-list/user-list.component.html | 4 +- .../user-list/user-list.component.ts | 27 +++- 8 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 client/src/app/core/services/csv-export.service.spec.ts create mode 100644 client/src/app/core/services/csv-export.service.ts create mode 100644 client/src/app/core/services/file-export.service.spec.ts create mode 100644 client/src/app/core/services/file-export.service.ts diff --git a/client/package.json b/client/package.json index 01c170fa7..dc4ae9798 100644 --- a/client/package.json +++ b/client/package.json @@ -29,6 +29,7 @@ "@ngx-translate/core": "^10.0.2", "@ngx-translate/http-loader": "^3.0.1", "core-js": "^2.5.4", + "file-saver": "^2.0.0-rc.3", "material-design-icons": "^3.0.1", "ngx-mat-select-search": "^1.4.0", "roboto-fontface": "^0.10.0", diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 012d17361..b5e48b7f6 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -54,6 +54,7 @@ export class AppComponent { * TODO: Overloading can be extended to more functions. */ private overloadArrayToString(): void { + Array.prototype.toString = function(): string { let string = ''; const iterations = Math.min(this.length, 3); diff --git a/client/src/app/core/services/csv-export.service.spec.ts b/client/src/app/core/services/csv-export.service.spec.ts new file mode 100644 index 000000000..92060fee4 --- /dev/null +++ b/client/src/app/core/services/csv-export.service.spec.ts @@ -0,0 +1,17 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { CsvExportService } from './csv-export.service'; +import { E2EImportsModule } from '../../../e2e-imports.module'; + +describe('CsvExportService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + providers: [CsvExportService] + }); + }); + + it('should be created', inject([CsvExportService], (service: CsvExportService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/client/src/app/core/services/csv-export.service.ts b/client/src/app/core/services/csv-export.service.ts new file mode 100644 index 000000000..543e0a00a --- /dev/null +++ b/client/src/app/core/services/csv-export.service.ts @@ -0,0 +1,119 @@ +import { BaseViewModel } from '../../site/base/base-view-model'; +import { Injectable } from '@angular/core'; +import { FileExportService } from './file-export.service'; +import { TranslateService } from '@ngx-translate/core'; + +@Injectable({ + providedIn: 'root' +}) +export class CsvExportService { + /** + * Constructor + */ + public constructor(protected exporter: FileExportService, private translate: TranslateService) {} + + /** + * Saves an array of model data to a CSV. + * @param data Array of Model instances to be saved + * @param columns Column definitions + * @param filename name of the resulting file + * @param options optional: + * lineSeparator (defaults to \r\n windows style line separator), + * columnseparator defaults to semicolon (other usual separators are ',' '\T' (tab), ' 'whitespace) + */ + public export( + data: T[], + columns: { + property: keyof T; // name of the property used for export + label?: string; + assemble?: string; // (if property is further child object, the property of these to be used) + }[], + filename: string, + { lineSeparator = '\r\n', columnSeparator = ';' }: { lineSeparator?: string; columnSeparator?: string } = {} + ): void { + const allLines = []; // Array of arrays of entries + const usedColumns = []; // mapped properties to be included + + // initial array of usable text separators. The first character not used + // in any text data or as column separator will be used as text separator + let tsList = ['"', "'", '`', '/', '\\', ';', '.']; + + if (lineSeparator === columnSeparator) { + throw new Error('lineseparator and columnseparator must differ from each other'); + } + + tsList = this.checkCsvTextSafety(lineSeparator, tsList); + tsList = this.checkCsvTextSafety(columnSeparator, tsList); + + // create header data + const header = []; + columns.forEach(column => { + const rawLabel: string = column.label ? column.label : (column.property as string); + const colLabel = this.capitalizeTranslate(rawLabel); + tsList = this.checkCsvTextSafety(colLabel, tsList); + header.push(colLabel); + usedColumns.push(column.property); + }); + allLines.push(header); + // create lines + data.forEach(item => { + const line = []; + for (let i = 0; i < usedColumns.length; i++ ){ + const property = usedColumns[i]; + let prop: any = item[property]; + if (columns[i].assemble){ + prop = item[property].map(subitem => this.translate.instant(subitem[columns[i].assemble])).join(','); + } + tsList = this.checkCsvTextSafety(prop, tsList); + line.push(prop); + }; + allLines.push(line); + }); + + // assemble lines, putting text separator in place + if (!tsList.length) { + throw new Error('no usable text separator left for valid csv text'); + } + + const allLinesAssembled = []; + allLines.forEach(line => { + const assembledLine = []; + line.forEach(item => { + if (typeof item === 'number') { + assembledLine.push(item.toString(10)); + } else if (item === null || item === undefined || item === '') { + assembledLine.push(''); + } else if (item === true) { + assembledLine.push('1'); + } else if (item === false) { + assembledLine.push('0'); + } else { + assembledLine.push(tsList[0] + item + tsList[0]); + } + }); + allLinesAssembled.push(assembledLine.join(columnSeparator)); + }); + this.exporter.saveFile(allLinesAssembled.join(lineSeparator), filename); + } + + /** + * Checks if a given input contains any of the characters defined in a list + * used for textseparators. The list is then returned without the 'special' + * characters, as they may not be used as text separator in this csv. + * @param input any input to be sent to CSV + * @param tsList The list of special characters to check. + */ + public checkCsvTextSafety(input: any, tsList: string[]): string[] { + if (input === null || input === undefined ) { + return tsList; + } + + const inputAsString = String(input); + return tsList.filter(char => inputAsString.indexOf(char) < 0); + } + + private capitalizeTranslate(input: string): string { + const temp = input.charAt(0).toUpperCase() + input.slice(1); + return this.translate.instant(temp); + } +} diff --git a/client/src/app/core/services/file-export.service.spec.ts b/client/src/app/core/services/file-export.service.spec.ts new file mode 100644 index 000000000..5a2e09ff2 --- /dev/null +++ b/client/src/app/core/services/file-export.service.spec.ts @@ -0,0 +1,17 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { FileExportService } from './file-export.service'; +import { E2EImportsModule } from '../../../e2e-imports.module'; + +describe('FileExportService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + providers: [FileExportService] + }); + }); + + it('should be created', inject([FileExportService], (service: FileExportService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/client/src/app/core/services/file-export.service.ts b/client/src/app/core/services/file-export.service.ts new file mode 100644 index 000000000..61d026d70 --- /dev/null +++ b/client/src/app/core/services/file-export.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { saveAs } from 'file-saver'; + +@Injectable({ + providedIn: 'root' +}) +export class FileExportService { + /** + * Constructor + */ + public constructor() {} + + /** + * Saves a file + * @param file + * @param filename + */ + public saveFile(file: BlobPart, filename: string): void { + const blob = new Blob([file]); + saveAs(blob, filename, { autoBOM: true }); + // autoBOM = automatic byte-order-mark + } + + /** + * Validates a file name for characters that might break. + * @param filename A name used to save a file + */ + protected validateFileName(filename: string): boolean { + // everything not containing \/?%*:|"<> and not ending on a dot + const pattern = new RegExp(/^[^\\\/\?%\*:\|\"\<\>]*[^\.]+$/i); + return pattern.test(filename); + } +} diff --git a/client/src/app/site/users/components/user-list/user-list.component.html b/client/src/app/site/users/components/user-list/user-list.component.html index 5daaefb96..a777ee53a 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.html +++ b/client/src/app/site/users/components/user-list/user-list.component.html @@ -75,8 +75,8 @@ Import ... - diff --git a/client/src/app/site/users/components/user-list/user-list.component.ts b/client/src/app/site/users/components/user-list/user-list.component.ts index 0c7eb484b..3d1c27edf 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.ts +++ b/client/src/app/site/users/components/user-list/user-list.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; +import { CsvExportService } from '../../../../core/services/csv-export.service'; import { ViewUser } from '../../models/view-user'; import { UserRepositoryService } from '../../services/user-repository.service'; @@ -17,6 +18,7 @@ import { Router, ActivatedRoute } from '@angular/router'; styleUrls: ['./user-list.component.scss'] }) export class UserListComponent extends ListViewBaseComponent implements OnInit { + /** * The usual constructor for components * @param repo the user repository @@ -28,7 +30,8 @@ export class UserListComponent extends ListViewBaseComponent implement protected titleService: Title, protected translate: TranslateService, private router: Router, - private route: ActivatedRoute + private route: ActivatedRoute, + protected csvExport: CsvExportService ) { super(titleService, translate); } @@ -69,4 +72,26 @@ export class UserListComponent extends ListViewBaseComponent implement public onPlusButton(): void { this.router.navigate(['./new'], { relativeTo: this.route }); } + + // TODO save all data from the dataSource + public csvExportUserList(): void { + this.csvExport.export( + this.dataSource.data, + [ + { property: 'title' }, + { property: 'first_name', label: 'First Name' }, + { property: 'last_name', label: 'Last Name' }, + { property: 'structure_level', label: 'Structure Level' }, + { property: 'participant_number', label: 'Participant Number' }, + { property: 'groups', assemble: 'name'}, + { property: 'comment' }, + { property: 'is_active', label: 'Active' }, + { property: 'is_present', label: 'Presence' }, + { property: 'is_committee', label: 'Committee' }, + { property: 'default_password', label: 'Default password' }, + { property: 'email', label: 'E-Mail' } + ], + 'export.csv' + ); + } }