diff --git a/ansible/roles/common-cartridge/tasks/main.yml b/ansible/roles/common-cartridge/tasks/main.yml index a4d6e99575e..6771d9f73f8 100644 --- a/ansible/roles/common-cartridge/tasks/main.yml +++ b/ansible/roles/common-cartridge/tasks/main.yml @@ -52,11 +52,11 @@ - service # This is a testing route and will not be deployed -# - name: Ingress -# kubernetes.core.k8s: -# kubeconfig: ~/.kube/config -# namespace: "{{ NAMESPACE }}" -# template: ingress.yml.j2 -# when: WITH_COMMON_CARTRIDGE is defined and WITH_COMMON_CARTRIDGE|bool -# tags: -# - ingress +- name: Ingress + kubernetes.core.k8s: + kubeconfig: ~/.kube/config + namespace: "{{ NAMESPACE }}" + template: ingress.yml.j2 + when: WITH_COMMON_CARTRIDGE is defined and WITH_COMMON_CARTRIDGE|bool + tags: + - ingress diff --git a/apps/server/src/core/error/domain/domain-error-handler.spec.ts b/apps/server/src/core/error/domain/domain-error-handler.spec.ts index 4edf8b5d167..f67ee755384 100644 --- a/apps/server/src/core/error/domain/domain-error-handler.spec.ts +++ b/apps/server/src/core/error/domain/domain-error-handler.spec.ts @@ -4,8 +4,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BusinessError } from '@shared/common'; import { ErrorLogger, ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; import util from 'util'; +import { AxiosError } from 'axios'; import { ErrorLoggable } from '../loggable/error.loggable'; import { ErrorUtils } from '../utils'; +import { AxiosErrorLoggable } from '../loggable'; import { DomainErrorHandler } from './domain-error-handler'; class SampleLoggableException extends BadRequestException implements Loggable { @@ -201,5 +203,22 @@ describe('GlobalErrorFilter', () => { expect(logger.error).toBeCalledWith(loggable); }); }); + + describe('when error is a axios error', () => { + const setup = () => { + const error = new AxiosError('test'); + const axiosLoggable = new AxiosErrorLoggable(error, 'AXIOS_REQUEST_ERROR'); + + return { axiosLoggable }; + }; + + it('should call logger with axios error', () => { + const { axiosLoggable } = setup(); + + domainErrorHandler.exec(axiosLoggable); + + expect(logger.error).toBeCalledWith(axiosLoggable); + }); + }); }); }); diff --git a/apps/server/src/core/error/filter/global-error.filter.spec.ts b/apps/server/src/core/error/filter/global-error.filter.spec.ts index 31ce2b9f04f..4e99f0ca396 100644 --- a/apps/server/src/core/error/filter/global-error.filter.spec.ts +++ b/apps/server/src/core/error/filter/global-error.filter.spec.ts @@ -7,6 +7,7 @@ import { WsException } from '@nestjs/websockets'; import { BusinessError } from '@shared/common'; import { ErrorLogMessage, Loggable } from '@src/core/logger'; import { Response } from 'express'; +import { AxiosError } from 'axios'; import { DomainErrorHandler } from '../domain'; import { ErrorResponse } from '../dto'; import { ErrorUtils } from '../utils'; @@ -102,6 +103,26 @@ describe('GlobalErrorFilter', () => { }); }); + describe('given context is axios', () => { + const setup = () => { + const argumentsHost = createMock(); + argumentsHost.getType.mockReturnValueOnce(UseableContextType.http); + + const error = new AxiosError('test'); + + return { error, argumentsHost }; + }; + + it('should call exec on domain error handler', () => { + const { error, argumentsHost } = setup(); + + service.catch(error, argumentsHost); + + expect(domainErrorHandler.execHttpContext).toBeCalledWith(error, {}); + expect(domainErrorHandler.execHttpContext).toBeCalledTimes(1); + }); + }); + describe('given context is http', () => { const mockHttpArgumentsHost = () => { const argumentsHost = createMock(); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/card-list-response.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/card-list-response.dto.ts index ce011649d1b..0d71a967efa 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/card-list-response.dto.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/card-list-response.dto.ts @@ -1,7 +1,7 @@ import { CardResponseDto } from './card-response.dto'; export class CardListResponseDto { - data: CardResponseDto[]; + public data: CardResponseDto[]; constructor(data: CardResponseDto[]) { this.data = data; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/card-response.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/card-response.dto.ts index 2139dfcbd1e..c08d4f134ad 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/card-response.dto.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/card-response.dto.ts @@ -3,31 +3,24 @@ import { TimestampResponseDto } from './timestamp-response.dto'; import { VisibilitySettingsResponseDto } from './visibility-settings-response.dto'; export class CardResponseDto { - id: string; + public id: string; - title?: string; + public title?: string; - height: number; + public height: number; - elements: Array; + public elements: Array; - visibilitySettings: VisibilitySettingsResponseDto; + public visibilitySettings: VisibilitySettingsResponseDto; - timeStamps: TimestampResponseDto; + public timeStamps: TimestampResponseDto; - constructor( - id: string, - title: string, - height: number, - elements: CardResponseElementsInnerDto[], - visibilitySettings: VisibilitySettingsResponseDto, - timestamps: TimestampResponseDto - ) { - this.id = id; - this.title = title; - this.height = height; - this.elements = elements; - this.visibilitySettings = visibilitySettings; - this.timeStamps = timestamps; + constructor(props: Readonly) { + this.id = props.id; + this.title = props.title; + this.height = props.height; + this.elements = props.elements; + this.visibilitySettings = props.visibilitySettings; + this.timeStamps = props.timeStamps; } } diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/collaborative-text-editor-element-response.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/collaborative-text-editor-element-response.dto.ts index 7d864480759..b950d0f383d 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/collaborative-text-editor-element-response.dto.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/collaborative-text-editor-element-response.dto.ts @@ -2,13 +2,13 @@ import { ContentElementType } from '../cards-api-client'; import { TimestampResponseDto } from './timestamp-response.dto'; export class CollaborativeTextEditorElementResponseDto { - id: string; + public id: string; - type: ContentElementType; + public type: ContentElementType; - timestamps: TimestampResponseDto; + public timestamps: TimestampResponseDto; - content: object; + public content: object; constructor(id: string, type: ContentElementType, content: object, timestamps: TimestampResponseDto) { this.id = id; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/index.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/index.ts new file mode 100644 index 00000000000..64c051f06c7 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/index.ts @@ -0,0 +1,8 @@ +export { RichTextElementResponseDto } from './rich-text-element-response.dto'; +export { RichTextElementContentDto } from './rich-text-element-content.dto'; +export { LinkElementContentDto } from './link-element-content.dto'; +export { LinkElementResponseDto } from './link-element-response.dto'; +export { CardResponseDto } from './card-response.dto'; +export { CardListResponseDto } from './card-list-response.dto'; +export { TimestampResponseDto } from './timestamp-response.dto'; +export { VisibilitySettingsResponseDto } from './visibility-settings-response.dto'; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/link-element-content.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/link-element-content.dto.ts index 6b257248f30..1783f4b8def 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/link-element-content.dto.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/link-element-content.dto.ts @@ -1,16 +1,16 @@ export class LinkElementContentDto { - url: string; + public url: string; - title: string; + public title: string; - description?: string; + public description?: string; - imageUrl?: string; + public imageUrl?: string; - constructor(url: string, title: string, description: string, imageUrl: string) { - this.url = url; - this.title = title; - this.description = description; - this.imageUrl = imageUrl; + constructor(props: Readonly) { + this.url = props.url; + this.title = props.title; + this.description = props.description; + this.imageUrl = props.imageUrl; } } diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/link-element-response.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/link-element-response.dto.ts index 4c4fa40a48a..1e11e34450a 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/link-element-response.dto.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/link-element-response.dto.ts @@ -3,18 +3,22 @@ import { LinkElementContentDto } from './link-element-content.dto'; import { TimestampResponseDto } from './timestamp-response.dto'; export class LinkElementResponseDto { - id: string; + public id: string; - type: ContentElementType; + public type: ContentElementType; - content: LinkElementContentDto; + public content: LinkElementContentDto; - timestamps: TimestampResponseDto; + public timestamps: TimestampResponseDto; - constructor(id: string, type: ContentElementType, content: LinkElementContentDto, timestamps: TimestampResponseDto) { - this.id = id; - this.type = type; - this.content = content; - this.timestamps = timestamps; + constructor(props: LinkElementResponseDto) { + this.id = props.id; + this.type = props.type; + this.content = props.content; + this.timestamps = props.timestamps; + } + + public static isLinkElement(reference: unknown): reference is LinkElementResponseDto { + return reference instanceof LinkElementResponseDto; } } diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/rich-text-element-content.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/rich-text-element-content.dto.ts index 7726852b1dd..b5649341f87 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/rich-text-element-content.dto.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/rich-text-element-content.dto.ts @@ -1,7 +1,7 @@ export class RichTextElementContentDto { - text: string; + public text: string; - inputFormat: string; + public inputFormat: string; constructor(text: string, inputFormat: string) { this.text = text; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/rich-text-element-response.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/rich-text-element-response.dto.ts index b8e5c3811b5..162575ade32 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/rich-text-element-response.dto.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/rich-text-element-response.dto.ts @@ -3,23 +3,22 @@ import { RichTextElementContentDto } from './rich-text-element-content.dto'; import { TimestampResponseDto } from './timestamp-response.dto'; export class RichTextElementResponseDto { - id: string; + public id: string; - type: ContentElementType; + public type: ContentElementType; - content: RichTextElementContentDto; + public content: RichTextElementContentDto; - timestamps: TimestampResponseDto; + public timestamps: TimestampResponseDto; - constructor( - id: string, - type: ContentElementType, - content: RichTextElementContentDto, - timestamps: TimestampResponseDto - ) { - this.id = id; - this.type = type; - this.content = content; - this.timestamps = timestamps; + constructor(props: Readonly) { + this.id = props.id; + this.type = props.type; + this.content = props.content; + this.timestamps = props.timestamps; + } + + public static isRichTextElement(reference: unknown): reference is RichTextElementResponseDto { + return reference instanceof RichTextElementResponseDto; } } diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/timestamp-response.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/timestamp-response.dto.ts index a75998154b1..dcf83d2e696 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/timestamp-response.dto.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/dto/timestamp-response.dto.ts @@ -1,13 +1,13 @@ export class TimestampResponseDto { - lastUpdatedAt: string; + public lastUpdatedAt: string; - createdAt: string; + public createdAt: string; - deletedAt?: string; + public deletedAt?: string; - constructor(lastUpdatedAt: string, createdAt: string, deletedAt: string) { - this.lastUpdatedAt = lastUpdatedAt; - this.createdAt = createdAt; - this.deletedAt = deletedAt; + constructor(props: Readonly) { + this.lastUpdatedAt = props.lastUpdatedAt; + this.createdAt = props.createdAt; + this.deletedAt = props.deletedAt; } } diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/mapper/card-response.mapper.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/mapper/card-response.mapper.spec.ts index e99cbecbc24..b7662e27f22 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/mapper/card-response.mapper.spec.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/mapper/card-response.mapper.spec.ts @@ -178,17 +178,5 @@ describe('CardResponseMapper', () => { expect(cardResponse.visibilitySettings.publishedAt).toBe(''); }); }); - - describe('when deletedAt in TimestampsResponse is null', () => { - const mockList: CardListResponse = setup([]); - mockList.data[0].timestamps.deletedAt = undefined; - - it('should return an empty string', () => { - const mapperResult = CardResponseMapper.mapToCardListResponseDto(mockList); - const cardResponse: CardResponseDto = mapperResult.data[0]; - - expect(cardResponse.timeStamps.deletedAt).toBe(''); - }); - }); }); }); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/mapper/card-response.mapper.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/mapper/card-response.mapper.ts index cf8b228cb9f..da13bb72a9d 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/mapper/card-response.mapper.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/card-client/mapper/card-response.mapper.ts @@ -48,14 +48,14 @@ export class CardResponseMapper { } private static mapToCardResponseDto(cardResponse: CardResponse): CardResponseDto { - return new CardResponseDto( - cardResponse.id, - cardResponse.title ?? '', - cardResponse.height, - this.mapToCardResponseElementsInnerDto(cardResponse.elements), - this.mapToVisibilitySettingsDto(cardResponse.visibilitySettings), - this.mapToTimestampDto(cardResponse.timestamps) - ); + return new CardResponseDto({ + id: cardResponse.id, + title: cardResponse.title, + height: cardResponse.height, + elements: this.mapToCardResponseElementsInnerDto(cardResponse.elements), + visibilitySettings: this.mapToVisibilitySettingsDto(cardResponse.visibilitySettings), + timeStamps: this.mapToTimestampDto(cardResponse.timestamps), + }); } private static mapToCardResponseElementsInnerDto( @@ -138,24 +138,28 @@ export class CardResponseMapper { case ContentElementType.LINK: { const content: LinkElementContent = element.content as LinkElementContent; elements.push( - new LinkElementResponseDto( - element.id, - ContentElementType.LINK, - new LinkElementContentDto(content.url, content.title, content.description ?? '', content.imageUrl ?? ''), - this.mapToTimestampDto(element.timestamps) - ) + new LinkElementResponseDto({ + id: element.id, + type: ContentElementType.LINK, + content: new LinkElementContentDto({ + url: content.url, + title: content.title, + description: content.description, + }), + timestamps: this.mapToTimestampDto(element.timestamps), + }) ); break; } case ContentElementType.RICH_TEXT: { const content: RichTextElementContent = element.content as RichTextElementContent; elements.push( - new RichTextElementResponseDto( - element.id, - ContentElementType.RICH_TEXT, - new RichTextElementContentDto(content.text, content.inputFormat), - this.mapToTimestampDto(element.timestamps) - ) + new RichTextElementResponseDto({ + id: element.id, + type: ContentElementType.RICH_TEXT, + content: new RichTextElementContentDto(content.text, content.inputFormat), + timestamps: this.mapToTimestampDto(element.timestamps), + }) ); break; } @@ -185,6 +189,10 @@ export class CardResponseMapper { } private static mapToTimestampDto(timestamp: TimestampsResponse): TimestampResponseDto { - return new TimestampResponseDto(timestamp.lastUpdatedAt, timestamp.createdAt, timestamp.deletedAt ?? ''); + return new TimestampResponseDto({ + lastUpdatedAt: timestamp.lastUpdatedAt, + createdAt: timestamp.createdAt, + deletedAt: timestamp.deletedAt, + }); } } diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.spec.ts index 058a7517f98..abd0d86d4a7 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.spec.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.spec.ts @@ -74,7 +74,7 @@ describe(CoursesClientAdapter.name, () => { expect(coursesApi.courseControllerGetCourseCcMetadataById).toHaveBeenCalledWith(courseId, expectedOptions); expect(result.id).toBeDefined(); - expect(result.title).toBeDefined(); + expect(result.courseName).toBeDefined(); expect(result.creationDate).toBeDefined(); expect(result.copyRightOwners).toBeDefined(); }); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.ts index 324f907f06f..5b6955bb36b 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.ts @@ -15,7 +15,7 @@ export class CoursesClientAdapter { const response = await this.coursesApi.courseControllerGetCourseCcMetadataById(courseId, options); const courseCommonCartridgeMetadata: CourseCommonCartridgeMetadataDto = new CourseCommonCartridgeMetadataDto({ id: response.data.id, - title: response.data.title, + courseName: response.data.title, creationDate: response.data.creationDate, copyRightOwners: response.data.copyRightOwners, }); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/dto/course-common-cartridge-metadata.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/dto/course-common-cartridge-metadata.dto.ts index 117963823ca..179bb62e7b4 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/dto/course-common-cartridge-metadata.dto.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/dto/course-common-cartridge-metadata.dto.ts @@ -1,7 +1,7 @@ export class CourseCommonCartridgeMetadataDto { id: string; - title: string; + courseName: string; creationDate?: string; @@ -9,7 +9,7 @@ export class CourseCommonCartridgeMetadataDto { constructor(props: CourseCommonCartridgeMetadataDto) { this.id = props.id; - this.title = props.title; + this.courseName = props.courseName; this.creationDate = props.creationDate; this.copyRightOwners = props.copyRightOwners; } diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-etherpad-props.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-etherpad-props.dto.ts new file mode 100644 index 00000000000..085dcb12af5 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-etherpad-props.dto.ts @@ -0,0 +1,13 @@ +export class ComponentEtherpadPropsDto { + public description: string; + + public title: string; + + public url: string; + + constructor(props: Readonly) { + this.description = props.description; + this.title = props.title; + this.url = props.url; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-geogebra-props.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-geogebra-props.dto.ts new file mode 100644 index 00000000000..2688370b192 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-geogebra-props.dto.ts @@ -0,0 +1,9 @@ +import { ComponentGeogebraPropsImpl } from '../lessons-api-client'; + +export class ComponentGeogebraPropsDto { + public materialId: string; + + constructor(geogebraContent: ComponentGeogebraPropsImpl) { + this.materialId = geogebraContent.materialId; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-internal-props.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-internal-props.dto.ts new file mode 100644 index 00000000000..d896caeee76 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-internal-props.dto.ts @@ -0,0 +1,9 @@ +import { ComponentInternalPropsImpl } from '../lessons-api-client'; + +export class ComponentInternalPropsDto { + public url: string; + + constructor(internalContent: ComponentInternalPropsImpl) { + this.url = internalContent.url; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-lernstore-props.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-lernstore-props.dto.ts new file mode 100644 index 00000000000..10895739f98 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-lernstore-props.dto.ts @@ -0,0 +1,9 @@ +import { ComponentLernstorePropsImpl } from '../lessons-api-client'; + +export class ComponentLernstorePropsDto { + public resources: string[]; + + constructor(lernstoreContent: ComponentLernstorePropsImpl) { + this.resources = lernstoreContent.resources; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-nexboard-props-dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-nexboard-props-dto.ts new file mode 100644 index 00000000000..7115667cffc --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-nexboard-props-dto.ts @@ -0,0 +1,18 @@ +import { ComponentNexboardPropsImpl } from '../lessons-api-client'; + +export class ComponentNexboardPropsDto { + public board: string; + + public description: string; + + public title: string; + + public url: string; + + constructor(nexboardContent: ComponentNexboardPropsImpl) { + this.board = nexboardContent.board; + this.description = nexboardContent.description; + this.title = nexboardContent.title; + this.url = nexboardContent.url; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-text-props.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-text-props.dto.ts new file mode 100644 index 00000000000..8e5eb7f8493 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/component-text-props.dto.ts @@ -0,0 +1,9 @@ +import { ComponentTextPropsImpl } from '../lessons-api-client'; + +export class ComponentTextPropsDto { + public text: string; + + constructor(textContent: ComponentTextPropsImpl) { + this.text = textContent.text; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/lesson-content-response-inner.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/lesson-content-response-inner.dto.ts new file mode 100644 index 00000000000..bebfcd784eb --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/lesson-content-response-inner.dto.ts @@ -0,0 +1,14 @@ +import { ComponentEtherpadPropsDto } from './component-etherpad-props.dto'; +import { ComponentGeogebraPropsDto } from './component-geogebra-props.dto'; +import { ComponentInternalPropsDto } from './component-internal-props.dto'; +import { ComponentLernstorePropsDto } from './component-lernstore-props.dto'; +import { ComponentNexboardPropsDto } from './component-nexboard-props-dto'; +import { ComponentTextPropsDto } from './component-text-props.dto'; + +export type LessonContentResponseContentInnerDto = + | ComponentEtherpadPropsDto + | ComponentGeogebraPropsDto + | ComponentInternalPropsDto + | ComponentLernstorePropsDto + | ComponentTextPropsDto + | ComponentNexboardPropsDto; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/lesson-contents.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/lesson-contents.dto.ts index e9f00a471d4..78e5e97e5d3 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/lesson-contents.dto.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/lesson-contents.dto.ts @@ -1,11 +1,15 @@ +import { LessonContentResponseContentInnerDto } from './lesson-content-response-inner.dto'; + export class LessonContentDto { - content: object; + public id: string | undefined; + + public content: LessonContentResponseContentInnerDto; - title: string; + public title: string; - component: LessonContentDtoComponent; + public component: LessonContentDtoComponent; - hidden: boolean; + public hidden: boolean; constructor(props: LessonContentDto) { this.content = props.content; @@ -22,6 +26,7 @@ export const LessonContentDtoComponentValues = { RESOURCES: 'resources', TEXT: 'text', NE_XBOARD: 'neXboard', + LERNSTORE: 'lernstore', } as const; export type LessonContentDtoComponent = diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/lesson.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/lesson.dto.ts index 864b6502f19..1385d6136e1 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/lesson.dto.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/dto/lesson.dto.ts @@ -1,22 +1,25 @@ import { LessonContentDto } from './lesson-contents.dto'; +import { LessonLinkedTaskDto } from './lesson-linked-task.dto'; import { LessonMaterialsDto } from './lesson-materials.dto'; export class LessonDto { - lessonId: string; + public lessonId: string; - name: string; + public name: string; - courseId?: string; + public courseId?: string; - courseGroupId?: string; + public courseGroupId?: string; - hidden: boolean; + public hidden: boolean; - position: number; + public position: number; - contents: LessonContentDto[]; + public contents: LessonContentDto[]; - materials: LessonMaterialsDto[]; + public materials: LessonMaterialsDto[]; + + public linkedTasks: LessonLinkedTaskDto[]; constructor(props: LessonDto) { this.lessonId = props.lessonId; @@ -27,5 +30,6 @@ export class LessonDto { this.position = props.position; this.contents = props.contents; this.materials = props.materials; + this.linkedTasks = props.linkedTasks; } } diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lesson-client.adapter.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lesson-client.adapter.spec.ts index fef7a941220..50590bb8f59 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lesson-client.adapter.spec.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lesson-client.adapter.spec.ts @@ -52,10 +52,26 @@ describe(LessonClientAdapter.name, () => { describe('getLessonById', () => { describe('When getLessonById is called', () => { const setup = () => { + const lessonId = faker.string.uuid(); + const linkedTasks = createMock>({ + data: [ + { + name: faker.lorem.sentence(), + description: faker.lorem.sentence(), + descriptionInputFormat: faker.helpers.arrayElement(['plainText', 'richTextCk4', 'richTextCk5Simple']), + availableDate: faker.date.recent().toString(), + dueDate: faker.date.future().toString(), + private: faker.datatype.boolean(), + publicSubmissions: faker.datatype.boolean(), + teamSubmissions: faker.datatype.boolean(), + }, + ], + }); + const response = createMock>({ data: { _id: faker.string.uuid(), - id: faker.string.uuid(), + id: lessonId, name: faker.lorem.sentence(), courseId: faker.string.uuid(), courseGroupId: faker.string.uuid(), @@ -86,6 +102,7 @@ describe(LessonClientAdapter.name, () => { }, }); + lessonApiMock.lessonControllerGetLessonTasks.mockResolvedValue(linkedTasks); lessonApiMock.lessonControllerGetLesson.mockResolvedValue(response); return { lessonId: response.data.id }; @@ -137,75 +154,4 @@ describe(LessonClientAdapter.name, () => { }); }); }); - - describe('getLessonTasks', () => { - describe('When getLessonTasks is called', () => { - const setup = () => { - const lessonId = faker.string.uuid(); - const response = createMock>({ - data: [ - { - name: faker.lorem.sentence(), - description: faker.lorem.sentence(), - descriptionInputFormat: faker.helpers.arrayElement(['plainText', 'richTextCk4', 'richTextCk5Simple']), - availableDate: faker.date.recent().toString(), - dueDate: faker.date.future().toString(), - private: faker.datatype.boolean(), - publicSubmissions: faker.datatype.boolean(), - teamSubmissions: faker.datatype.boolean(), - }, - ], - }); - - lessonApiMock.lessonControllerGetLessonTasks.mockResolvedValue(response); - - return { lessonId }; - }; - - it('should call lessonControllerGetLessonTasks', async () => { - const { lessonId } = setup(); - - await sut.getLessonTasks(lessonId); - - expect(lessonApiMock.lessonControllerGetLessonTasks).toHaveBeenCalled(); - }); - }); - - describe('When getLessonTasks is called with invalid id', () => { - const setup = () => { - const lessonResponseId = faker.string.uuid(); - - lessonApiMock.lessonControllerGetLessonTasks.mockRejectedValueOnce(new Error('error')); - - return { lessonResponseId }; - }; - - it('should throw an error', async () => { - const { lessonResponseId } = setup(); - - const result = sut.getLessonTasks(lessonResponseId); - - await expect(result).rejects.toThrowError('error'); - }); - }); - - describe('When no JWT token is found', () => { - const setup = () => { - const lessonResponseId = faker.string.uuid(); - const request = createMock({ - headers: {}, - }) as Request; - - const adapter: LessonClientAdapter = new LessonClientAdapter(lessonApiMock, request); - - return { lessonResponseId, adapter }; - }; - - it('should throw an UnauthorizedError', async () => { - const { lessonResponseId, adapter } = setup(); - - await expect(adapter.getLessonTasks(lessonResponseId)).rejects.toThrowError(UnauthorizedException); - }); - }); - }); }); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lesson-client.adapter.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lesson-client.adapter.ts index 22e8abf7eec..2f3930ad9f9 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lesson-client.adapter.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lesson-client.adapter.ts @@ -15,6 +15,7 @@ export class LessonClientAdapter { const options = this.createOptionParams(); const response = await this.lessonApi.lessonControllerGetLesson(lessonId, options); const lessonDto = LessonDtoMapper.mapToLessonDto(response.data); + lessonDto.linkedTasks = await this.getLessonTasks(lessonId); return lessonDto; } diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/.openapi-generator/FILES b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/.openapi-generator/FILES index 96db8953479..3cfa46d40b7 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/.openapi-generator/FILES +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/.openapi-generator/FILES @@ -8,7 +8,14 @@ common.ts configuration.ts git_push.sh index.ts +models/component-etherpad-props-impl.ts +models/component-geogebra-props-impl.ts +models/component-internal-props-impl.ts +models/component-lernstore-props-impl.ts +models/component-nexboard-props-impl.ts +models/component-text-props-impl.ts models/index.ts +models/lesson-content-response-content.ts models/lesson-content-response.ts models/lesson-linked-task-response.ts models/lesson-metadata-list-response.ts diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-etherpad-props-impl.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-etherpad-props-impl.ts new file mode 100644 index 00000000000..2c18cef729c --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-etherpad-props-impl.ts @@ -0,0 +1,42 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface ComponentEtherpadPropsImpl + */ +export interface ComponentEtherpadPropsImpl { + /** + * description of a Etherpad component + * @type {string} + * @memberof ComponentEtherpadPropsImpl + */ + 'description': string; + /** + * title of a Etherpad component + * @type {string} + * @memberof ComponentEtherpadPropsImpl + */ + 'title': string; + /** + * url of a Etherpad component + * @type {string} + * @memberof ComponentEtherpadPropsImpl + */ + 'url': string; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-geogebra-props-impl.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-geogebra-props-impl.ts new file mode 100644 index 00000000000..9f8f609c364 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-geogebra-props-impl.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface ComponentGeogebraPropsImpl + */ +export interface ComponentGeogebraPropsImpl { + /** + * materialId of a Geogebra component + * @type {string} + * @memberof ComponentGeogebraPropsImpl + */ + 'materialId': string; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-internal-props-impl.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-internal-props-impl.ts new file mode 100644 index 00000000000..97b65de6639 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-internal-props-impl.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface ComponentInternalPropsImpl + */ +export interface ComponentInternalPropsImpl { + /** + * url of a Internal component + * @type {string} + * @memberof ComponentInternalPropsImpl + */ + 'url': string; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-lernstore-props-impl.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-lernstore-props-impl.ts new file mode 100644 index 00000000000..b8f4ad7ec17 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-lernstore-props-impl.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface ComponentLernstorePropsImpl + */ +export interface ComponentLernstorePropsImpl { + /** + * resources of a Lernstore component + * @type {Array} + * @memberof ComponentLernstorePropsImpl + */ + 'resources': Array; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-nexboard-props-impl.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-nexboard-props-impl.ts new file mode 100644 index 00000000000..e12f6298288 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-nexboard-props-impl.ts @@ -0,0 +1,48 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface ComponentNexboardPropsImpl + */ +export interface ComponentNexboardPropsImpl { + /** + * board of a Nexboard component + * @type {string} + * @memberof ComponentNexboardPropsImpl + */ + 'board': string; + /** + * description of a Nexboard component + * @type {string} + * @memberof ComponentNexboardPropsImpl + */ + 'description': string; + /** + * title of a Nexboard component + * @type {string} + * @memberof ComponentNexboardPropsImpl + */ + 'title': string; + /** + * url of a Nexboard component + * @type {string} + * @memberof ComponentNexboardPropsImpl + */ + 'url': string; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-text-props-impl.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-text-props-impl.ts new file mode 100644 index 00000000000..6ee414db6d5 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/component-text-props-impl.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface ComponentTextPropsImpl + */ +export interface ComponentTextPropsImpl { + /** + * + * @type {string} + * @memberof ComponentTextPropsImpl + */ + 'text': string; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/index.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/index.ts index 9abd938430d..bced2458355 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/index.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/index.ts @@ -1,4 +1,11 @@ +export * from './component-etherpad-props-impl'; +export * from './component-geogebra-props-impl'; +export * from './component-internal-props-impl'; +export * from './component-lernstore-props-impl'; +export * from './component-nexboard-props-impl'; +export * from './component-text-props-impl'; export * from './lesson-content-response'; +export * from './lesson-content-response-content'; export * from './lesson-linked-task-response'; export * from './lesson-metadata-list-response'; export * from './lesson-metadata-response'; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/lesson-content-response-content.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/lesson-content-response-content.ts new file mode 100644 index 00000000000..87cf1155cf7 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/lesson-content-response-content.ts @@ -0,0 +1,41 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { ComponentEtherpadPropsImpl } from './component-etherpad-props-impl'; +// May contain unused imports in some cases +// @ts-ignore +import type { ComponentGeogebraPropsImpl } from './component-geogebra-props-impl'; +// May contain unused imports in some cases +// @ts-ignore +import type { ComponentInternalPropsImpl } from './component-internal-props-impl'; +// May contain unused imports in some cases +// @ts-ignore +import type { ComponentLernstorePropsImpl } from './component-lernstore-props-impl'; +// May contain unused imports in some cases +// @ts-ignore +import type { ComponentNexboardPropsImpl } from './component-nexboard-props-impl'; +// May contain unused imports in some cases +// @ts-ignore +import type { ComponentTextPropsImpl } from './component-text-props-impl'; + +/** + * @type LessonContentResponseContent + * @export + */ +export type LessonContentResponseContent = ComponentEtherpadPropsImpl | ComponentGeogebraPropsImpl | ComponentInternalPropsImpl | ComponentLernstorePropsImpl | ComponentNexboardPropsImpl | ComponentTextPropsImpl; + + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/lesson-content-response.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/lesson-content-response.ts index da12106638f..668e233e326 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/lesson-content-response.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/lessons-api-client/models/lesson-content-response.ts @@ -13,6 +13,9 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { LessonContentResponseContent } from './lesson-content-response-content'; /** * @@ -22,10 +25,10 @@ export interface LessonContentResponse { /** * - * @type {object} + * @type {LessonContentResponseContent} * @memberof LessonContentResponse */ - 'content': object; + 'content': LessonContentResponseContent; /** * The id of the Material entity * @type {string} @@ -65,7 +68,7 @@ export const LessonContentResponseComponent = { INTERNAL: 'internal', RESOURCES: 'resources', TEXT: 'text', - NE_XBOARD: 'neXboard' + NEX_BOARD: 'neXboard' } as const; export type LessonContentResponseComponent = typeof LessonContentResponseComponent[keyof typeof LessonContentResponseComponent]; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/mapper/lesson-dto.mapper.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/mapper/lesson-dto.mapper.spec.ts index 3b294527520..03402407a43 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/mapper/lesson-dto.mapper.spec.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/mapper/lesson-dto.mapper.spec.ts @@ -5,30 +5,78 @@ import { LessonResponse, LessonLinkedTaskResponse, LessonLinkedTaskResponseDescriptionInputFormat, + ComponentEtherpadPropsImpl, + ComponentGeogebraPropsImpl, + ComponentInternalPropsImpl, + ComponentTextPropsImpl, + ComponentLernstorePropsImpl, + ComponentNexboardPropsImpl, + LessonContentResponseComponent, } from '../lessons-api-client'; import { LessonDtoMapper } from './lesson-dto.mapper'; +import { LessonDto } from '../dto'; +import { ComponentGeogebraPropsDto } from '../dto/component-geogebra-props.dto'; +import { ComponentTextPropsDto } from '../dto/component-text-props.dto'; +import { ComponentInternalPropsDto } from '../dto/component-internal-props.dto'; +import { ComponentLernstorePropsDto } from '../dto/component-lernstore-props.dto'; +import { ComponentNexboardPropsDto } from '../dto/component-nexboard-props-dto'; describe('LessonDtoMapper', () => { describe('mapToLessonDto', () => { - describe('when mapping to LessonResponse', () => { + const materialResponse: MaterialResponse = { + _id: faker.string.uuid(), + id: faker.string.uuid(), + title: faker.lorem.sentence(), + relatedResources: [faker.lorem.sentence()], + url: faker.internet.url(), + client: faker.lorem.sentence(), + license: [faker.lorem.sentence()], + merlinReference: faker.lorem.sentence(), + }; + + describe('when mapping LessonResponse to lesson DTO with etherpad contnet', () => { const setup = () => { - const materialResponse: MaterialResponse = { + const lessonContentResponse: LessonContentResponse = { + content: { title: faker.lorem.sentence() } as ComponentEtherpadPropsImpl, _id: faker.string.uuid(), id: faker.string.uuid(), title: faker.lorem.sentence(), - relatedResources: [faker.lorem.sentence()], - url: faker.internet.url(), - client: faker.lorem.sentence(), - license: [faker.lorem.sentence()], - merlinReference: faker.lorem.sentence(), + component: faker.helpers.arrayElement(['Etherpad']), + hidden: faker.datatype.boolean(), + }; + + const lessonResponse: LessonResponse = { + _id: faker.string.uuid(), + id: faker.string.uuid(), + name: faker.lorem.sentence(), + courseId: faker.string.uuid(), + courseGroupId: faker.string.uuid(), + hidden: faker.datatype.boolean(), + position: faker.number.int(), + contents: [lessonContentResponse], + materials: [materialResponse], }; + return { lessonResponse, lessonContentResponse }; + }; + + it('should return LessonDto with etherpad content', () => { + const { lessonResponse } = setup(); + + const result = LessonDtoMapper.mapToLessonDto(lessonResponse); + + expect(result).toBeInstanceOf(LessonDto); + }); + }); + + describe('when mapping LessonResponse to lesson DTO with GeoGebra content', () => { + const setup = () => { const lessonContentResponse: LessonContentResponse = { - content: { text: faker.lorem.sentence() }, + content: { materialId: faker.string.uuid() } as ComponentGeogebraPropsImpl, _id: faker.string.uuid(), id: faker.string.uuid(), title: faker.lorem.sentence(), - component: faker.helpers.arrayElement(['Etherpad', 'neXboard', 'geoGebra']), + component: faker.helpers.arrayElement(['geoGebra']), hidden: faker.datatype.boolean(), }; @@ -44,41 +92,210 @@ describe('LessonDtoMapper', () => { materials: [materialResponse], }; - return { lessonResponse }; + return { lessonResponse, lessonContentResponse }; }; - it('should return LessonDto', () => { + it('should return LessonDto with GeoGebra content', () => { const { lessonResponse } = setup(); const result = LessonDtoMapper.mapToLessonDto(lessonResponse); - expect(result).toEqual({ - lessonId: lessonResponse.id, - name: lessonResponse.name, - courseId: lessonResponse.courseId, - courseGroupId: lessonResponse.courseGroupId, - hidden: lessonResponse.hidden, - position: lessonResponse.position, - contents: [ - { - content: lessonResponse.contents[0].content, - title: lessonResponse.contents[0].title, - component: lessonResponse.contents[0].component, - hidden: lessonResponse.contents[0].hidden, - }, - ], - materials: [ - { - materialsId: lessonResponse.materials[0].id, - title: lessonResponse.materials[0].title, - relatedResources: [lessonResponse.materials[0].relatedResources[0]], - url: lessonResponse.materials[0].url, - client: lessonResponse.materials[0].client, - license: lessonResponse.materials[0].license, - merlinReference: lessonResponse.materials[0].merlinReference, - }, - ], - }); + expect(result).toBeInstanceOf(LessonDto); + expect(result.contents[0].component).toEqual('geoGebra'); + expect(result.contents[0].content).toBeInstanceOf(ComponentGeogebraPropsDto); + }); + }); + + describe('when mapping LessonResponse to lesson DTO with Text content', () => { + const setup = () => { + const lessonContentResponse: LessonContentResponse = { + content: { text: faker.lorem.sentence() } as ComponentTextPropsImpl, + _id: faker.string.uuid(), + id: faker.string.uuid(), + title: faker.lorem.sentence(), + component: faker.helpers.arrayElement(['text']), + hidden: faker.datatype.boolean(), + }; + + const lessonResponse: LessonResponse = { + _id: faker.string.uuid(), + id: faker.string.uuid(), + name: faker.lorem.sentence(), + courseId: faker.string.uuid(), + courseGroupId: faker.string.uuid(), + hidden: faker.datatype.boolean(), + position: faker.number.int(), + contents: [lessonContentResponse], + materials: [materialResponse], + }; + + return { lessonResponse, lessonContentResponse }; + }; + + it('should return LessonDto with text content', () => { + const { lessonResponse } = setup(); + + const result = LessonDtoMapper.mapToLessonDto(lessonResponse); + + expect(result).toBeInstanceOf(LessonDto); + expect(result.contents[0].component).toEqual('text'); + expect(result.contents[0].content).toBeInstanceOf(ComponentTextPropsDto); + }); + }); + + describe('when mapping LessonResponse to lesson DTO with internal content', () => { + const setup = () => { + const lessonContentResponse: LessonContentResponse = { + content: { url: faker.internet.url() } as ComponentInternalPropsImpl, + _id: faker.string.uuid(), + id: faker.string.uuid(), + title: faker.lorem.sentence(), + component: faker.helpers.arrayElement(['internal']), + hidden: faker.datatype.boolean(), + }; + + const lessonResponse: LessonResponse = { + _id: faker.string.uuid(), + id: faker.string.uuid(), + name: faker.lorem.sentence(), + courseId: faker.string.uuid(), + courseGroupId: faker.string.uuid(), + hidden: faker.datatype.boolean(), + position: faker.number.int(), + contents: [lessonContentResponse], + materials: [materialResponse], + }; + + return { lessonResponse, lessonContentResponse }; + }; + + it('should return LessonDto with internal content', () => { + const { lessonResponse } = setup(); + + const result = LessonDtoMapper.mapToLessonDto(lessonResponse); + + expect(result).toBeInstanceOf(LessonDto); + expect(result.contents[0].component).toEqual('internal'); + expect(result.contents[0].content).toBeInstanceOf(ComponentInternalPropsDto); + }); + }); + + describe('when mapping LessonResponse to lesson DTO with lernstore content', () => { + const setup = () => { + const lessonContentResponse: LessonContentResponse = { + content: { resources: [faker.internet.url(), faker.lorem.text()] } as ComponentLernstorePropsImpl, + _id: faker.string.uuid(), + id: faker.string.uuid(), + title: faker.lorem.sentence(), + component: faker.helpers.arrayElement(['resources']), + hidden: faker.datatype.boolean(), + }; + + const lessonResponse: LessonResponse = { + _id: faker.string.uuid(), + id: faker.string.uuid(), + name: faker.lorem.sentence(), + courseId: faker.string.uuid(), + courseGroupId: faker.string.uuid(), + hidden: faker.datatype.boolean(), + position: faker.number.int(), + contents: [lessonContentResponse], + materials: [materialResponse], + }; + + return { lessonResponse, lessonContentResponse }; + }; + + it('should return LessonDto with lernstore content', () => { + const { lessonResponse } = setup(); + + const result = LessonDtoMapper.mapToLessonDto(lessonResponse); + + expect(result).toBeInstanceOf(LessonDto); + expect(result.contents[0].component).toEqual('resources'); + expect(result.contents[0].content).toBeInstanceOf(ComponentLernstorePropsDto); + }); + }); + + describe('when mapping LessonResponse to lesson DTO with next board content', () => { + const setup = () => { + const lessonContentResponse: LessonContentResponse = { + content: { + board: faker.lorem.text(), + description: faker.lorem.word(), + title: faker.lorem.text(), + url: faker.internet.url(), + } as ComponentNexboardPropsImpl, + _id: faker.string.uuid(), + id: faker.string.uuid(), + title: faker.lorem.sentence(), + component: faker.helpers.arrayElement(['neXboard']), + hidden: faker.datatype.boolean(), + }; + + const lessonResponse: LessonResponse = { + _id: faker.string.uuid(), + id: faker.string.uuid(), + name: faker.lorem.sentence(), + courseId: faker.string.uuid(), + courseGroupId: faker.string.uuid(), + hidden: faker.datatype.boolean(), + position: faker.number.int(), + contents: [lessonContentResponse], + materials: [materialResponse], + }; + + return { lessonResponse, lessonContentResponse }; + }; + + it('should return LessonDto with nexboard content', () => { + const { lessonResponse } = setup(); + + const result = LessonDtoMapper.mapToLessonDto(lessonResponse); + + expect(result).toBeInstanceOf(LessonDto); + expect(result.contents[0].component).toEqual('neXboard'); + expect(result.contents[0].content).toBeInstanceOf(ComponentNexboardPropsDto); + }); + }); + + describe('when mapping LessonResponse to lesson DTO with an empty content', () => { + const setup = () => { + const lessonContentResponse: LessonContentResponse = { + content: { + board: faker.lorem.text(), + description: faker.lorem.word(), + title: faker.lorem.text(), + url: faker.internet.url(), + } as ComponentNexboardPropsImpl, + _id: faker.string.uuid(), + id: faker.string.uuid(), + title: faker.lorem.sentence(), + component: faker.helpers.arrayElement(['unknown']) as unknown as LessonContentResponseComponent, + hidden: faker.datatype.boolean(), + }; + + const lessonResponse: LessonResponse = { + _id: faker.string.uuid(), + id: faker.string.uuid(), + name: faker.lorem.sentence(), + courseId: faker.string.uuid(), + courseGroupId: faker.string.uuid(), + hidden: faker.datatype.boolean(), + position: faker.number.int(), + contents: [lessonContentResponse], + materials: [], + }; + + return { lessonResponse }; + }; + it('should return an empty array of contents', () => { + const { lessonResponse } = setup(); + + const result = LessonDtoMapper.mapToLessonDto(lessonResponse); + + expect(result).toBeInstanceOf(LessonDto); + expect(result.contents).toEqual([]); }); }); }); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/mapper/lesson-dto.mapper.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/mapper/lesson-dto.mapper.ts index 150037ddaee..aeeeba2cf3a 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/mapper/lesson-dto.mapper.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/mapper/lesson-dto.mapper.ts @@ -1,6 +1,19 @@ import { LessonContentDto, LessonDto, LessonLinkedTaskDto, LessonMaterialsDto } from '../dto'; +import { ComponentEtherpadPropsDto } from '../dto/component-etherpad-props.dto'; +import { ComponentGeogebraPropsDto } from '../dto/component-geogebra-props.dto'; +import { ComponentInternalPropsDto } from '../dto/component-internal-props.dto'; +import { ComponentLernstorePropsDto } from '../dto/component-lernstore-props.dto'; +import { ComponentNexboardPropsDto } from '../dto/component-nexboard-props-dto'; +import { ComponentTextPropsDto } from '../dto/component-text-props.dto'; import { + ComponentEtherpadPropsImpl, + ComponentGeogebraPropsImpl, + ComponentInternalPropsImpl, + ComponentLernstorePropsImpl, + ComponentNexboardPropsImpl, + ComponentTextPropsImpl, LessonContentResponse, + LessonContentResponseComponent, LessonLinkedTaskResponse, LessonResponse, MaterialResponse, @@ -34,7 +47,10 @@ export class LessonDtoMapper { courseGroupId: lessonResponse.courseGroupId, hidden: lessonResponse.hidden, position: lessonResponse.position, - contents: lessonResponse.contents.map((content) => this.mapToLessenContentDto(content)), + contents: lessonResponse.contents + .map((content) => this.mapToLessenContentDto(content)) + .filter((contetnDto) => contetnDto !== null), + linkedTasks: [], materials: lessonResponse.materials.map((material) => this.mapToLessonMaterialDto(material)), }); @@ -55,14 +71,58 @@ export class LessonDtoMapper { return lessonMaterialsDto; } - private static mapToLessenContentDto(lessonContentResponse: LessonContentResponse): LessonContentDto { - const lessonContentDto = new LessonContentDto({ - content: lessonContentResponse.content, - title: lessonContentResponse.title, - component: lessonContentResponse.component, - hidden: lessonContentResponse.hidden, - }); - - return lessonContentDto; + private static mapToLessenContentDto(lessonContentResponse: LessonContentResponse): LessonContentDto | null { + switch (lessonContentResponse.component) { + case LessonContentResponseComponent.TEXT: + return new LessonContentDto({ + id: lessonContentResponse.id, + title: lessonContentResponse.title, + component: lessonContentResponse.component, + hidden: lessonContentResponse.hidden, + content: new ComponentTextPropsDto(lessonContentResponse.content as ComponentTextPropsImpl), + }); + case LessonContentResponseComponent.ETHERPAD: + return new LessonContentDto({ + id: lessonContentResponse.id, + title: lessonContentResponse.title, + component: lessonContentResponse.component, + hidden: lessonContentResponse.hidden, + content: new ComponentEtherpadPropsDto(lessonContentResponse.content as ComponentEtherpadPropsImpl), + }); + case LessonContentResponseComponent.GEO_GEBRA: + return new LessonContentDto({ + id: lessonContentResponse.id, + title: lessonContentResponse.title, + component: lessonContentResponse.component, + hidden: lessonContentResponse.hidden, + content: new ComponentGeogebraPropsDto(lessonContentResponse.content as ComponentGeogebraPropsImpl), + }); + case LessonContentResponseComponent.INTERNAL: + return new LessonContentDto({ + id: lessonContentResponse.id, + title: lessonContentResponse.title, + component: lessonContentResponse.component, + hidden: lessonContentResponse.hidden, + content: new ComponentInternalPropsDto(lessonContentResponse.content as ComponentInternalPropsImpl), + }); + case LessonContentResponseComponent.RESOURCES: + return new LessonContentDto({ + id: lessonContentResponse.id, + title: lessonContentResponse.title, + component: lessonContentResponse.component, + hidden: lessonContentResponse.hidden, + content: new ComponentLernstorePropsDto(lessonContentResponse.content as ComponentLernstorePropsImpl), + }); + case LessonContentResponseComponent.NEX_BOARD: + return new LessonContentDto({ + id: lessonContentResponse.id, + title: lessonContentResponse.title, + component: lessonContentResponse.component, + hidden: lessonContentResponse.hidden, + content: new ComponentNexboardPropsDto(lessonContentResponse.content as ComponentNexboardPropsImpl), + }); + default: + return null; + } } } diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/index.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/index.ts new file mode 100644 index 00000000000..eff692a98e4 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/index.ts @@ -0,0 +1,6 @@ +export { RoomBoardDto } from './room-board.dto'; +export { BoardElementDto } from './board-element.dto'; +export { BoardTaskDto } from './board-task.dto'; +export { BoardTaskStatusDto } from './board-task-status.dto'; +export { BoardLessonDto } from './board-lesson.dto'; +export { BoardColumnBoardDto } from './board-column-board.dto'; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.spec.ts index f38859c9bad..aabb717c7aa 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.spec.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.spec.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker'; import { BoardTaskStatusResponse } from '../room-api-client'; import { BoardTaskStatusMapper } from './board-task-status-dto.mapper'; -import { BoardTaskStatusDto } from '../dto/board-task-status.dto'; +import { BoardTaskStatusDto } from '../dto'; describe(BoardTaskStatusMapper.name, () => { describe('mapBoardTaskStatusToDto', () => { diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.ts index c4834f148f9..709533afaeb 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.ts @@ -1,4 +1,4 @@ -import { BoardTaskStatusDto } from '../dto/board-task-status.dto'; +import { BoardTaskStatusDto } from '../dto'; import { BoardTaskStatusResponse } from '../room-api-client'; export class BoardTaskStatusMapper { diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.spec.ts index 037ba49cae7..f1fc542b2b0 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.spec.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.spec.ts @@ -8,7 +8,7 @@ import { SingleColumnBoardResponse, } from '../room-api-client'; import { RoomBoardDtoMapper } from './room-board-dto.mapper'; -import { BoardTaskStatusDto } from '../dto/board-task-status.dto'; +import { BoardTaskStatusDto } from '../dto'; describe(RoomBoardDtoMapper.name, () => { describe('mapResponseToRoomBoardDto', () => { diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.ts index 6bacca7eff6..5074e143e98 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.ts @@ -1,8 +1,4 @@ -import { BoardColumnBoardDto } from '../dto/board-column-board.dto'; -import { BoardElementDto } from '../dto/board-element.dto'; -import { BoardLessonDto } from '../dto/board-lesson.dto'; -import { BoardTaskDto } from '../dto/board-task.dto'; -import { RoomBoardDto } from '../dto/room-board.dto'; +import { RoomBoardDto, BoardTaskDto, BoardLessonDto, BoardElementDto, BoardColumnBoardDto } from '../dto'; import { BoardElementDtoType } from '../enums/board-element.enum'; import { BoardColumnBoardResponse, diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.spec.ts index 63ff0dbf697..56264183d89 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.spec.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.spec.ts @@ -8,7 +8,7 @@ import { jest } from '@jest/globals'; import { CourseRoomsApi, SingleColumnBoardResponse } from './room-api-client'; import { CourseRoomsClientAdapter } from './room-client.adapter'; import { RoomBoardDtoMapper } from './mapper/room-board-dto.mapper'; -import { RoomBoardDto } from './dto/room-board.dto'; +import { RoomBoardDto } from './dto'; const jwtToken = 'dummyJwtToken'; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.ts index b53c3932334..b11354c8d4d 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.ts @@ -4,7 +4,7 @@ import { Request } from 'express'; import { extractJwtFromHeader } from '@shared/common'; import { RawAxiosRequestConfig } from 'axios'; import { CourseRoomsApi } from './room-api-client'; -import { RoomBoardDto } from './dto/room-board.dto'; +import { RoomBoardDto } from './dto'; import { RoomBoardDtoMapper } from './mapper/room-board-dto.mapper'; @Injectable() diff --git a/apps/server/src/modules/common-cartridge/common-cartridge.module.ts b/apps/server/src/modules/common-cartridge/common-cartridge.module.ts index c5beb84df62..5997ab8f977 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge.module.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge.module.ts @@ -1,6 +1,5 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { FilesStorageClientModule } from '@modules/files-storage-client'; import { Module } from '@nestjs/common'; import { ALL_ENTITIES } from '@shared/domain/entity'; import { DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config'; @@ -13,11 +12,11 @@ import { CommonCartridgeUc } from './uc/common-cartridge.uc'; import { CourseRoomsModule } from './common-cartridge-client/room-client'; import { CardClientModule } from './common-cartridge-client/card-client/card-client.module'; import { LessonClientModule } from './common-cartridge-client/lesson-client/lesson-client.module'; +import { CommonCartridgeExportMapper } from './service/common-cartridge.mapper'; @Module({ imports: [ RabbitMQWrapperModule, - FilesStorageClientModule, MikroOrmModule.forRoot({ ...defaultMikroOrmOptions, type: 'mongo', @@ -43,7 +42,7 @@ import { LessonClientModule } from './common-cartridge-client/lesson-client/less basePath: `${Configuration.get('API_HOST') as string}/v3/`, }), ], - providers: [CommonCartridgeUc, CommonCartridgeExportService], + providers: [CommonCartridgeExportMapper, CommonCartridgeUc, CommonCartridgeExportService], exports: [CommonCartridgeUc], }) export class CommonCartridgeModule {} diff --git a/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.spec.ts b/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.spec.ts index a274eff5f7c..245579d7364 100644 --- a/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.spec.ts +++ b/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.spec.ts @@ -1,10 +1,14 @@ import { faker } from '@faker-js/faker'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; +import { Response } from 'express'; +import { StreamableFile } from '@nestjs/common'; import { CommonCartridgeUc } from '../uc/common-cartridge.uc'; import { CommonCartridgeController } from './common-cartridge.controller'; -import { CourseFileIdsResponse, ExportCourseParams } from './dto'; -import { CourseExportBodyResponse } from './dto/course-export-body.response'; +import { ExportCourseParams } from './dto'; +import { CourseQueryParams } from './dto/course.query.params'; +import { CourseExportBodyParams } from './dto/course-export.body.params'; +import { CommonCartridgeVersion } from '../export/common-cartridge.enums'; describe('CommonCartridgeController', () => { let module: TestingModule; @@ -37,28 +41,33 @@ describe('CommonCartridgeController', () => { describe('exportCourse', () => { const setup = () => { const courseId = faker.string.uuid(); - const request = new ExportCourseParams(); - const expected = new CourseExportBodyResponse({ - courseFileIds: new CourseFileIdsResponse([]), - courseCommonCartridgeMetadata: { - id: courseId, - title: faker.lorem.sentence(), - copyRightOwners: [faker.lorem.words()], - }, - }); + const params = { courseId } as ExportCourseParams; + const query = { version: CommonCartridgeVersion.V_1_1_0 } as CourseQueryParams; + const body = { + topics: [faker.string.uuid(), faker.string.uuid()], + tasks: [faker.string.uuid()], + columnBoards: [faker.string.uuid(), faker.string.uuid()], + } as CourseExportBodyParams; + const expected = Buffer.from(faker.lorem.paragraphs(100)); + const mockResponse = { + set: jest.fn(), + } as unknown as Response; - Reflect.set(request, 'parentId', courseId); commonCartridgeUcMock.exportCourse.mockResolvedValue(expected); - return { request, expected }; + return { params, expected, query, body, mockResponse }; }; - it('should return a list of found FileRecords', async () => { - const { request, expected } = setup(); + it('should return a streamable file', async () => { + const { params, query, body, mockResponse } = setup(); - const result = await sut.exportCourse(request); + const result = await sut.exportCourse(params, query, body, mockResponse); - expect(result).toEqual(expected); + expect(mockResponse.set).toHaveBeenCalledWith({ + 'Content-Type': 'application/zip', + 'Content-Disposition': `attachment; filename=course_${params.courseId}.zip`, + }); + expect(result).toBeInstanceOf(StreamableFile); }); }); }); diff --git a/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.ts b/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.ts index bd609c4ff88..80e1e5d10ea 100644 --- a/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.ts +++ b/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.ts @@ -1,16 +1,34 @@ -import { Controller, Get, Param } from '@nestjs/common'; +import { Body, Controller, Param, Post, Query, Res, StreamableFile } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { Response } from 'express'; import { CommonCartridgeUc } from '../uc/common-cartridge.uc'; -import { ExportCourseParams } from './dto'; -import { CourseExportBodyResponse } from './dto/course-export-body.response'; +import { ExportCourseParams, CourseQueryParams, CourseExportBodyParams } from './dto'; @ApiTags('common-cartridge') @Controller('common-cartridge') export class CommonCartridgeController { constructor(private readonly commonCartridgeUC: CommonCartridgeUc) {} - @Get('export/:parentId') - public async exportCourse(@Param() exportCourseParams: ExportCourseParams): Promise { - return this.commonCartridgeUC.exportCourse(exportCourseParams.parentId); + @Post('export/:courseId') + public async exportCourse( + @Param() exportCourseParams: ExportCourseParams, + @Query() queryParams: CourseQueryParams, + @Body() bodyParams: CourseExportBodyParams, + @Res({ passthrough: true }) response: Response + ): Promise { + const result = await this.commonCartridgeUC.exportCourse( + exportCourseParams.courseId, + queryParams.version, + bodyParams.topics, + bodyParams.tasks, + bodyParams.columnBoards + ); + + response.set({ + 'Content-Type': 'application/zip', + 'Content-Disposition': `attachment; filename=course_${exportCourseParams.courseId}.zip`, + }); + + return new StreamableFile(result); } } diff --git a/apps/server/src/modules/common-cartridge/controller/dto/common-cartridge.params.ts b/apps/server/src/modules/common-cartridge/controller/dto/common-cartridge.params.ts index a93c604f793..59f13b82182 100644 --- a/apps/server/src/modules/common-cartridge/controller/dto/common-cartridge.params.ts +++ b/apps/server/src/modules/common-cartridge/controller/dto/common-cartridge.params.ts @@ -5,5 +5,5 @@ import { IsMongoId } from 'class-validator'; export class ExportCourseParams { @IsMongoId() @ApiProperty() - public readonly parentId!: EntityId; + public readonly courseId!: EntityId; } diff --git a/apps/server/src/modules/common-cartridge/controller/dto/course-export.body.params.ts b/apps/server/src/modules/common-cartridge/controller/dto/course-export.body.params.ts new file mode 100644 index 00000000000..4bfe89851bc --- /dev/null +++ b/apps/server/src/modules/common-cartridge/controller/dto/course-export.body.params.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray } from 'class-validator'; + +export class CourseExportBodyParams { + @IsArray() + @ApiProperty({ + description: 'The list of ids of topics which should be exported. If empty no topics are exported.', + type: [String], + }) + public readonly topics!: string[]; + + @IsArray() + @ApiProperty({ + description: 'The list of ids of tasks which should be exported. If empty no tasks are exported.', + type: [String], + }) + public readonly tasks!: string[]; + + @IsArray() + @ApiProperty({ + description: 'The list of ids of column boards which should be exported. If empty no column boards are exported.', + type: [String], + }) + public readonly columnBoards!: string[]; +} diff --git a/apps/server/src/modules/common-cartridge/controller/dto/course.query.params.ts b/apps/server/src/modules/common-cartridge/controller/dto/course.query.params.ts new file mode 100644 index 00000000000..aa0a1b8bd2b --- /dev/null +++ b/apps/server/src/modules/common-cartridge/controller/dto/course.query.params.ts @@ -0,0 +1,14 @@ +import { IsString, Matches } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { CommonCartridgeVersion } from '../../export/common-cartridge.enums'; + +export class CourseQueryParams { + @IsString() + @Matches(Object.values(CommonCartridgeVersion).join('|')) + @ApiProperty({ + description: 'The version of CC export', + nullable: false, + enum: CommonCartridgeVersion, + }) + public readonly version!: CommonCartridgeVersion; +} diff --git a/apps/server/src/modules/common-cartridge/controller/dto/index.ts b/apps/server/src/modules/common-cartridge/controller/dto/index.ts index e93173f89f7..6f0d5c3e079 100644 --- a/apps/server/src/modules/common-cartridge/controller/dto/index.ts +++ b/apps/server/src/modules/common-cartridge/controller/dto/index.ts @@ -1,2 +1,4 @@ export * from './common-cartridge.params'; export * from './common-cartridge.response'; +export * from './course.query.params'; +export * from './course-export.body.params'; diff --git a/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.spec.ts b/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.spec.ts index f6d1066f5b0..769de8f23ed 100644 --- a/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.spec.ts +++ b/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.spec.ts @@ -1,28 +1,111 @@ -import { faker } from '@faker-js/faker'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { Test, TestingModule } from '@nestjs/testing'; -import { BoardClientAdapter } from '../common-cartridge-client/board-client'; +import AdmZip from 'adm-zip'; +import { BoardClientAdapter, BoardSkeletonDto } from '../common-cartridge-client/board-client'; import { CommonCartridgeExportService } from './common-cartridge-export.service'; -import { CoursesClientAdapter } from '../common-cartridge-client/course-client'; +import { CourseCommonCartridgeMetadataDto, CoursesClientAdapter } from '../common-cartridge-client/course-client'; import { CourseRoomsClientAdapter } from '../common-cartridge-client/room-client'; import { CardClientAdapter } from '../common-cartridge-client/card-client/card-client.adapter'; -import { CardListResponseDto } from '../common-cartridge-client/card-client/dto/card-list-response.dto'; -import { CardResponseDto } from '../common-cartridge-client/card-client/dto/card-response.dto'; -import { ContentElementType } from '../common-cartridge-client/card-client/enums/content-element-type.enum'; +import { LessonClientAdapter } from '../common-cartridge-client/lesson-client/lesson-client.adapter'; +import { CommonCartridgeExportMapper } from './common-cartridge.mapper'; +import { CommonCartridgeVersion } from '../export/common-cartridge.enums'; +import { + RoomBoardDto, + BoardTaskDto, + BoardLessonDto, + BoardColumnBoardDto, +} from '../common-cartridge-client/room-client/dto'; +import { + RichTextElementContentDto, + LinkElementContentDto, + CardListResponseDto, +} from '../common-cartridge-client/card-client/dto'; +import { + boardCloumnBoardFactory, + boardLessonFactory, + boardTaskFactory, + columnBoardFactory, + courseMetadataFactory, + lessonFactory, + listOfCardResponseFactory, + roomFactory, +} from '../testing/common-cartridge-dtos.factory'; describe('CommonCartridgeExportService', () => { let module: TestingModule; let sut: CommonCartridgeExportService; - let filesStorageServiceMock: DeepMocked; let coursesClientAdapterMock: DeepMocked; let courseRoomsClientAdapterMock: DeepMocked; let cardClientAdapterMock: DeepMocked; + let boardClientAdapterMock: DeepMocked; + let lessonClientAdapterMock: DeepMocked; + + const createXmlString = (nodeName: string, value: boolean | number | string): string => + `<${nodeName}>${value.toString()}`; + const getFileContent = (archive: AdmZip, filePath: string): string | undefined => + archive.getEntry(filePath)?.getData().toString(); + const setupParams = async ( + version: CommonCartridgeVersion, + exportTopics: boolean, + exportTasks: boolean, + exportColumnBoards: boolean + ) => { + const courseMetadata: CourseCommonCartridgeMetadataDto = courseMetadataFactory.build(); + const lessons = lessonFactory.buildList(2); + const [lesson] = lessons; + lesson.courseId = courseMetadata.id; + + const boardSkeleton: BoardSkeletonDto = columnBoardFactory.build(); + const listOfCardsResponse: CardListResponseDto = listOfCardResponseFactory.build(); + const boardTask: BoardTaskDto = boardTaskFactory.build(); + boardTask.courseName = courseMetadata.courseName; + + const room: RoomBoardDto = roomFactory.build(); + room.title = courseMetadata.courseName; + room.elements[0].content = boardTask; + room.elements[1].content = new BoardLessonDto(boardLessonFactory.build()); + room.elements[1].content.id = lesson.lessonId; + room.elements[1].content.name = lesson.name; + room.elements[2].content = new BoardColumnBoardDto(boardCloumnBoardFactory.build()); + + coursesClientAdapterMock.getCourseCommonCartridgeMetadata.mockResolvedValue(courseMetadata); + lessonClientAdapterMock.getLessonById.mockResolvedValue(lesson); + lessonClientAdapterMock.getLessonTasks.mockResolvedValue(lesson.linkedTasks ?? []); + boardClientAdapterMock.getBoardSkeletonById.mockResolvedValue(boardSkeleton); + cardClientAdapterMock.getAllBoardCardsByIds.mockResolvedValue(listOfCardsResponse); + courseRoomsClientAdapterMock.getRoomBoardByCourseId.mockResolvedValue(room); + + const buffer = await sut.exportCourse( + courseMetadata.id, + version, + exportTopics ? [room.elements[1].content.id] : [], + exportTasks ? [room.elements[0].content.id] : [], + exportColumnBoards ? [room.elements[2].content.id] : [] + ); + + const archive = new AdmZip(buffer); + + return { + courseMetadata, + archive, + version, + room, + lesson, + lessons, + boardTask, + boardSkeleton, + listOfCardsResponse, + textElement: listOfCardsResponse.data[0].elements[0].content as RichTextElementContentDto, + linkElement: listOfCardsResponse.data[0].elements[1].content as LinkElementContentDto, + }; + }; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ CommonCartridgeExportService, + CommonCartridgeExportMapper, { provide: FilesStorageClientAdapterService, useValue: createMock(), @@ -43,14 +126,19 @@ describe('CommonCartridgeExportService', () => { provide: CardClientAdapter, useValue: createMock(), }, + { + provide: LessonClientAdapter, + useValue: createMock(), + }, ], }).compile(); sut = module.get(CommonCartridgeExportService); - filesStorageServiceMock = module.get(FilesStorageClientAdapterService); coursesClientAdapterMock = module.get(CoursesClientAdapter); courseRoomsClientAdapterMock = module.get(CourseRoomsClientAdapter); cardClientAdapterMock = module.get(CardClientAdapter); + boardClientAdapterMock = module.get(BoardClientAdapter); + lessonClientAdapterMock = module.get(LessonClientAdapter); }); afterAll(async () => { @@ -61,115 +149,193 @@ describe('CommonCartridgeExportService', () => { expect(sut).toBeDefined(); }); - describe('findCourseFileRecords', () => { - const setup = () => { - const courseId = faker.string.uuid(); - const expected = []; + describe('exportCourse', () => { + describe('when using version 1.1', () => { + const setup = async () => setupParams(CommonCartridgeVersion.V_1_1_0, true, true, true); - filesStorageServiceMock.listFilesOfParent.mockResolvedValue([]); + it('should use schema version 1.1.0', async () => { + const { archive } = await setup(); - return { courseId, expected }; - }; + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(createXmlString('schemaversion', '1.1.0')); + }); + + it('should add course', async () => { + const { archive, courseMetadata } = await setup(); + + expect(getFileContent(archive, 'imsmanifest.xml')).toContain( + createXmlString('mnf:string', courseMetadata.courseName) + ); + }); + + it('should add lesson', async () => { + const { archive, lesson } = await setup(); - it('should return a list of FileRecords', async () => { - const { courseId, expected } = setup(); + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(createXmlString('title', lesson.name)); + }); - const result = await sut.findCourseFileRecords(courseId); + it('should add task', async () => { + const { archive, boardTask } = await setup(); - expect(result).toEqual(expected); + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(createXmlString('title', boardTask.name)); + + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(` { + const { archive, lesson } = await setup(); + const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); + + lesson.linkedTasks.forEach((linkedTask) => { + expect(manifest).toContain(`${linkedTask.name}`); + }); + }); + + it('should add lernstore element of lesson to manifest file', async () => { + const { archive, lesson } = await setup(); + const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); + + lesson.contents.forEach((content) => { + expect(manifest).toContain(`${content.title}`); + }); + }); + + it('should add column boards', async () => { + const { archive, boardSkeleton } = await setup(); + const manifest = getFileContent(archive, 'imsmanifest.xml'); + + expect(manifest).toContain(createXmlString('title', boardSkeleton.title)); + }); + + it('should add column', async () => { + const { archive, boardSkeleton } = await setup(); + const manifest = getFileContent(archive, 'imsmanifest.xml'); + + expect(manifest).toContain(createXmlString('title', boardSkeleton.columns[0].title ?? '')); + }); + + it('should add card', async () => { + const { archive, listOfCardsResponse } = await setup(); + const manifest = getFileContent(archive, 'imsmanifest.xml'); + + expect(manifest).toContain(createXmlString('title', listOfCardsResponse.data[0].title ?? '')); + }); }); - }); - describe('findCourseCcMetadata', () => { - const setup = () => { - const courseId = faker.string.uuid(); - const expected = { - id: courseId, - title: faker.lorem.sentence(), - copyRightOwners: [faker.lorem.word()], - }; + describe('when using version 1.3', () => { + const setup = async () => setupParams(CommonCartridgeVersion.V_1_3_0, true, true, true); - coursesClientAdapterMock.getCourseCommonCartridgeMetadata.mockResolvedValue(expected); + it('should use schema version 1.3.0', async () => { + const { archive } = await setup(); - return { courseId, expected }; - }; + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(createXmlString('schemaversion', '1.3.0')); + }); - it('should return a CourseCommonCartridgeMetadataDto', async () => { - const { courseId, expected } = setup(); + it('should add course', async () => { + const { archive, courseMetadata } = await setup(); - const result = await sut.findCourseCommonCartridgeMetadata(courseId); + expect(getFileContent(archive, 'imsmanifest.xml')).toContain( + createXmlString('mnf:string', courseMetadata.courseName) + ); + }); - expect(result).toEqual(expected); + it('should add lesson', async () => { + const { archive, lesson } = await setup(); + + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(createXmlString('title', lesson.name)); + }); + + it('should add tasks', async () => { + const { archive, boardTask } = await setup(); + + expect(getFileContent(archive, 'imsmanifest.xml')).toContain(` { + const { archive, lesson } = await setup(); + const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); + + lesson.linkedTasks.forEach((linkedTask) => { + expect(manifest).toContain(`${linkedTask.name}`); + }); + }); + + it('should add lernstore element of lesson to manifest file', async () => { + const { archive, lesson } = await setup(); + const manifest = archive.getEntry('imsmanifest.xml')?.getData().toString(); + + lesson.contents.forEach((content) => { + expect(manifest).toContain(`${content.title}`); + }); + }); + + it('should add column boards', async () => { + const { archive, boardSkeleton } = await setup(); + const manifest = getFileContent(archive, 'imsmanifest.xml'); + + expect(manifest).toContain(createXmlString('title', boardSkeleton.title)); + }); + + it('should add column', async () => { + const { archive, boardSkeleton } = await setup(); + const manifest = getFileContent(archive, 'imsmanifest.xml'); + + expect(manifest).toContain(createXmlString('title', boardSkeleton.columns[0].title ?? '')); + }); + + it('should add card', async () => { + const { archive, listOfCardsResponse } = await setup(); + const manifest = getFileContent(archive, 'imsmanifest.xml'); + + expect(manifest).toContain(createXmlString('title', listOfCardsResponse.data[0].title ?? '')); + }); + + it('should add link element of card', async () => { + const { archive, linkElement } = await setup(); + const manifest = getFileContent(archive, 'imsmanifest.xml'); + + expect(manifest).toContain(createXmlString('title', linkElement.title)); + }); + + it('should add text element of card', async () => { + const { archive, textElement } = await setup(); + const manifest = getFileContent(archive, 'imsmanifest.xml'); + + expect(manifest).toContain(createXmlString('title', textElement.text)); + }); }); - }); - describe('findCourseRoomBoard', () => { - const setup = () => { - const roomId = faker.string.uuid(); - const expected = { - roomId, - title: faker.lorem.word(), - displayColor: faker.date.recent().toString(), - isSynchronized: faker.datatype.boolean(), - elements: [], - isArchived: faker.datatype.boolean(), - }; - - courseRoomsClientAdapterMock.getRoomBoardByCourseId.mockResolvedValue(expected); - - return { roomId, expected }; - }; + describe('when topics array is empty', () => { + const setup = async () => setupParams(CommonCartridgeVersion.V_1_1_0, false, true, true); - it('should return a room board', async () => { - const { roomId, expected } = setup(); + it("shouldn't add lessons", async () => { + const { archive, lessons } = await setup(); - const result = await sut.findRoomBoardByCourseId(roomId); + lessons.forEach((lesson) => { + expect(getFileContent(archive, 'imsmanifest.xml')).not.toContain(createXmlString('title', lesson.name)); + }); + }); + }); - expect(result).toEqual(expected); + describe('when tasks array is empty', () => { + const setup = async () => setupParams(CommonCartridgeVersion.V_1_1_0, true, false, true); + + it("shouldn't add tasks", async () => { + const { archive, boardTask } = await setup(); + + expect(getFileContent(archive, 'imsmanifest.xml')).not.toContain(createXmlString('title', boardTask.name)); + }); }); - }); - describe('findAllCardsByIds', () => { - const setup = () => { - const cardsIds: Array = new Array(faker.string.uuid()); - const mockCard: CardResponseDto = { - id: cardsIds[0], - title: faker.lorem.word(), - height: faker.number.int(), - elements: [ - { - id: 'element-1', - type: ContentElementType.RICH_TEXT, - content: { - text: faker.string.alphanumeric.toString(), - inputFormat: 'HTML', - }, - timestamps: { - lastUpdatedAt: faker.date.anytime.toString(), - createdAt: faker.date.anytime.toString(), - deletedAt: '', - }, - }, - ], - visibilitySettings: { - publishedAt: '2024-10-01T12:00:00Z', - }, - timeStamps: { - lastUpdatedAt: '2024-10-01T11:00:00Z', - createdAt: faker.date.anytime.toString(), - deletedAt: faker.date.anytime.toString(), - }, - }; - const expected: CardListResponseDto = new CardListResponseDto(new Array(mockCard)); - cardClientAdapterMock.getAllBoardCardsByIds.mockResolvedValue(expected); + describe('when columnBoards array is empty', () => { + const setup = async () => setupParams(CommonCartridgeVersion.V_1_1_0, true, true, false); - return { cardsIds, expected }; - }; - it('should return a card', async () => { - const { cardsIds, expected } = setup(); - const result = await sut.findAllCardsByIds(cardsIds); + it("shouldn't add column boards", async () => { + const { archive, boardSkeleton } = await setup(); - expect(result).toEqual(expected); + expect(getFileContent(archive, 'imsmanifest.xml')).not.toContain( + createXmlString('title', boardSkeleton.columns[0].title) + ); + }); }); }); }); diff --git a/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.ts b/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.ts index 05895643bf1..49fe182de05 100644 --- a/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.ts +++ b/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.ts @@ -1,43 +1,256 @@ -import { FileDto, FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { Injectable } from '@nestjs/common'; -import { BoardClientAdapter } from '../common-cartridge-client/board-client'; +import { BoardClientAdapter, BoardSkeletonDto, ColumnSkeletonDto } from '../common-cartridge-client/board-client'; import { CourseCommonCartridgeMetadataDto, CoursesClientAdapter } from '../common-cartridge-client/course-client'; import { CourseRoomsClientAdapter } from '../common-cartridge-client/room-client'; -import { RoomBoardDto } from '../common-cartridge-client/room-client/dto/room-board.dto'; +import { + RoomBoardDto, + BoardElementDto, + BoardColumnBoardDto, + BoardLessonDto, + BoardTaskDto, +} from '../common-cartridge-client/room-client/dto'; import { CardClientAdapter } from '../common-cartridge-client/card-client/card-client.adapter'; -import { CardListResponseDto } from '../common-cartridge-client/card-client/dto/card-list-response.dto'; +import { LessonClientAdapter } from '../common-cartridge-client/lesson-client/lesson-client.adapter'; +import { LessonContentDto, LessonDto } from '../common-cartridge-client/lesson-client/dto'; +import { CommonCartridgeFileBuilder } from '../export/builders/common-cartridge-file-builder'; +import { CommonCartridgeVersion } from '../export/common-cartridge.enums'; +import { CommonCartridgeExportMapper } from './common-cartridge.mapper'; +import { CommonCartridgeOrganizationNode } from '../export/builders/common-cartridge-organization-node'; +import { createIdentifier } from '../export/utils'; +import { BoardElementDtoType } from '../common-cartridge-client/room-client/enums/board-element.enum'; +import { CardResponseElementsInnerDto } from '../common-cartridge-client/card-client/types/card-response-elements-inner.type'; +import { + RichTextElementResponseDto, + LinkElementResponseDto, + CardListResponseDto, + CardResponseDto, +} from '../common-cartridge-client/card-client/dto'; @Injectable() export class CommonCartridgeExportService { constructor( - private readonly filesService: FilesStorageClientAdapterService, private readonly boardClientAdapter: BoardClientAdapter, private readonly cardClientAdapter: CardClientAdapter, private readonly coursesClientAdapter: CoursesClientAdapter, - private readonly courseRoomsClientAdapter: CourseRoomsClientAdapter + private readonly courseRoomsClientAdapter: CourseRoomsClientAdapter, + private readonly lessonClientAdapter: LessonClientAdapter, + private readonly mapper: CommonCartridgeExportMapper ) {} - public async findCourseFileRecords(courseId: string): Promise { - const courseFiles = await this.filesService.listFilesOfParent(courseId); + public async exportCourse( + courseId: string, + version: CommonCartridgeVersion, + exportedTopics: string[], + exportedTasks: string[], + exportedColumnBoards: string[] + ): Promise { + const builder = new CommonCartridgeFileBuilder(this.mapper.mapCourseToManifest(version, courseId)); - return courseFiles; + const courseCommonCartridgeMetadata: CourseCommonCartridgeMetadataDto = + await this.findCourseCommonCartridgeMetadata(courseId); + + builder.addMetadata(this.mapper.mapCourseToMetadata(courseCommonCartridgeMetadata)); + + // get room board and the structure of the course + const roomBoard: RoomBoardDto = await this.findRoomBoardByCourseId(courseId); + + // add lessons to organization + await this.addLessons(builder, version, roomBoard.elements, exportedTopics); + + // add tasks to organization + this.addTasks(builder, version, roomBoard.elements, exportedTasks); + + // add column boards and cards to organization + await this.addColumnBoards(builder, roomBoard.elements, exportedColumnBoards); + + return builder.build(); + } + + private addComponentToOrganization( + component: LessonContentDto, + lessonOrganization: CommonCartridgeOrganizationNode + ): void { + const resources = this.mapper.mapContentToResources(component); + + if (Array.isArray(resources)) { + const componentOrganization = lessonOrganization.createChild(this.mapper.mapContentToOrganization(component)); + + resources.forEach((resource) => { + componentOrganization.addResource(resource); + }); + } else { + lessonOrganization.addResource(resources); + } + } + + private async addLessons( + builder: CommonCartridgeFileBuilder, + version: CommonCartridgeVersion, + elements: BoardElementDto[], + topics: string[] + ): Promise { + const filteredLessons = this.filterLessonFromBoardElements(elements); + const lessonsIds = filteredLessons.filter((lesson) => topics.includes(lesson.id)).map((lesson) => lesson.id); + const lessons = await Promise.all(lessonsIds.map((elementId) => this.findLessonById(elementId))); + + lessons.forEach((lesson) => { + const lessonsOrganization = builder.createOrganization(this.mapper.mapLessonToOrganization(lesson)); + + lesson.contents.forEach((content) => { + this.addComponentToOrganization(content, lessonsOrganization); + }); + + lesson.linkedTasks.forEach((task) => { + lessonsOrganization.addResource(this.mapper.mapLinkedTaskToResource(task, version)); + }); + }); + } + + private addTasks( + builder: CommonCartridgeFileBuilder, + version: CommonCartridgeVersion, + elements: BoardElementDto[], + exportedTasks: string[] + ): void { + const tasks: BoardTaskDto[] = this.filterTasksFromBoardElements(elements).filter((task) => + exportedTasks.includes(task.id) + ); + const tasksOrganization = builder.createOrganization({ + title: 'Aufgaben', + identifier: createIdentifier(), + }); + + tasks.forEach((task) => { + tasksOrganization.addResource(this.mapper.mapTaskToResource(task, version)); + }); + } + + private async addColumnBoards( + builder: CommonCartridgeFileBuilder, + elements: BoardElementDto[], + exportedColumnBoards: string[] + ): Promise { + const columnBoards = this.filterColumnBoardFromBoardElement(elements); + const columnBoardsIds = columnBoards + .filter((columnBoard) => exportedColumnBoards.includes(columnBoard.id)) + .map((columBoard) => columBoard.columnBoardId); + const boardSkeletons: BoardSkeletonDto[] = await Promise.all( + columnBoardsIds.map((columnBoardId) => this.findBoardSkeletonById(columnBoardId)) + ); + + await Promise.all( + boardSkeletons.map(async (boardSkeleton) => { + const columnBoardOrganization = builder.createOrganization({ + title: boardSkeleton.title, + identifier: createIdentifier(boardSkeleton.boardId), + }); + + await Promise.all( + boardSkeleton.columns.map((column) => this.addColumnToOrganization(column, columnBoardOrganization)) + ); + }) + ); + } + + private async addColumnToOrganization( + column: ColumnSkeletonDto, + columnBoardOrganization: CommonCartridgeOrganizationNode + ): Promise { + const { columnId } = column; + const columnOrganization = columnBoardOrganization.createChild({ + title: column.title ?? '', + identifier: createIdentifier(columnId), + }); + + if (column.cards.length) { + const cardsIds = column.cards.map((card) => card.cardId); + const listOfCards: CardListResponseDto = await this.findAllCardsByIds(cardsIds); + + listOfCards.data.forEach((card) => { + this.addCardToOrganization(card, columnOrganization); + }); + } + } + + private addCardToOrganization(card: CardResponseDto, columnOrganization: CommonCartridgeOrganizationNode): void { + const cardOrganization = columnOrganization.createChild({ + title: card.title ?? '', + identifier: createIdentifier(card.id), + }); + + card.elements.forEach((element) => { + this.addCardElementToOrganization(element, cardOrganization); + }); + } + + private addCardElementToOrganization( + element: CardResponseElementsInnerDto, + cardOrganization: CommonCartridgeOrganizationNode + ): void { + if (RichTextElementResponseDto.isRichTextElement(element)) { + const resource = this.mapper.mapRichTextElementToResource(element); + + cardOrganization.addResource(resource); + } + + if (LinkElementResponseDto.isLinkElement(element)) { + const resource = this.mapper.mapLinkElementToResource(element); + + cardOrganization.addResource(resource); + } + } + + private filterTasksFromBoardElements(elements: BoardElementDto[]): BoardTaskDto[] { + const tasks = elements + .filter((element) => element.type === BoardElementDtoType.TASK) + .map((element) => element.content as BoardTaskDto); + + return tasks; + } + + private filterLessonFromBoardElements(elements: BoardElementDto[]): BoardLessonDto[] { + const lessons = elements + .filter((element) => element.content instanceof BoardLessonDto) + .map((element) => element.content as BoardLessonDto); + + return lessons; + } + + private filterColumnBoardFromBoardElement(elements: BoardElementDto[]): BoardColumnBoardDto[] { + const columnBoard = elements + .filter((element) => element.type === BoardElementDtoType.COLUMN_BOARD) + .map((element) => element.content as BoardColumnBoardDto); + + return columnBoard; + } + + private async findCourseCommonCartridgeMetadata(courseId: string): Promise { + const courseMetadata = await this.coursesClientAdapter.getCourseCommonCartridgeMetadata(courseId); + + return courseMetadata; + } + + private async findRoomBoardByCourseId(courseId: string): Promise { + const roomBoardDto = await this.courseRoomsClientAdapter.getRoomBoardByCourseId(courseId); + + return roomBoardDto; } - public async findCourseCommonCartridgeMetadata(courseId: string): Promise { - const courseCommonCartridgeMetadata = await this.coursesClientAdapter.getCourseCommonCartridgeMetadata(courseId); + private async findBoardSkeletonById(boardId: string): Promise { + const boardSkeletonDto = await this.boardClientAdapter.getBoardSkeletonById(boardId); - return courseCommonCartridgeMetadata; + return boardSkeletonDto; } - public async findRoomBoardByCourseId(courseId: string): Promise { - const courseRooms = await this.courseRoomsClientAdapter.getRoomBoardByCourseId(courseId); + private async findAllCardsByIds(ids: Array): Promise { + const cardListResponseDto = await this.cardClientAdapter.getAllBoardCardsByIds(ids); - return courseRooms; + return cardListResponseDto; } - public async findAllCardsByIds(ids: Array): Promise { - const cards = await this.cardClientAdapter.getAllBoardCardsByIds(ids); + private async findLessonById(lessonId: string): Promise { + const lessonDto = await this.lessonClientAdapter.getLessonById(lessonId); - return cards; + return lessonDto; } } diff --git a/apps/server/src/modules/common-cartridge/service/common-cartridge.mapper.spec.ts b/apps/server/src/modules/common-cartridge/service/common-cartridge.mapper.spec.ts new file mode 100644 index 00000000000..a4bbafa1423 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/service/common-cartridge.mapper.spec.ts @@ -0,0 +1,375 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { faker } from '@faker-js/faker'; +import { CommonCartridgeExportMapper } from './common-cartridge.mapper'; +import { + boardTaskFactory, + courseMetadataFactory, + lessonContentFactory, + lessonFactory, + lessonLinkedTaskFactory, +} from '../testing/common-cartridge-dtos.factory'; +import { linkElementFactory } from '../testing/link-element.factory'; +import { richTextElementFactroy } from '../testing/rich-text-element.factory'; +import { + CommonCartridgeElementType, + CommonCartridgeIntendedUseType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '../export/common-cartridge.enums'; +import { ComponentGeogebraPropsDto } from '../common-cartridge-client/lesson-client/dto/component-geogebra-props.dto'; +import { ComponentEtherpadPropsDto } from '../common-cartridge-client/lesson-client/dto/component-etherpad-props.dto'; +import { createIdentifier } from '../export/utils'; +import { LessonContentResponseContentInnerDto } from '../common-cartridge-client/lesson-client/dto/lesson-content-response-inner.dto'; +import { + LessonContentDtoComponent, + LessonContentDtoComponentValues, +} from '../common-cartridge-client/lesson-client/dto'; + +const GEOGEBRA_BASE_URL = 'https://geogebra.org'; + +describe('CommonCartridgeExportMapper', () => { + let module: TestingModule; + let sut: CommonCartridgeExportMapper; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [CommonCartridgeExportMapper], + }).compile(); + + sut = module.get(CommonCartridgeExportMapper); + }); + + afterAll(async () => { + await module.close(); + }); + + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + + describe('mapCourseToManifest', () => { + const setup = () => { + const courseId = faker.string.uuid(); + const version = CommonCartridgeVersion.V_1_1_0; + return { courseId, version }; + }; + + describe('when mapping course to manifest', () => { + const { courseId, version } = setup(); + it('should map course to manifest', () => { + const result = sut.mapCourseToManifest(version, courseId); + + expect(result).toEqual({ + version, + identifier: createIdentifier(courseId), + }); + }); + }); + }); + + describe('mapCourseToMetadata', () => { + const setup = () => { + const courseMetadata = courseMetadataFactory.build(); + return { courseMetadata }; + }; + + describe('when mapping metadata of a course to DTO', () => { + const { courseMetadata } = setup(); + it('should map metadata to a CourseCommonCartridgeMetadataDto', () => { + const result = sut.mapCourseToMetadata(courseMetadata); + + expect(result).toEqual({ + type: CommonCartridgeElementType.METADATA, + title: courseMetadata.courseName, + copyrightOwners: courseMetadata.copyRightOwners, + creationDate: courseMetadata.creationDate ? new Date(courseMetadata.creationDate) : new Date(), + }); + }); + }); + }); + + describe('mapLessonToOrganization', () => { + const setup = () => { + const lesson = lessonFactory.build(); + + return { lesson }; + }; + + describe('when mapping lesson to organization', () => { + const { lesson } = setup(); + + it('should map lesson identifier and title to organization', () => { + const result = sut.mapLessonToOrganization(lesson); + + expect(result).toEqual({ + identifier: createIdentifier(lesson.lessonId), + title: lesson.name, + }); + }); + }); + }); + + describe('mapContentToResources', () => { + describe('when lesson content is GeoGebra', () => { + const setup = () => { + const lessonContent = lessonContentFactory.build(); + lessonContent.component = LessonContentDtoComponentValues.GEO_GEBRA; + lessonContent.content = { + materialId: faker.string.uuid(), + }; + return { lessonContent }; + }; + + it('should map lesson content to GeoGebra resources', () => { + const { lessonContent } = setup(); + const result = sut.mapContentToResources(lessonContent); + + expect(result).toEqual({ + type: CommonCartridgeResourceType.WEB_LINK, + identifier: `i${lessonContent.id ?? ''}`, + title: lessonContent.title, + url: `${GEOGEBRA_BASE_URL}/m/${(lessonContent.content as ComponentGeogebraPropsDto).materialId}`, + }); + }); + }); + + describe('when lesson content is Etherpad', () => { + const setup = () => { + const lessonContent = lessonContentFactory.build(); + lessonContent.component = LessonContentDtoComponentValues.ETHERPAD; + lessonContent.content = { + description: faker.lorem.sentence(), + title: faker.lorem.sentence(), + url: faker.internet.url(), + }; + return { lessonContent }; + }; + + it('should map lesson content to Etherpad resources', () => { + const { lessonContent } = setup(); + const result = sut.mapContentToResources(lessonContent); + + expect(result).toEqual({ + type: CommonCartridgeResourceType.WEB_LINK, + identifier: `i${lessonContent.id ?? ''}`, + title: `${(lessonContent.content as ComponentEtherpadPropsDto).title} - ${ + (lessonContent.content as ComponentEtherpadPropsDto).description + }`, + url: (lessonContent.content as ComponentEtherpadPropsDto).url, + }); + }); + }); + + describe('when lesson content is Lernstore', () => { + const setup = () => { + const lessonContent = lessonContentFactory.build(); + lessonContent.component = LessonContentDtoComponentValues.LERNSTORE; + lessonContent.content = { + resources: [faker.internet.url(), faker.internet.url()], + }; + return { lessonContent }; + }; + + it('should map lesson content to Lernstore resources', () => { + const { lessonContent } = setup(); + const result = sut.mapContentToResources(lessonContent); + + expect(result[0]).toEqual({ + type: CommonCartridgeResourceType.WEB_LINK, + identifier: `i${lessonContent.id ?? ''}`, + title: '', + url: '', + }); + }); + }); + + describe('when lesson has no content', () => { + const setup = () => { + const lessonContent = lessonContentFactory.build(); + lessonContent.content = {} as unknown as LessonContentResponseContentInnerDto; + lessonContent.component = {} as unknown as LessonContentDtoComponent; + + return { lessonContent }; + }; + + it('should return an empty array of contents', () => { + const { lessonContent } = setup(); + const result = sut.mapContentToResources(lessonContent); + + expect(result).toBeInstanceOf(Array); + }); + }); + }); + + describe('mapContentToOrganization', () => { + const setup = () => { + const lessonContent = lessonContentFactory.build(); + return { lessonContent }; + }; + describe('when mapping lesson to organization', () => { + it('should map lesson identifier and title to organization', () => { + const { lessonContent } = setup(); + const result = sut.mapContentToOrganization(lessonContent); + + expect(result).toEqual({ + identifier: `i${lessonContent.id ?? ''}`, + title: lessonContent.title, + }); + }); + }); + }); + + describe('mapTaskToResources', () => { + const setup = () => { + const task = boardTaskFactory.build(); + + return { task }; + }; + + describe('when mapping task to resources with version 1.1.0', () => { + const { task } = setup(); + it('should map task to resources with version 1.1.0', () => { + const result = sut.mapTaskToResource(task, CommonCartridgeVersion.V_1_1_0); + + expect(result).toEqual({ + identifier: `i${task.id}`, + title: task.name, + html: `

