Exporting comments in CSV

This commit is contained in:
GabrielMeyer 2019-07-12 18:25:15 +02:00
parent 1c3d60fe39
commit 8337f8928c
7 changed files with 70 additions and 63 deletions

View File

@ -94,15 +94,11 @@ export class CsvExportService {
let csvContent = []; // Holds all lines as arrays with each column-value let csvContent = []; // Holds all lines as arrays with each column-value
// initial array of usable text separators. The first character not used // 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 // in any text data or as column separator will be used as text separator
let tsList = ['"', "'", '`', '/', '\\', ';', '.'];
if (lineSeparator === columnSeparator) { if (lineSeparator === columnSeparator) {
throw new Error('lineseparator and columnseparator must differ from each other'); throw new Error('lineseparator and columnseparator must differ from each other');
} }
tsList = this.checkCsvTextSafety(lineSeparator, tsList);
tsList = this.checkCsvTextSafety(columnSeparator, tsList);
// create header data // create header data
const header = columns.map(column => { const header = columns.map(column => {
let label: string; let label: string;
@ -112,15 +108,14 @@ export class CsvExportService {
label = column.label; label = column.label;
} }
label = this.capitalizeTranslate(label); label = this.capitalizeTranslate(label);
tsList = this.checkCsvTextSafety(label, tsList);
return label; return label;
}); });
csvContent.push(header); csvContent.push(header);
// create lines // create lines
csvContent = csvContent.concat( csvContent = csvContent.concat(
models.map(model => { models.map(model =>
return columns.map(column => { columns.map(column => {
let value: string; let value: string;
if (isPropertyDefinition(column)) { if (isPropertyDefinition(column)) {
@ -139,21 +134,13 @@ export class CsvExportService {
} else if (isMapDefinition(column)) { } else if (isMapDefinition(column)) {
value = column.map(model); value = column.map(model);
} }
tsList = this.checkCsvTextSafety(value, tsList); return this.checkCsvTextSafety(value);
return 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 const csvContentAsString: string = csvContent
.map(line => { .map((line: string[]) => line.join(columnSeparator))
return line.map(entry => tsList[0] + entry + tsList[0]).join(columnSeparator);
})
.join(lineSeparator); .join(lineSeparator);
const filetype = `text/csv;charset=${encoding}`; const filetype = `text/csv;charset=${encoding}`;
if (encoding === 'iso-8859-15') { 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 * Ensures, that the given string has escaped double quotes
* used for textseparators. The list is then returned without the 'special' * and no linebreak. The string itself will also be escaped by `double quotes`.
* characters, as they may not be used as text separator in this csv.
* *
* @param input any input to be sent to CSV * @param {string} input any input to be sent to CSV
* @param tsList The list of special characters to check. * @returns {string} the cleaned string.
* @returns the cleaned CSV String list
*/ */
public checkCsvTextSafety(input: string, tsList: string[]): string[] { public checkCsvTextSafety(input: string): string {
if (input === null || input === undefined) { if (!input) {
return tsList; return '';
} }
return '"' + input.replace(/"/g, '""').replace(/(\r\n\t|\n|\r\t)/gm, '') + '"';
return tsList.filter(char => input.indexOf(char) < 0);
} }
/** /**
@ -193,21 +177,22 @@ export class CsvExportService {
public dummyCSVExport(header: string[], rows: (string | number | boolean | null)[][], filename: string): void { public dummyCSVExport(header: string[], rows: (string | number | boolean | null)[][], filename: string): void {
const separator = this.config.instant<string>('general_csv_separator'); const separator = this.config.instant<string>('general_csv_separator');
const tsList = this.checkCsvTextSafety(separator, ['"', "'", '`', '/', '\\', ';', '.']);
const headerRow = [header.map(item => this.translate.instant(item)).join(separator)]; const headerRow = [header.map(item => this.translate.instant(item)).join(separator)];
const content = rows.map(row => const content = rows.map(row =>
row row
.map(item => { .map(item => {
if (item === null) { let value = '';
return ''; if (!item) {
value = '';
} }
if (typeof item === 'string') { if (typeof item === 'number') {
return `${tsList[0]}${item}${tsList[0]}`; value = item.toString(10);
} else if (typeof item === 'boolean') { } else if (typeof item === 'boolean') {
return item ? '1' : '0'; value = item ? '1' : '0';
} else { } else {
return `${item}`; value = item;
} }
return this.checkCsvTextSafety(value);
}) })
.join(separator) .join(separator)
); );

View File

@ -14,5 +14,6 @@ export function reconvertChars(text: string): string {
.replace(/&uuml;/g, 'ü') .replace(/&uuml;/g, 'ü')
.replace(/&Uuml;/g, 'Ü') .replace(/&Uuml;/g, 'Ü')
.replace(/&aring;|&#229;/g, 'å') .replace(/&aring;|&#229;/g, 'å')
.replace(/&Aring;|&#197;/g, 'Å'); .replace(/&Aring;|&#197;/g, 'Å')
.replace(/&szlig;|&#223;/g, 'ß');
} }

View File

@ -199,11 +199,11 @@
> >
warning warning
</mat-icon> </mat-icon>
<span *ngFor="let submitter of entry.newEntry.csvSubmitters"> <div *ngFor="let submitter of entry.newEntry.csvSubmitters">
{{ submitter.name }} {{ submitter.name }}
<mat-icon class="newBadge" color="accent" inline *ngIf="!submitter.id">add</mat-icon> <mat-icon class="newBadge" color="accent" inline *ngIf="!submitter.id">add</mat-icon>
&nbsp; &nbsp;
</span> </div>
</div> </div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>

View File

@ -148,13 +148,6 @@ export class MotionExportDialogComponent implements OnInit {
this.enableControl('content'); 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) { if (format === FileFormat.CSV || format === FileFormat.XLSX) {
this.disableControl('lnMode'); this.disableControl('lnMode');
this.disableControl('crMode'); this.disableControl('crMode');

View File

@ -320,11 +320,18 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
} }
} }
} else if (exportInfo.format === FileFormat.CSV) { } else if (exportInfo.format === FileFormat.CSV) {
this.motionCsvExport.exportMotionList( const content = [];
data, const comments = [];
[...exportInfo.content, ...exportInfo.metaInfo], if (exportInfo.content) {
exportInfo.crMode 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) { } else if (exportInfo.format === FileFormat.XLSX) {
this.motionXlsxExport.exportMotionList(data, exportInfo.metaInfo, exportInfo.comments); this.motionXlsxExport.exportMotionList(data, exportInfo.metaInfo, exportInfo.comments);
} }

View File

@ -14,6 +14,9 @@ import { ChangeRecommendationRepositoryService } from 'app/core/repositories/mot
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change'; import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change';
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service'; 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. * 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 configService: ConfigService,
private linenumberingService: LinenumberingService, private linenumberingService: LinenumberingService,
private changeRecoRepo: ChangeRecommendationRepositoryService, 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 contentToExport content properties to export
* @param crMode * @param crMode
*/ */
public exportMotionList(motions: ViewMotion[], contentToExport: string[], crMode?: ChangeRecoMode): void { public exportMotionList(
motions: ViewMotion[],
contentToExport: string[],
comments: number[],
crMode?: ChangeRecoMode
): void {
if (!crMode) { if (!crMode) {
crMode = this.configService.instant('motions_recommendation_text_mode'); crMode = this.configService.instant('motions_recommendation_text_mode');
} }
@ -116,6 +125,18 @@ export class MotionCsvExportService {
return { property: option } as CsvColumnDefinitionProperty<ViewMotion>; return { property: option } as CsvColumnDefinitionProperty<ViewMotion>;
} }
}); });
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'); this.csvExport.export(motions, exportProperties, this.translate.instant('Motions') + '.csv');
} }
@ -147,29 +168,29 @@ export class MotionCsvExportService {
public exportDummyMotion(): void { public exportDummyMotion(): void {
const headerRow = [ const headerRow = [
'Identifier', 'Identifier',
'Submitters',
'Title', 'Title',
'Text', 'Text',
'Reason', 'Reason',
'Submitters',
'Category', 'Category',
'Tags', 'Tags',
'Origin', 'Motion block',
'Motion block' 'Origin'
]; ];
const rows = [ const rows = [
[ [
'A1', 'A1',
'Submitter A',
'Title 1', 'Title 1',
'Text 1', 'Text 1',
'Reason 1', 'Reason 1',
'Submitter A',
'Category A', 'Category A',
'Tag 1, Tag 2', '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'], ['B1', 'Submitter B', 'Title 2', 'Text 2', 'Reason 2', 'Category B', null, 'Block A', 'Origin B'],
[null, 'Title 3', 'Text 3', null, null, null, null, null, null] ['C2', null, 'Title 3', 'Text 3', null, null, null, null, null]
]; ];
this.csvExport.dummyCSVExport(headerRow, rows, `${this.translate.instant('motions-example')}.csv`); this.csvExport.dummyCSVExport(headerRow, rows, `${this.translate.instant('motions-example')}.csv`);
} }

View File

@ -201,7 +201,7 @@ export class MotionImportService extends BaseImportService<ViewMotion> {
} }
const submitterArray = submitterlist.split(','); // TODO fails with 'full name' const submitterArray = submitterlist.split(','); // TODO fails with 'full name'
for (const submitter of submitterArray) { for (const submitter of submitterArray) {
const existingSubmitters = this.userRepo.getUsersByName(submitter); const existingSubmitters = this.userRepo.getUsersByName(submitter.trim());
if (!existingSubmitters.length) { if (!existingSubmitters.length) {
if (!this.newSubmitters.find(listedSubmitter => listedSubmitter.name === submitter)) { if (!this.newSubmitters.find(listedSubmitter => listedSubmitter.name === submitter)) {
this.newSubmitters.push({ name: submitter }); this.newSubmitters.push({ name: submitter });