Skip to content

Commit

Permalink
feat(export): export entity references both as id and as human-readab…
Browse files Browse the repository at this point in the history
…le names (#2295)

closes #2151
  • Loading branch information
christophscheuing authored Apr 12, 2024
1 parent 160ad67 commit 0abd547
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 23 deletions.
5 changes: 0 additions & 5 deletions src/app/core/config/config-fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,11 +714,6 @@ export const defaultJsonConfig = {
"title",
"type",
"assignedTo"
],
"exportConfig": [
{ "label": "Title", "query": "title" },
{ "label": "Type", "query": "type" },
{ "label": "Assigned users", "query": "assignedTo" }
]
}
},
Expand Down
74 changes: 73 additions & 1 deletion src/app/core/export/download-service/download.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,34 @@ import { Entity } from "../../entity/model/entity";
import { ConfigurableEnumValue } from "../../basic-datatypes/configurable-enum/configurable-enum.interface";
import { DatabaseField } from "../../entity/database-field.decorator";
import moment from "moment";
import { EntityMapperService } from "app/core/entity/entity-mapper/entity-mapper.service";
import { School } from "app/child-dev-project/schools/model/school";
import { Child } from "app/child-dev-project/children/model/child";
import { mockEntityMapper } from "app/core/entity/entity-mapper/mock-entity-mapper-service";