${task.name}

${task.description ?? ''}

`, + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + type: CommonCartridgeResourceType.WEB_CONTENT, + }); + }); + }); + + describe('when mapping task to resources with version 1.3.0', () => { + const { task } = setup(); + it('should map task to resources with version 1.3.0', () => { + const result = sut.mapTaskToResource(task, CommonCartridgeVersion.V_1_3_0); + + expect(result).toEqual({ + identifier: `i${task.id}`, + title: task.name, + html: `

${task.name}

${task.description ?? ''}

`, + intendedUse: CommonCartridgeIntendedUseType.ASSIGNMENT, + type: CommonCartridgeResourceType.WEB_CONTENT, + }); + }); + }); + + describe('when mapping task to resources with not supported version', () => { + const { task } = setup(); + it('should map task to resources with version 1.1.0 as default', () => { + const result = sut.mapTaskToResource(task, CommonCartridgeVersion.V_1_4_0); + + expect(result).toEqual({ + identifier: `i${task.id}`, + title: task.name, + html: `

${task.name}

${task.description ?? ''}

`, + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + type: CommonCartridgeResourceType.WEB_CONTENT, + }); + }); + }); + }); + + describe('mapLinkedTaskToResource', () => { + const setup = () => { + const linkedTask = lessonLinkedTaskFactory.build(); + + return { linkedTask }; + }; + + describe('when mapping linked task to resources with version 1.1.0', () => { + const { linkedTask } = setup(); + it('should map linked task to resources with version 1.1.0', () => { + const result = sut.mapLinkedTaskToResource(linkedTask, CommonCartridgeVersion.V_1_1_0); + + expect(result).toStrictEqual( + expect.objectContaining({ + type: CommonCartridgeResourceType.WEB_CONTENT, + title: linkedTask.name, + html: `

