diff --git a/client/src/app/core/ui-services/csv-export.service.ts b/client/src/app/core/ui-services/csv-export.service.ts index 081ba38a9..a5df9db93 100644 --- a/client/src/app/core/ui-services/csv-export.service.ts +++ b/client/src/app/core/ui-services/csv-export.service.ts @@ -94,15 +94,11 @@ export class CsvExportService { let csvContent = []; // Holds all lines as arrays with each column-value // 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.map(column => { let label: string; @@ -112,15 +108,14 @@ export class CsvExportService { label = column.label; } label = this.capitalizeTranslate(label); - tsList = this.checkCsvTextSafety(label, tsList); return label; }); csvContent.push(header); // create lines csvContent = csvContent.concat( - models.map(model => { - return columns.map(column => { + models.map(model => + columns.map(column => { let value: string; if (isPropertyDefinition(column)) { @@ -139,21 +134,13 @@ export class CsvExportService { } else if (isMapDefinition(column)) { value = column.map(model); } - tsList = this.checkCsvTextSafety(value, tsList); - - return value; - }); - }) + return this.checkCsvTextSafety(value); + }) + ) ); - // assemble lines, putting text separator in place - if (!tsList.length) { - throw new Error('no usable text separator left for valid csv text'); - } const csvContentAsString: string = csvContent - .map(line => { - return line.map(entry => tsList[0] + entry + tsList[0]).join(columnSeparator); - }) + .map((line: string[]) => line.join(columnSeparator)) .join(lineSeparator); const filetype = `text/csv;charset=${encoding}`; if (encoding === 'iso-8859-15') { @@ -164,20 +151,17 @@ export class CsvExportService { } /** - * 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. + * Ensures, that the given string has escaped double quotes + * and no linebreak. The string itself will also be escaped by `double quotes`. * - * @param input any input to be sent to CSV - * @param tsList The list of special characters to check. - * @returns the cleaned CSV String list + * @param {string} input any input to be sent to CSV + * @returns {string} the cleaned string. */ - public checkCsvTextSafety(input: string, tsList: string[]): string[] { - if (input === null || input === undefined) { - return tsList; + public checkCsvTextSafety(input: string): string { + if (!input) { + return ''; } - - return tsList.filter(char => input.indexOf(char) < 0); + return '"' + input.replace(/"/g, '""').replace(/(\r\n\t|\n|\r\t)/gm, '') + '"'; } /** @@ -193,21 +177,22 @@ export class CsvExportService { public dummyCSVExport(header: string[], rows: (string | number | boolean | null)[][], filename: string): void { const separator = this.config.instant('general_csv_separator'); - const tsList = this.checkCsvTextSafety(separator, ['"', "'", '`', '/', '\\', ';', '.']); const headerRow = [header.map(item => this.translate.instant(item)).join(separator)]; const content = rows.map(row => row .map(item => { - if (item === null) { - return ''; + let value = ''; + if (!item) { + value = ''; } - if (typeof item === 'string') { - return `${tsList[0]}${item}${tsList[0]}`; + if (typeof item === 'number') { + value = item.toString(10); } else if (typeof item === 'boolean') { - return item ? '1' : '0'; + value = item ? '1' : '0'; } else { - return `${item}`; + value = item; } + return this.checkCsvTextSafety(value); }) .join(separator) ); diff --git a/client/src/app/shared/utils/reconvert-chars.ts b/client/src/app/shared/utils/reconvert-chars.ts index 74ec7111f..d1b289191 100644 --- a/client/src/app/shared/utils/reconvert-chars.ts +++ b/client/src/app/shared/utils/reconvert-chars.ts @@ -14,5 +14,6 @@ export function reconvertChars(text: string): string { .replace(/ü/g, 'ü') .replace(/Ü/g, 'Ü') .replace(/å|å/g, 'å') - .replace(/Å|Å/g, 'Å'); + .replace(/Å|Å/g, 'Å') + .replace(/ß|ß/g, 'ß'); } diff --git a/client/src/app/site/motions/modules/motion-import/motion-import-list.component.html b/client/src/app/site/motions/modules/motion-import/motion-import-list.component.html index 5dccc4a97..213422b4b 100644 --- a/client/src/app/site/motions/modules/motion-import/motion-import-list.component.html +++ b/client/src/app/site/motions/modules/motion-import/motion-import-list.component.html @@ -199,11 +199,11 @@ > warning - +
{{ submitter.name }} add   - +
diff --git a/client/src/app/site/motions/modules/motion-list/components/motion-export-dialog/motion-export-dialog.component.ts b/client/src/app/site/motions/modules/motion-list/components/motion-export-dialog/motion-export-dialog.component.ts index eb5183165..fef1a6c93 100644 --- a/client/src/app/site/motions/modules/motion-list/components/motion-export-dialog/motion-export-dialog.component.ts +++ b/client/src/app/site/motions/modules/motion-list/components/motion-export-dialog/motion-export-dialog.component.ts @@ -148,13 +148,6 @@ export class MotionExportDialogComponent implements OnInit { this.enableControl('content'); } - // At the moment the csv can't export comments. - if (format === FileFormat.CSV) { - this.disableControl('comments'); - } else { - this.enableControl('comments'); - } - if (format === FileFormat.CSV || format === FileFormat.XLSX) { this.disableControl('lnMode'); this.disableControl('crMode'); diff --git a/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.ts b/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.ts index 7891a924e..9f3700779 100644 --- a/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.ts +++ b/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.ts @@ -320,11 +320,18 @@ export class MotionListComponent extends BaseListViewComponent imple } } } else if (exportInfo.format === FileFormat.CSV) { - this.motionCsvExport.exportMotionList( - data, - [...exportInfo.content, ...exportInfo.metaInfo], - exportInfo.crMode - ); + const content = []; + const comments = []; + if (exportInfo.content) { + content.push(...exportInfo.content); + } + if (exportInfo.metaInfo) { + content.push(...exportInfo.metaInfo); + } + if (exportInfo.comments) { + comments.push(...exportInfo.comments); + } + this.motionCsvExport.exportMotionList(data, content, comments, exportInfo.crMode); } else if (exportInfo.format === FileFormat.XLSX) { this.motionXlsxExport.exportMotionList(data, exportInfo.metaInfo, exportInfo.comments); } diff --git a/client/src/app/site/motions/services/motion-csv-export.service.ts b/client/src/app/site/motions/services/motion-csv-export.service.ts index 2fc8ea18e..76d1cbd7d 100644 --- a/client/src/app/site/motions/services/motion-csv-export.service.ts +++ b/client/src/app/site/motions/services/motion-csv-export.service.ts @@ -14,6 +14,9 @@ import { ChangeRecommendationRepositoryService } from 'app/core/repositories/mot import { ConfigService } from 'app/core/ui-services/config.service'; import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change'; import { LinenumberingService } from 'app/core/ui-services/linenumbering.service'; +import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service'; +import { reconvertChars } from 'app/shared/utils/reconvert-chars'; +import { stripHtmlTags } from 'app/shared/utils/strip-html-tags'; /** * Exports CSVs for motions. Collect all CSV types here to have them in one place. @@ -38,7 +41,8 @@ export class MotionCsvExportService { private configService: ConfigService, private linenumberingService: LinenumberingService, private changeRecoRepo: ChangeRecommendationRepositoryService, - private motionRepo: MotionRepositoryService + private motionRepo: MotionRepositoryService, + private commentRepo: MotionCommentSectionRepositoryService ) {} /** @@ -83,7 +87,12 @@ export class MotionCsvExportService { * @param contentToExport content properties to export * @param crMode */ - public exportMotionList(motions: ViewMotion[], contentToExport: string[], crMode?: ChangeRecoMode): void { + public exportMotionList( + motions: ViewMotion[], + contentToExport: string[], + comments: number[], + crMode?: ChangeRecoMode + ): void { if (!crMode) { crMode = this.configService.instant('motions_recommendation_text_mode'); } @@ -116,6 +125,18 @@ export class MotionCsvExportService { return { property: option } as CsvColumnDefinitionProperty; } }); + exportProperties.push( + ...comments.map(commentId => ({ + label: this.commentRepo.getViewModel(commentId).getTitle(), + map: (motion: ViewMotion) => { + const viewComment = this.commentRepo.getViewModel(commentId); + const motionComment = motion.getCommentForSection(viewComment); + return motionComment && motionComment.comment + ? reconvertChars(stripHtmlTags(motionComment.comment)) + : ''; + } + })) + ); this.csvExport.export(motions, exportProperties, this.translate.instant('Motions') + '.csv'); } @@ -147,29 +168,29 @@ export class MotionCsvExportService { public exportDummyMotion(): void { const headerRow = [ 'Identifier', + 'Submitters', 'Title', 'Text', 'Reason', - 'Submitters', 'Category', 'Tags', - 'Origin', - 'Motion block' + 'Motion block', + 'Origin' ]; const rows = [ [ 'A1', + 'Submitter A', 'Title 1', 'Text 1', 'Reason 1', - 'Submitter A', 'Category A', 'Tag 1, Tag 2', - 'Last Year Conference A', - 'Block A' + 'Block A', + 'Last Year Conference A' ], - ['B1', 'Title 2', 'Text 2', 'Reason 2', 'Submitter B', 'Category B', null, null, 'Block A'], - [null, 'Title 3', 'Text 3', null, null, null, null, null, null] + ['B1', 'Submitter B', 'Title 2', 'Text 2', 'Reason 2', 'Category B', null, 'Block A', 'Origin B'], + ['C2', null, 'Title 3', 'Text 3', null, null, null, null, null] ]; this.csvExport.dummyCSVExport(headerRow, rows, `${this.translate.instant('motions-example')}.csv`); } diff --git a/client/src/app/site/motions/services/motion-import.service.ts b/client/src/app/site/motions/services/motion-import.service.ts index 03633ccbb..79a201649 100644 --- a/client/src/app/site/motions/services/motion-import.service.ts +++ b/client/src/app/site/motions/services/motion-import.service.ts @@ -201,7 +201,7 @@ export class MotionImportService extends BaseImportService { } const submitterArray = submitterlist.split(','); // TODO fails with 'full name' for (const submitter of submitterArray) { - const existingSubmitters = this.userRepo.getUsersByName(submitter); + const existingSubmitters = this.userRepo.getUsersByName(submitter.trim()); if (!existingSubmitters.length) { if (!this.newSubmitters.find(listedSubmitter => listedSubmitter.name === submitter)) { this.newSubmitters.push({ name: submitter });