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
// 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<string>('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)
);

View File

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

View File

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

View File

@ -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');

View File

@ -320,11 +320,18 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> 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);
}

View File

@ -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<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');
}
@ -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`);
}

View File

@ -201,7 +201,7 @@ export class MotionImportService extends BaseImportService<ViewMotion> {
}
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 });