${linkedTask.name}

${linkedTask.description}

`, + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }) + ); + }); + }); + + describe('when mapping linked task to resources with version 1.3.0', () => { + const { linkedTask } = setup(); + it('should map linked task to resources with version 1.3.0', () => { + const result = sut.mapLinkedTaskToResource(linkedTask, CommonCartridgeVersion.V_1_3_0); + + expect(result).toStrictEqual( + expect.objectContaining({ + type: CommonCartridgeResourceType.WEB_CONTENT, + title: linkedTask.name, + html: `

${linkedTask.name}

${linkedTask.description}

`, + intendedUse: CommonCartridgeIntendedUseType.ASSIGNMENT, + }) + ); + }); + }); + + describe('when mapping linked task to resources with not supported version', () => { + const { linkedTask } = setup(); + it('should map linked task to resources with version 1.1.0 as default', () => { + const result = sut.mapLinkedTaskToResource(linkedTask, CommonCartridgeVersion.V_1_4_0); + + expect(result).toStrictEqual( + expect.objectContaining({ + type: CommonCartridgeResourceType.WEB_CONTENT, + title: linkedTask.name, + html: `

${linkedTask.name}

${linkedTask.description}

`, + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }) + ); + }); + }); + }); + + describe('mapRichTextElementToResource', () => { + const setup = () => { + const richTextElement = richTextElementFactroy.build(); + + return { richTextElement }; + }; + + describe('when mapping rich text element to resources', () => { + const { richTextElement } = setup(); + it('should map rich text element to resources', () => { + const result = sut.mapRichTextElementToResource(richTextElement); + + expect(result).toEqual({ + type: CommonCartridgeResourceType.WEB_CONTENT, + identifier: createIdentifier(richTextElement.id), + title: richTextElement.content.text, + html: `