describe("DownloadService", () => {
let service: DownloadService;
let mockDataTransformationService: jasmine.SpyObj<DownloadService>;
let mockedEntityMapper;
const testSchool = School.create({ name: "Test School" });
const testChild = Child.create("Test Child");

beforeEach(() => {
mockDataTransformationService = jasmine.createSpyObj([
"queryAndTransformData",
]);
mockedEntityMapper = mockEntityMapper([testSchool, testChild]);
TestBed.configureTestingModule({
providers: [
DownloadService,
{
provide: DataTransformationService,
useValue: mockDataTransformationService,
},
{
provide: EntityMapperService,
useValue: mockedEntityMapper,
},
LoggingService,
],
});
Expand Down Expand Up @@ -78,7 +90,7 @@ describe("DownloadService", () => {
'"_id","_rev","propOne","propTwo"' +
DownloadService.SEPARATOR_ROW +
'"TestForCsvEntity:1","2","first","second"';
spyOn(service, "exportFile").and.returnValue(expected);
spyOn(service, "exportFile").and.resolveTo(expected);
const result = await service.createCsv([test]);
expect(result).toEqual(expected);
});
Expand Down Expand Up @@ -114,6 +126,66 @@ describe("DownloadService", () => {
expect(columnValues).toContain('"true"');
});

it("should add columns with entity toString for referenced entities in export", async () => {
class EntityRefDownloadTestEntity extends Entity {
@DatabaseField({ dataType: "entity", label: "referenced entity" })
relatedEntity: string;
@DatabaseField({ dataType: "entity", label: "referenced entity 2" })
relatedEntity2: string;
}
const relatedEntity = testSchool;
const relatedEntity2 = testChild;

const testEntity = new EntityRefDownloadTestEntity();
testEntity.relatedEntity = relatedEntity.getId();
testEntity.relatedEntity2 = relatedEntity2.getId();

const csvExport = await service.createCsv([testEntity]);

const rows = csvExport.split(DownloadService.SEPARATOR_ROW);
expect(rows).toHaveSize(1 + 1); // includes 1 header line
const columnValues = rows[1].split(DownloadService.SEPARATOR_COL);
expect(columnValues).toHaveSize(4);
expect(columnValues).toContain('"' + relatedEntity.getId() + '"');
expect(columnValues).toContain('"' + relatedEntity.toString() + '"');
expect(columnValues).toContain('"' + relatedEntity2.getId() + '"');
expect(columnValues).toContain('"' + relatedEntity2.toString() + '"');
});

it("should add column with entity toString for referenced array of entities in export", async () => {
class EntityRefDownloadTestEntity extends Entity {
@DatabaseField({ dataType: "entity-array", label: "referenced entities" })
relatedEntitiesArray: string[];
}
const testEntity = new EntityRefDownloadTestEntity();
testEntity.relatedEntitiesArray = [testSchool.getId(), testChild.getId()];

const csvExport = await service.createCsv([testEntity]);

const rows = csvExport.split(DownloadService.SEPARATOR_ROW);
expect(rows).toHaveSize(1 + 1); // includes 1 header line
expect(rows[1]).toBe(
`"${testSchool.getId()},${testChild.getId()}","${testSchool.toString()},${testChild.toString()}"`,
);
});

it("should handle undefined entity ids without errors", async () => {
class EntityRefDownloadTestEntity extends Entity {
@DatabaseField({ dataType: "entity-array", label: "referenced entities" })
relatedEntitiesArray: string[];
}
const testEntity = new EntityRefDownloadTestEntity();
testEntity.relatedEntitiesArray = ["undefined-id", testChild.getId()];

const csvExport = await service.createCsv([testEntity]);

const rows = csvExport.split(DownloadService.SEPARATOR_ROW);
expect(rows).toHaveSize(1 + 1); // includes 1 header line
expect(rows[1]).toBe(
`"undefined-id,${testChild.getId()}","<not_found>,${testChild.toString()}"`,
);
});

it("should export all properties using object keys as headers, if no schema is available", async () => {
const docs = [
{ _id: "Test:1", name: "Child 1" },
Expand Down
80 changes: 63 additions & 17 deletions src/app/core/export/download-service/download.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { LoggingService } from "../../logging/logging.service";
import { DataTransformationService } from "../data-transformation-service/data-transformation.service";
import { transformToReadableFormat } from "../../common-components/entities-table/value-accessor/value-accessor";
import { Papa } from "ngx-papaparse";
import { EntitySchemaField } from "app/core/entity/schema/entity-schema-field";
import { Entity, EntityConstructor } from "app/core/entity/model/entity";
import { EntityDatatype } from "app/core/basic-datatypes/entity/entity.datatype";
import { EntityArrayDatatype } from "app/core/basic-datatypes/entity-array/entity-array.datatype";
import { EntityMapperService } from "app/core/entity/entity-mapper/entity-mapper.service";

/**
* This service allows to start a download process from the browser.
Expand All @@ -22,6 +25,7 @@ export class DownloadService {
private dataTransformationService: DataTransformationService,
private papa: Papa,
private loggingService: LoggingService,
private entityMapperService: EntityMapperService,
) {}

/**
Expand Down Expand Up @@ -110,27 +114,33 @@ export class DownloadService {
});
}

const result = this.exportFile(data, entityConstructor);
const result = await this.exportFile(data, entityConstructor);
return result;
}

exportFile(data: any[], entityConstructor: { schema: any }) {
async exportFile(data: any[], entityConstructor: EntityConstructor) {
const entitySchema = entityConstructor.schema;
const columnLabels = new Map<string, EntitySchemaField>();

entitySchema.forEach((value: { label: EntitySchemaField }, key: string) => {
if (value.label) columnLabels.set(key, value.label);
});

const exportEntities = data.map((item) => {
let newItem = {};
for (const key in item) {
if (columnLabels.has(key)) {
newItem[key] = item[key];
}
const columnLabels = new Map<string, string>();

for (const [id, field] of entitySchema.entries()) {
if (!field.label) {
// skip "technical" fields without an explicit label
continue;
}
return newItem;
});

columnLabels.set(id, field.label);

if (
field.dataType === EntityDatatype.dataType ||
field.dataType === EntityArrayDatatype.dataType
) {
columnLabels.set(id + "_readable", field.label + " (readable)");
}
}

const exportEntities = await Promise.all(
data.map((item) => this.mapEntityToExportRow(item, columnLabels)),
);

const columnKeys: string[] = Array.from(columnLabels.keys());
const labels: any[] = Array.from(columnLabels.values());
Expand All @@ -149,4 +159,40 @@ export class DownloadService {
},
);
}

private async mapEntityToExportRow(
item: Entity,
columnLabels: Map<string, string>,
): Promise<Object> {
const newItem = {};
for (const key in item) {
if (columnLabels.has(key)) {
newItem[key] = item[key];
}

if (columnLabels.has(key + "_readable")) {
newItem[key + "_readable"] = await this.loadRelatedEntitiesToString(
item[key],
);
}
}
return newItem;
}

private async loadRelatedEntitiesToString(value: string | string[]) {
const relatedEntitiesToStrings: string[] = [];

const relatedEntitiesIds: string[] = Array.isArray(value) ? value : [value];
for (const relatedEntityId of relatedEntitiesIds) {
relatedEntitiesToStrings.push(
(
await this.entityMapperService
.load(Entity.extractTypeFromId(relatedEntityId), relatedEntityId)
.catch((e) => "<not_found>")
).toString(),
);
}

return relatedEntitiesToStrings;
}
}

0 comments on commit 0abd547

Please sign in to comment.