Skip to content

Commit

Permalink
Implement backend topic import (#2244)
Browse files Browse the repository at this point in the history
  • Loading branch information
luisa-beerboom authored Aug 11, 2023
1 parent 9a3fbdd commit a0a7b28
Show file tree
Hide file tree
Showing 23 changed files with 1,702 additions and 85 deletions.
2 changes: 1 addition & 1 deletion client/src/app/domain/models/agenda/agenda-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class AgendaItem extends BaseModel<AgendaItem> {
public type!: AgendaItemType;
public is_hidden!: boolean;
public is_internal!: boolean;
public duration!: number; // in seconds
public duration!: number; // in minutes
public weight!: number;
/**
* Client-calculated field: The level indicates the indentation of an agenda-item.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { CsvExportForBackendService } from './csv-export-for-backend.service';

xdescribe(`CsvExportForBackendService`, () => {
let service: CsvExportForBackendService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CsvExportForBackendService);
});

it(`should be created`, () => {
expect(service).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { Injectable } from '@angular/core';
import { BaseViewModel } from 'src/app/site/base/base-view-model';

import { ExportServiceModule } from '../export-service.module';
import { FileExportService } from '../file-export.service';
import {
CsvColumnsDefinition,
DEFAULT_COLUMN_SEPARATOR,
DEFAULT_ENCODING,
DEFAULT_LINE_SEPARATOR,
isMapDefinition,
ISO_8859_15_ENCODING,
isPropertyDefinition
} from './csv-export-utils';

@Injectable({
providedIn: ExportServiceModule
})
export class CsvExportForBackendService {
public constructor(private exporter: FileExportService) {}

/**
* Saves an array of model data to a CSV.
* @param models 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 configured option (',' , other values are ';', '\t' (tab), ' 'whitespace)
*/
public export<T extends BaseViewModel>(
models: T[],
columns: CsvColumnsDefinition<T>,
filename: string,
{
lineSeparator = DEFAULT_LINE_SEPARATOR,
columnSeparator = DEFAULT_COLUMN_SEPARATOR,
encoding = DEFAULT_ENCODING
}: {
lineSeparator?: string;
columnSeparator?: string;
encoding?: 'utf-8' | 'iso-8859-15';
} = {}
): void {
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

if (lineSeparator === columnSeparator) {
throw new Error(`lineseparator and columnseparator must differ from each other`);
}

// create header data
const header = columns.map(column => {
let label: string = ``;
if (isPropertyDefinition(column)) {
label = column.label ? column.label : (column.property as string);
} else if (isMapDefinition(column)) {
label = column.label;
}
return label;
});
csvContent.push(header);

// create lines
csvContent = csvContent.concat(
models.map(model =>
columns.map(column => {
let value: string = ``;

if (isPropertyDefinition(column)) {
const property: any = model[column.property];
if (typeof property === `number`) {
value = property.toString(10);
} else if (!property) {
value = ``;
} else if (property === true) {
value = `1`;
} else if (property === false) {
value = `0`;
} else if (typeof property === `function`) {
const bindedFn = property.bind(model); // bind model to access 'this'
value = bindedFn()?.toString();
} else {
value = property.toString();
}
} else if (isMapDefinition(column)) {
value = column.map(model);
}
return this.checkCsvTextSafety(value);
})
)
);

const csvContentAsString: string = csvContent
.map((line: string[]) => line.join(columnSeparator))
.join(lineSeparator);
const filetype = `text/csv;charset=${encoding}`;
if (encoding === ISO_8859_15_ENCODING) {
this.exporter.saveFile(this.convertTo8859_15(csvContentAsString), filename, filetype);
} else {
this.exporter.saveFile(csvContentAsString, filename, filetype);
}
}

public dummyCSVExport<I>(headerAndVerboseNames: I, rows: I[], filename: string): void {
const separator = DEFAULT_COLUMN_SEPARATOR;
const encoding: `utf-8` | `iso-8859-15` = DEFAULT_ENCODING as any;
const headerRow = [Object.keys(headerAndVerboseNames).join(separator)];
const content = rows.map(row =>
Object.keys(headerAndVerboseNames)
.map(key => {
let value = row[key as keyof I] || ``;
if (typeof value === `number`) {
value = value.toString(10);
} else if (typeof value === `boolean`) {
value = value ? `1` : `0`;
}
return this.checkCsvTextSafety(value as string);
})
.join(separator)
);
const csv = headerRow.concat(content).join(`\r\n`);
const toExport = encoding === ISO_8859_15_ENCODING ? this.convertTo8859_15(csv) : csv;
this.exporter.saveFile(toExport, filename, `text/csv`);
}

/**
* Ensures, that the given string has escaped double quotes
* and no linebreak. The string itself will also be escaped by `double quotes`.
*
* @param {string} input any input to be sent to CSV
* @returns {string} the cleaned string.
*/
public checkCsvTextSafety(input: string): string {
if (!input) {
return ``;
}
return `"` + input.replace(/"/g, `""`).replace(/(\r\n\t|\n|\r\t)/gm, ``) + `"`;
}

/**
* get an iso-8859-15 - compatible blob part
*
* @param data
* @returns a Blob part
*/
private convertTo8859_15(data: string): BlobPart {
const array = new Uint8Array(new ArrayBuffer(data.length));
for (let i = 0; i < data.length; i++) {
array[i] = data.charCodeAt(i);
}
return array;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { Injectable } from '@angular/core';
import { Identifiable } from 'src/app/domain/interfaces';
import { Topic } from 'src/app/domain/models/topics/topic';
import { ViewAgendaItem, ViewTopic } from 'src/app/site/pages/meetings/pages/agenda';
import { BackendImportRawPreview } from 'src/app/ui/modules/import-list/definitions/backend-import-preview';

import { Action } from '../../actions';
import { createAgendaItem } from '../agenda';
import { AgendaItemRepositoryService } from '../agenda/agenda-item-repository.service';
import { BaseAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-agenda-item-and-list-of-speakers-content-object-repository';
Expand Down Expand Up @@ -40,6 +42,14 @@ export class TopicRepositoryService extends BaseAgendaItemAndListOfSpeakersConte
return this.sendBulkActionToBackend(TopicAction.DELETE, payload);
}

public jsonUpload(payload: { [key: string]: any }): Action<BackendImportRawPreview> {
return this.createAction<BackendImportRawPreview>(TopicAction.JSON_UPLOAD, payload);
}

public import(payload: { id: number; import: boolean }[]): Action<BackendImportRawPreview | void> {
return this.createAction<BackendImportRawPreview | void>(TopicAction.IMPORT, payload);
}

public getTitle = (topic: ViewTopic) => topic.title;

public override getListTitle = (topic: ViewTopic) => {
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/gateways/repositories/topics/topic.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ export class TopicAction {
public static readonly CREATE = `topic.create`;
public static readonly UPDATE = `topic.update`;
public static readonly DELETE = `topic.delete`;
public static readonly JSON_UPLOAD = `topic.json_upload`;
public static readonly IMPORT = `topic.import`;
}
Loading

0 comments on commit a0a7b28

Please sign in to comment.