${richTextElement.content.text}

`, + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }); + }); + }); + }); + + describe('mapLinkElementToResource', () => { + const setup = () => { + const linkElement = linkElementFactory.build(); + + return { linkElement }; + }; + + describe('when mapping link element to resources', () => { + const { linkElement } = setup(); + it('should map link element to resources', () => { + const result = sut.mapLinkElementToResource(linkElement); + + expect(result).toEqual({ + type: CommonCartridgeResourceType.WEB_LINK, + identifier: createIdentifier(linkElement.id), + title: linkElement.content.title, + url: linkElement.content.url, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/service/common-cartridge.mapper.ts b/apps/server/src/modules/common-cartridge/service/common-cartridge.mapper.ts new file mode 100644 index 00000000000..7e594081f99 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/service/common-cartridge.mapper.ts @@ -0,0 +1,203 @@ +import sanitizeHtml from 'sanitize-html'; +import { CourseCommonCartridgeMetadataDto } from '../common-cartridge-client/course-client'; +import { + LessonContentDto, + LessonContentDtoComponentValues, + LessonDto, + LessonLinkedTaskDto, +} from '../common-cartridge-client/lesson-client/dto'; +import { CommonCartridgeOrganizationProps } from '../export/builders/common-cartridge-file-builder'; +import { + CommonCartridgeElementType, + CommonCartridgeIntendedUseType, + CommonCartridgeResourceType, + CommonCartridgeVersion, +} from '../export/common-cartridge.enums'; +import { CommonCartridgeElementProps } from '../export/elements/common-cartridge-element-factory'; +import { createIdentifier } from '../export/utils'; +import { CommonCartridgeResourceProps } from '../export/resources/common-cartridge-resource-factory'; +import { BoardTaskDto } from '../common-cartridge-client/room-client/dto/board-task.dto'; +import { RichTextElementResponseDto } from '../common-cartridge-client/card-client/dto/rich-text-element-response.dto'; +import { LinkElementResponseDto } from '../common-cartridge-client/card-client/dto/link-element-response.dto'; +import { ComponentTextPropsDto } from '../common-cartridge-client/lesson-client/dto/component-text-props.dto'; +import { ComponentGeogebraPropsDto } from '../common-cartridge-client/lesson-client/dto/component-geogebra-props.dto'; +import { ComponentLernstorePropsDto } from '../common-cartridge-client/lesson-client/dto/component-lernstore-props.dto'; +import { ComponentEtherpadPropsDto } from '../common-cartridge-client/lesson-client/dto/component-etherpad-props.dto'; + +export class CommonCartridgeExportMapper { + private static readonly GEOGEBRA_BASE_URL: string = 'https://geogebra.org'; + + public mapCourseToManifest( + version: CommonCartridgeVersion, + courseId: string + ): { version: CommonCartridgeVersion; identifier: string } { + return { + version, + identifier: createIdentifier(courseId), + }; + } + + public mapCourseToMetadata(courseMetadata: CourseCommonCartridgeMetadataDto): CommonCartridgeElementProps { + return { + type: CommonCartridgeElementType.METADATA, + title: courseMetadata.courseName, + copyrightOwners: courseMetadata.copyRightOwners, + creationDate: courseMetadata.creationDate ? new Date(courseMetadata.creationDate) : new Date(), + }; + } + + public mapLessonToOrganization(lesson: LessonDto): CommonCartridgeOrganizationProps { + return { + identifier: createIdentifier(lesson.lessonId), + title: lesson.name, + }; + } + + public mapContentToResources( + lessonContent: LessonContentDto + ): CommonCartridgeResourceProps | CommonCartridgeResourceProps[] { + switch (lessonContent.component) { + case LessonContentDtoComponentValues.TEXT: + return { + type: CommonCartridgeResourceType.WEB_CONTENT, + identifier: createIdentifier(lessonContent.id), + title: lessonContent.title, + html: `

${lessonContent.title ?? ''}

${ + (lessonContent.content as ComponentTextPropsDto).text ?? '' + }

`, + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }; + case LessonContentDtoComponentValues.GEO_GEBRA: + return { + type: CommonCartridgeResourceType.WEB_LINK, + identifier: createIdentifier(lessonContent.id), + title: lessonContent.title, + url: `${CommonCartridgeExportMapper.GEOGEBRA_BASE_URL}/m/${ + (lessonContent.content as ComponentGeogebraPropsDto).materialId + }`, + }; + case LessonContentDtoComponentValues.ETHERPAD: + return { + type: CommonCartridgeResourceType.WEB_LINK, + identifier: createIdentifier(lessonContent.id), + title: `${(lessonContent.content as ComponentEtherpadPropsDto).title} - ${ + (lessonContent.content as ComponentEtherpadPropsDto).description + }`, + url: (lessonContent.content as ComponentEtherpadPropsDto).url, + }; + case LessonContentDtoComponentValues.LERNSTORE: { + const { resources } = lessonContent.content as ComponentLernstorePropsDto; + const extractedResources = this.extractResources(resources); + return ( + extractedResources.map((resource) => { + return { + type: CommonCartridgeResourceType.WEB_LINK, + identifier: createIdentifier(lessonContent.id), + title: resource.title, + url: resource.url, + }; + }) || [] + ); + } + default: + return []; + } + } + + // should be removed after fixing the issue with the Lernstore component + private extractResources(resources: string[]): { title: string; url: string }[] { + return resources.map((resource) => { + const fields = resource.split(',').map((field) => field.trim()); + let title = ''; + let url = ''; + + fields.forEach((field) => { + const [key, value] = field.split('=').map((part) => part.trim()); + if (key === 'title') title = value; + if (key === 'url') url = value; + }); + + return { title, url }; + }); + } + + public mapContentToOrganization(content: LessonContentDto): CommonCartridgeOrganizationProps { + return { + identifier: createIdentifier(content.id), + title: content.title, + }; + } + + public mapTaskToResource(task: BoardTaskDto, version: CommonCartridgeVersion): CommonCartridgeResourceProps { + const intendedUse = ((): CommonCartridgeIntendedUseType => { + switch (version) { + case CommonCartridgeVersion.V_1_1_0: + return CommonCartridgeIntendedUseType.UNSPECIFIED; + case CommonCartridgeVersion.V_1_3_0: + return CommonCartridgeIntendedUseType.ASSIGNMENT; + default: + return CommonCartridgeIntendedUseType.UNSPECIFIED; + } + })(); + + return { + type: CommonCartridgeResourceType.WEB_CONTENT, + identifier: createIdentifier(task.id), + title: task.name, + html: `

${task.name}

${task.description ?? ''}

`, + intendedUse, + }; + } + + public mapLinkedTaskToResource( + task: LessonLinkedTaskDto, + version: CommonCartridgeVersion + ): CommonCartridgeResourceProps { + const intendedUse = ((): CommonCartridgeIntendedUseType => { + switch (version) { + case CommonCartridgeVersion.V_1_1_0: + return CommonCartridgeIntendedUseType.UNSPECIFIED; + case CommonCartridgeVersion.V_1_3_0: + return CommonCartridgeIntendedUseType.ASSIGNMENT; + default: + return CommonCartridgeIntendedUseType.UNSPECIFIED; + } + })(); + + return { + type: CommonCartridgeResourceType.WEB_CONTENT, + identifier: createIdentifier(), + title: task.name, + html: `

${task.name}

${task.description}

`, + intendedUse, + }; + } + + public mapRichTextElementToResource(element: RichTextElementResponseDto): CommonCartridgeResourceProps { + return { + type: CommonCartridgeResourceType.WEB_CONTENT, + identifier: createIdentifier(element.id), + title: this.getTextTitle(element.content.text), + html: `

${element.content.text}

`, + intendedUse: CommonCartridgeIntendedUseType.UNSPECIFIED, + }; + } + + public mapLinkElementToResource(element: LinkElementResponseDto): CommonCartridgeResourceProps { + return { + type: CommonCartridgeResourceType.WEB_LINK, + identifier: createIdentifier(element.id), + title: element.content.title, + url: element.content.url, + }; + } + + private getTextTitle(text: string): string { + const title = sanitizeHtml(text, { + allowedTags: [], + allowedAttributes: {}, + }).slice(0, 20); + + return title.length > 20 ? `${title}...` : title; + } +} diff --git a/apps/server/src/modules/common-cartridge/testing/common-cartridge-dtos.factory.ts b/apps/server/src/modules/common-cartridge/testing/common-cartridge-dtos.factory.ts new file mode 100644 index 00000000000..f4978330dc3 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/testing/common-cartridge-dtos.factory.ts @@ -0,0 +1,200 @@ +import { faker } from '@faker-js/faker'; +import { Factory } from 'fishery'; +import { CourseCommonCartridgeMetadataDto } from '../common-cartridge-client/course-client'; +import { LessonContentDto, LessonDto, LessonLinkedTaskDto } from '../common-cartridge-client/lesson-client/dto'; +import { BoardSkeletonDto, CardSkeletonDto, ColumnSkeletonDto } from '../common-cartridge-client/board-client'; +import { CardListResponseDto } from '../common-cartridge-client/card-client/dto/card-list-response.dto'; +import { CardResponseDto } from '../common-cartridge-client/card-client/dto/card-response.dto'; +import { + RoomBoardDto, + BoardTaskStatusDto, + BoardTaskDto, + BoardLessonDto, + BoardColumnBoardDto, +} from '../common-cartridge-client/room-client/dto'; +import { BoardElementDtoType } from '../common-cartridge-client/room-client/enums/board-element.enum'; +import { BoardLayout } from '../common-cartridge-client/room-client/enums/board-layout.enum'; +import { richTextElementFactroy } from './rich-text-element.factory'; +import { linkElementFactory } from './link-element.factory'; + +export const courseMetadataFactory = Factory.define(({ sequence }) => { + return { + id: sequence.toString(), + courseName: faker.lorem.sentence(), + creationDate: faker.date.recent().toISOString(), + copyRightOwners: [faker.person.fullName(), faker.person.fullName()], + }; +}); + +export const cardFactory = Factory.define(({ sequence }) => { + return { + cardId: sequence.toString(), + height: faker.number.int(), + }; +}); + +export const columnFactory = Factory.define(({ sequence }) => { + return { + columnId: sequence.toString(), + title: faker.lorem.sentence(), + cards: [cardFactory.build(), cardFactory.build()], + }; +}); + +export const columnBoardFactory = Factory.define(({ sequence }) => { + return { + boardId: sequence.toString(), + title: faker.lorem.sentence(), + columns: [columnFactory.build(), columnFactory.build()], + isVisible: faker.datatype.boolean(), + layout: faker.lorem.word(), + }; +}); + +export const cardResponseFactory = Factory.define(({ sequence }) => { + return { + id: sequence.toString(), + height: faker.number.int(), + elements: [richTextElementFactroy.build(), linkElementFactory.build()], + visibilitySettings: { + publishedAt: faker.date.recent().toISOString(), + }, + timeStamps: { + lastUpdatedAt: faker.date.recent().toISOString(), + createdAt: faker.date.recent().toISOString(), + deletedAt: undefined, + }, + title: faker.lorem.sentence(), + }; +}); + +export const listOfCardResponseFactory = Factory.define(() => { + return { + data: [cardResponseFactory.build(), cardResponseFactory.build()], + }; +}); + +export const lessonLinkedTaskFactory = Factory.define(() => { + return { + name: faker.lorem.word(), + description: faker.lorem.paragraph(), + descriptionInputFormat: 'plainText', + availableDate: faker.date.recent().toISOString(), + dueDate: faker.date.future().toISOString(), + private: faker.datatype.boolean(), + publicSubmissions: faker.datatype.boolean(), + teamSubmissions: faker.datatype.boolean(), + creator: faker.internet.email(), + courseId: null, + submissionIds: [faker.string.uuid(), faker.string.uuid()], + finishedIds: [faker.string.uuid(), faker.string.uuid()], + }; +}); + +export const lernstoreContentFactory = Factory.define(({ sequence }) => { + return { + id: sequence.toString(), + type: 'lernstore', + content: { resources: [faker.internet.url(), faker.internet.url(), faker.internet.url()] }, + title: faker.lorem.sentence(), + component: 'lernstore', + hidden: faker.datatype.boolean(), + }; +}); + +export const lessonContentFactory = Factory.define(({ sequence }) => { + return { + id: sequence.toString(), + type: faker.lorem.word(), + content: { text: 'text' }, + title: faker.lorem.sentence(), + component: 'text', + hidden: faker.datatype.boolean(), + }; +}); + +export const lessonFactory = Factory.define(({ sequence }) => { + return { + lessonId: sequence.toString(), + name: faker.lorem.word(), + courseId: undefined, + courseGroupId: faker.string.uuid(), + hidden: faker.datatype.boolean(), + position: faker.number.int(), + contents: [lessonContentFactory.build(), lernstoreContentFactory.build()], + materials: [], + linkedTasks: [lessonLinkedTaskFactory.build(), lessonLinkedTaskFactory.build()], + }; +}); + +export const boardLessonFactory = Factory.define(() => { + return { + id: faker.string.uuid(), + name: faker.lorem.word(), + courseName: undefined, + hidden: faker.datatype.boolean(), + numberOfPublishedTasks: faker.number.int(), + numberOfDraftTasks: faker.number.int(), + numberOfPlannedTasks: faker.number.int(), + createdAt: faker.date.recent().toISOString(), + updatedAt: faker.date.recent().toISOString(), + }; +}); + +export const boardTaskFactory = Factory.define(({ sequence }) => { + return { + id: sequence.toString(), + name: faker.lorem.word(), + createdAt: faker.date.recent().toISOString(), + updatedAt: faker.date.recent().toISOString(), + availableDate: faker.date.recent().toISOString(), + courseName: undefined, + description: faker.lorem.word(), + displayColor: faker.lorem.word(), + dueDate: faker.date.recent().toISOString(), + status: new BoardTaskStatusDto({ + submitted: faker.number.int(), + maxSubmissions: faker.number.int(), + graded: faker.number.int(), + isDraft: faker.datatype.boolean(), + isSubstitutionTeacher: faker.datatype.boolean(), + isFinished: faker.datatype.boolean(), + }), + }; +}); + +export const boardCloumnBoardFactory = Factory.define(() => { + return { + id: faker.string.uuid(), + title: faker.lorem.word(), + published: faker.datatype.boolean(), + createdAt: faker.date.recent().toISOString(), + updatedAt: faker.date.recent().toISOString(), + columnBoardId: faker.string.uuid(), + layout: BoardLayout.COLUMNS, + }; +}); + +export const roomFactory = Factory.define(({ sequence }) => { + return { + roomId: sequence.toString(), + title: faker.lorem.word(), + displayColor: faker.lorem.word(), + elements: [ + { + type: BoardElementDtoType.TASK, + content: boardTaskFactory.build(), + }, + { + type: BoardElementDtoType.LESSON, + content: boardLessonFactory.build(), + }, + { + type: BoardElementDtoType.COLUMN_BOARD, + content: boardCloumnBoardFactory.build(), + }, + ], + isArchived: faker.datatype.boolean(), + isSynchronized: faker.datatype.boolean(), + }; +}); diff --git a/apps/server/src/modules/common-cartridge/testing/link-element.factory.ts b/apps/server/src/modules/common-cartridge/testing/link-element.factory.ts new file mode 100644 index 00000000000..726e711d765 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/testing/link-element.factory.ts @@ -0,0 +1,28 @@ +import { faker } from '@faker-js/faker'; +import { BaseFactory } from '@shared/testing'; +import { Factory } from 'fishery'; +import { LinkElementContentDto } from '../common-cartridge-client/card-client/dto/link-element-content.dto'; +import { LinkElementResponseDto } from '../common-cartridge-client/card-client/dto/link-element-response.dto'; +import { ContentElementType } from '../common-cartridge-client/card-client/enums/content-element-type.enum'; + +export const linkElementContentFactory = Factory.define(() => { + return { + url: faker.internet.url(), + title: faker.lorem.word(), + description: faker.lorem.sentence(), + }; +}); + +class LinkElementFactory extends BaseFactory> {} +export const linkElementFactory = LinkElementFactory.define(LinkElementResponseDto, () => { + return { + id: faker.string.uuid(), + type: ContentElementType.LINK, + content: linkElementContentFactory.build(), + timestamps: { + lastUpdatedAt: faker.date.recent().toISOString(), + createdAt: faker.date.recent().toISOString(), + deletedAt: undefined, + }, + }; +}); diff --git a/apps/server/src/modules/common-cartridge/testing/rich-text-element.factory.ts b/apps/server/src/modules/common-cartridge/testing/rich-text-element.factory.ts new file mode 100644 index 00000000000..01e880ae0de --- /dev/null +++ b/apps/server/src/modules/common-cartridge/testing/rich-text-element.factory.ts @@ -0,0 +1,27 @@ +import { BaseFactory } from '@shared/testing'; +import { faker } from '@faker-js/faker'; +import { Factory } from 'fishery'; +import { RichTextElementResponseDto } from '../common-cartridge-client/card-client/dto/rich-text-element-response.dto'; +import { RichTextElementContentDto } from '../common-cartridge-client/card-client/dto/rich-text-element-content.dto'; +import { ContentElementType } from '../common-cartridge-client/card-client/enums/content-element-type.enum'; + +export const richTextElementContentFactory = Factory.define(() => { + return { + text: faker.lorem.word(), + inputFormat: 'plainText', + }; +}); + +class RichTextElement extends BaseFactory> {} +export const richTextElementFactroy = RichTextElement.define(RichTextElementResponseDto, () => { + return { + id: faker.string.uuid(), + type: ContentElementType.RICH_TEXT, + content: richTextElementContentFactory.build(), + timestamps: { + lastUpdatedAt: faker.date.recent().toISOString(), + createdAt: faker.date.recent().toISOString(), + deletedAt: undefined, + }, + }; +}); diff --git a/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.spec.ts b/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.spec.ts index 8fc9bcba92b..f047c162ef2 100644 --- a/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.spec.ts +++ b/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.spec.ts @@ -1,10 +1,9 @@ import { faker } from '@faker-js/faker'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { CourseFileIdsResponse } from '../controller/dto'; import { CommonCartridgeExportService } from '../service/common-cartridge-export.service'; import { CommonCartridgeUc } from './common-cartridge.uc'; -import { CourseExportBodyResponse } from '../controller/dto/course-export-body.response'; +import { CommonCartridgeVersion } from '../export/common-cartridge.enums'; describe('CommonCartridgeUc', () => { let module: TestingModule; @@ -37,31 +36,28 @@ describe('CommonCartridgeUc', () => { describe('exportCourse', () => { const setup = () => { const courseId = faker.string.uuid(); - const expected = new CourseExportBodyResponse({ - courseFileIds: new CourseFileIdsResponse([]), - courseCommonCartridgeMetadata: { - id: courseId, - title: faker.lorem.sentence(), - copyRightOwners: [], - }, - }); + const version = CommonCartridgeVersion.V_1_1_0; + const topics = [faker.lorem.sentence(), faker.lorem.sentence()]; + const tasks = [faker.lorem.sentence(), faker.lorem.sentence()]; + const columnBoards = [faker.lorem.sentence(), faker.lorem.sentence()]; + const expected = Buffer.alloc(0); - commonCartridgeExportServiceMock.findCourseFileRecords.mockResolvedValue([]); - commonCartridgeExportServiceMock.findCourseCommonCartridgeMetadata.mockResolvedValue({ - id: expected.courseCommonCartridgeMetadata?.id ?? '', - title: expected.courseCommonCartridgeMetadata?.title ?? '', - copyRightOwners: expected.courseCommonCartridgeMetadata?.copyRightOwners ?? [], - }); + commonCartridgeExportServiceMock.exportCourse.mockResolvedValue(expected); - return { courseId, expected }; + return { courseId, version, topics, tasks, columnBoards, expected }; }; it('should return a course export response with file IDs and metadata of a course', async () => { - const { courseId, expected } = setup(); - - const result = await sut.exportCourse(courseId); - - expect(result).toEqual(expected); + const { courseId, expected, version, tasks, columnBoards, topics } = setup(); + + expect(await sut.exportCourse(courseId, version, topics, tasks, columnBoards)).toEqual(expected); + expect(commonCartridgeExportServiceMock.exportCourse).toHaveBeenCalledWith( + courseId, + version, + topics, + tasks, + columnBoards + ); }); }); }); diff --git a/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.ts b/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.ts index 8caa9381633..d7d00e6e02e 100644 --- a/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.ts +++ b/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.ts @@ -1,25 +1,21 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; -import { CourseFileIdsResponse } from '../controller/dto'; import { CommonCartridgeExportService } from '../service/common-cartridge-export.service'; -import { CourseExportBodyResponse } from '../controller/dto/course-export-body.response'; -import { CourseCommonCartridgeMetadataDto } from '../common-cartridge-client/course-client'; +import { CommonCartridgeVersion } from '../export/common-cartridge.enums'; @Injectable() export class CommonCartridgeUc { constructor(private readonly exportService: CommonCartridgeExportService) {} - public async exportCourse(courseId: EntityId): Promise { - const files = await this.exportService.findCourseFileRecords(courseId); - const courseFileIds = new CourseFileIdsResponse(files.map((file) => file.id)); - const courseCommonCartridgeMetadata: CourseCommonCartridgeMetadataDto = - await this.exportService.findCourseCommonCartridgeMetadata(courseId); + public async exportCourse( + courseId: EntityId, + version: CommonCartridgeVersion, + topics: string[], + tasks: string[], + columnBoards: string[] + ): Promise { + const exportedCourse = await this.exportService.exportCourse(courseId, version, topics, tasks, columnBoards); - const response = new CourseExportBodyResponse({ - courseFileIds, - courseCommonCartridgeMetadata, - }); - - return response; + return exportedCourse; } } diff --git a/apps/server/src/modules/learnroom/controller/dto/course-export.body.params.ts b/apps/server/src/modules/learnroom/controller/dto/course-export.body.params.ts index 1edbcf7869e..a2e86355e8a 100644 --- a/apps/server/src/modules/learnroom/controller/dto/course-export.body.params.ts +++ b/apps/server/src/modules/learnroom/controller/dto/course-export.body.params.ts @@ -1,8 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsArray } from 'class-validator'; +import { IsArray, IsString } from 'class-validator'; export class CourseExportBodyParams { @IsArray() + @IsString({ each: true }) @ApiProperty({ description: 'The list of ids of topics which should be exported. If empty no topics are exported.', type: [String], @@ -10,14 +11,15 @@ export class CourseExportBodyParams { public readonly topics!: string[]; @IsArray() + @IsString({ each: true }) @ApiProperty({ description: 'The list of ids of tasks which should be exported. If empty no tasks are exported.', type: [String], }) public readonly tasks!: string[]; - // AI next 6 lines @IsArray() + @IsString({ each: true }) @ApiProperty({ description: 'The list of ids of column boards which should be exported. If empty no column boards are exported.', type: [String], diff --git a/apps/server/src/modules/lesson/controller/dto/lesson-content.response.ts b/apps/server/src/modules/lesson/controller/dto/lesson-content.response.ts index 0af0d4006ba..3a52a17b47b 100644 --- a/apps/server/src/modules/lesson/controller/dto/lesson-content.response.ts +++ b/apps/server/src/modules/lesson/controller/dto/lesson-content.response.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'; import { EntityId } from '@shared/domain/types'; import { ComponentEtherpadProperties, @@ -11,6 +11,65 @@ import { ComponentType, } from '@shared/domain/entity/lesson.entity'; +// eslint problem will be solved in EW-1090 +class ComponentTextPropsImpl implements ComponentTextProperties { + @ApiProperty({ nullable: false }) + text!: string; +} + +class ComponentEtherpadPropsImpl implements ComponentEtherpadProperties { + @ApiProperty({ nullable: false, description: 'description of a Etherpad component' }) + description!: string; + + @ApiProperty({ nullable: false, description: 'title of a Etherpad component' }) + title!: string; + + @ApiProperty({ nullable: false, description: 'url of a Etherpad component' }) + url!: string; +} + +class ComponentNexboardPropsImpl implements ComponentNexboardProperties { + @ApiProperty({ nullable: false, description: 'board of a Nexboard component' }) + board!: string; + + @ApiProperty({ nullable: false, description: 'description of a Nexboard component' }) + description!: string; + + @ApiProperty({ nullable: false, description: 'title of a Nexboard component' }) + title!: string; + + @ApiProperty({ nullable: false, description: 'url of a Nexboard component' }) + url!: string; +} + +class ComponentGeogebraPropsImpl implements ComponentGeogebraProperties { + @ApiProperty({ nullable: false, description: 'materialId of a Geogebra component' }) + materialId!: string; +} + +class ComponentInternalPropsImpl implements ComponentInternalProperties { + @ApiProperty({ nullable: false, description: 'url of a Internal component' }) + url!: string; +} + +class ComponentLernstorePropsImpl implements ComponentLernstoreProperties { + @ApiProperty({ nullable: false, description: 'resources of a Lernstore component' }) + resources!: { + client: string; + description: string; + merlinReference?: string; + title: string; + url: string; + }[]; +} +@ApiExtraModels( + ComponentTextPropsImpl, + ComponentEtherpadPropsImpl, + ComponentGeogebraPropsImpl, + ComponentInternalPropsImpl, + ComponentLernstorePropsImpl, + ComponentNexboardPropsImpl +) export class LessonContentResponse { constructor(lessonContent: ComponentProperties) { this.id = lessonContent._id; @@ -22,14 +81,23 @@ export class LessonContentResponse { this.content = lessonContent.content; } - @ApiProperty() + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(ComponentTextPropsImpl) }, + { $ref: getSchemaPath(ComponentEtherpadPropsImpl) }, + { $ref: getSchemaPath(ComponentGeogebraPropsImpl) }, + { $ref: getSchemaPath(ComponentInternalPropsImpl) }, + { $ref: getSchemaPath(ComponentLernstorePropsImpl) }, + { $ref: getSchemaPath(ComponentNexboardPropsImpl) }, + ], + }) content?: - | ComponentTextProperties - | ComponentEtherpadProperties - | ComponentGeogebraProperties - | ComponentInternalProperties - | ComponentLernstoreProperties - | ComponentNexboardProperties; + | ComponentTextPropsImpl + | ComponentEtherpadPropsImpl + | ComponentGeogebraPropsImpl + | ComponentInternalPropsImpl + | ComponentLernstorePropsImpl + | ComponentNexboardPropsImpl; @ApiProperty({ description: 'The id of the Material entity', diff --git a/openapitools.json b/openapitools.json index 386abefb9e9..e074aa9db9f 100644 --- a/openapitools.json +++ b/openapitools.json @@ -28,6 +28,21 @@ "withSeparateModelsAndApi": true } }, + "svs-lesson-api": { + "generatorName": "typescript-axios", + "inputSpec": "http://localhost:3030/api/v3/docs-json", + "output": "./apps/server/src/modules/common-cartridge/common-cartridge-client/lesson-client/new-lesson-api-client", + "skipValidateSpec": true, + "enablePostProcessFile": true, + "openapiNormalizer": { + "FILTER": "operationId:LessonController_getLesson|LessonController_getLessonTasks" + }, + "globalProperty": { + "models": "LessonResponse:LessonLinkedTaskResponse:LessonContentResponse:ComponentTextPropsImpl:ComponentEtherpadPropsImpl:ComponentGeogebraPropsImpl:ComponentInternalPropsImpl:ComponentLernstorePropsImpl:ComponentNexboardPropsImpl", + "apis": "", + "supportingFiles": "" + } + }, "tldraw-api": { "generatorName": "typescript-axios", "inputSpec": "http://localhost:3349/docs-json", diff --git a/package.json b/package.json index 1a09275bc37..06a9cc0204b 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "generate-client:etherpad": "node ./scripts/generate-client.js -u 'http://localhost:9001/api/openapi.json' -p 'apps/server/src/infra/etherpad-client/etherpad-api-client' -c 'openapitools-config.json'", "pregenerate-client:tsp-api": "rimraf ./apps/server/src/infra/tsp-client/generated", "generate-client:tsp-api": "openapi-generator-cli generate -c ./openapitools.json --generator-key tsp-api", + "generate-client:lessons-api":"openapi-generator-cli generate -c ./openapitools.json --generator-key svs-lesson-api", "pregenerate-client:tldraw-api": "rimraf ./apps/server/src/infra/tldraw-client/generated", "generate-client:tldraw-api": "openapi-generator-cli generate -c ./openapitools.json --generator-key tldraw-api" },