diff --git a/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts index f6bcd16fd24..0f7a3795580 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts @@ -12,7 +12,7 @@ import { UserAndAccountTestFactory, } from '@shared/testing'; import { ServerTestModule } from '@src/modules/server/server.module'; -import { AnyContentElementResponse } from '../dto'; +import { AnyContentElementResponse, SubmissionContainerElementResponse } from '../dto'; const baseRouteName = '/cards'; @@ -91,7 +91,7 @@ describe(`content element create (api)`, () => { expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.EXTERNAL_TOOL); }); - it('should return the created content element of type SUBMISSION_CONTAINER', async () => { + it('should return the created content element of type SUBMISSION_CONTAINER with dueDate set to null', async () => { const { loggedInClient, cardNode } = await setup(); const response = await loggedInClient.post(`${cardNode.id}/elements`, { @@ -99,6 +99,7 @@ describe(`content element create (api)`, () => { }); expect((response.body as AnyContentElementResponse).type).toEqual(ContentElementType.SUBMISSION_CONTAINER); + expect((response.body as SubmissionContainerElementResponse).content.dueDate).toBeNull(); }); it('should actually create the content element', async () => { diff --git a/apps/server/src/modules/board/controller/api-test/content-element-update-content.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-update-content.spec.ts index 09e3f8c046b..bee1ad63f0f 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-update-content.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-update-content.spec.ts @@ -8,10 +8,9 @@ import { FileElementNode, InputFormat, RichTextElementNode, + SubmissionContainerElementNode, } from '@shared/domain'; import { - TestApiClient, - UserAndAccountTestFactory, cardNodeFactory, cleanupCollections, columnBoardNodeFactory, @@ -19,6 +18,9 @@ import { courseFactory, fileElementNodeFactory, richTextElementNodeFactory, + submissionContainerElementNodeFactory, + TestApiClient, + UserAndAccountTestFactory, } from '@shared/testing'; import { ServerTestModule } from '@src/modules/server/server.module'; @@ -59,8 +61,15 @@ describe(`content element update content (api)`, () => { const column = columnNodeFactory.buildWithId({ parent: columnBoardNode }); const parentCard = cardNodeFactory.buildWithId({ parent: column }); - const richTextelement = richTextElementNodeFactory.buildWithId({ parent: parentCard }); + const richTextElement = richTextElementNodeFactory.buildWithId({ parent: parentCard }); const fileElement = fileElementNodeFactory.buildWithId({ parent: parentCard }); + const submissionContainerElement = submissionContainerElementNodeFactory.buildWithId({ parent: parentCard }); + + const tomorrow = new Date(Date.now() + 86400000); + const submissionContainerElementWithDueDate = submissionContainerElementNodeFactory.buildWithId({ + parent: parentCard, + dueDate: tomorrow, + }); await em.persistAndFlush([ teacherAccount, @@ -68,20 +77,28 @@ describe(`content element update content (api)`, () => { parentCard, column, columnBoardNode, - richTextelement, + richTextElement, fileElement, + submissionContainerElement, + submissionContainerElementWithDueDate, ]); em.clear(); const loggedInClient = await testApiClient.login(teacherAccount); - return { loggedInClient, richTextelement, fileElement }; + return { + loggedInClient, + richTextElement, + fileElement, + submissionContainerElement, + submissionContainerElementWithDueDate, + }; }; it('should return status 204', async () => { - const { loggedInClient, richTextelement } = await setup(); + const { loggedInClient, richTextElement } = await setup(); - const response = await loggedInClient.patch(`${richTextelement.id}/content`, { + const response = await loggedInClient.patch(`${richTextElement.id}/content`, { data: { content: { text: 'hello world', inputFormat: InputFormat.RICH_TEXT_CK5 }, type: ContentElementType.RICH_TEXT, @@ -92,30 +109,30 @@ describe(`content element update content (api)`, () => { }); it('should actually change content of the element', async () => { - const { loggedInClient, richTextelement } = await setup(); + const { loggedInClient, richTextElement } = await setup(); - await loggedInClient.patch(`${richTextelement.id}/content`, { + await loggedInClient.patch(`${richTextElement.id}/content`, { data: { content: { text: 'hello world', inputFormat: InputFormat.RICH_TEXT_CK5 }, type: ContentElementType.RICH_TEXT, }, }); - const result = await em.findOneOrFail(RichTextElementNode, richTextelement.id); + const result = await em.findOneOrFail(RichTextElementNode, richTextElement.id); expect(result.text).toEqual('hello world'); }); it('should sanitize rich text before changing content of the element', async () => { - const { loggedInClient, richTextelement } = await setup(); + const { loggedInClient, richTextElement } = await setup(); const text = ' some more text'; const sanitizedText = sanitizeRichText(text, InputFormat.RICH_TEXT_CK5); - await loggedInClient.patch(`${richTextelement.id}/content`, { + await loggedInClient.patch(`${richTextElement.id}/content`, { data: { content: { text, inputFormat: InputFormat.RICH_TEXT_CK5 }, type: ContentElementType.RICH_TEXT }, }); - const result = await em.findOneOrFail(RichTextElementNode, richTextelement.id); + const result = await em.findOneOrFail(RichTextElementNode, richTextElement.id); expect(result.text).toEqual(sanitizedText); }); @@ -146,6 +163,76 @@ describe(`content element update content (api)`, () => { expect(result.alternativeText).toEqual('rich text 1 some more text'); }); + + it('should return status 204 (nothing changed) without dueDate parameter for submission container element', async () => { + const { loggedInClient, submissionContainerElement } = await setup(); + + const response = await loggedInClient.patch(`${submissionContainerElement.id}/content`, { + data: { + content: {}, + type: 'submissionContainer', + }, + }); + + expect(response.statusCode).toEqual(204); + }); + + it('should not change dueDate value without dueDate parameter for submission container element', async () => { + const { loggedInClient, submissionContainerElement } = await setup(); + + await loggedInClient.patch(`${submissionContainerElement.id}/content`, { + data: { + content: {}, + type: 'submissionContainer', + }, + }); + const result = await em.findOneOrFail(SubmissionContainerElementNode, submissionContainerElement.id); + + expect(result.dueDate).toBeUndefined(); + }); + + it('should set dueDate value when dueDate parameter is provided for submission container element', async () => { + const { loggedInClient, submissionContainerElement } = await setup(); + + const inThreeDays = new Date(Date.now() + 259200000); + + await loggedInClient.patch(`${submissionContainerElement.id}/content`, { + data: { + content: { dueDate: inThreeDays }, + type: 'submissionContainer', + }, + }); + const result = await em.findOneOrFail(SubmissionContainerElementNode, submissionContainerElement.id); + + expect(result.dueDate).toEqual(inThreeDays); + }); + + it('should unset dueDate value when dueDate parameter is not provided for submission container element', async () => { + const { loggedInClient, submissionContainerElementWithDueDate } = await setup(); + + await loggedInClient.patch(`${submissionContainerElementWithDueDate.id}/content`, { + data: { + content: {}, + type: 'submissionContainer', + }, + }); + const result = await em.findOneOrFail(SubmissionContainerElementNode, submissionContainerElementWithDueDate.id); + + expect(result.dueDate).toBeUndefined(); + }); + + it('should return status 400 for wrong date format for submission container element', async () => { + const { loggedInClient, submissionContainerElement } = await setup(); + + const response = await loggedInClient.patch(`${submissionContainerElement.id}/content`, { + data: { + content: { dueDate: 'hello world' }, + type: 'submissionContainer', + }, + }); + + expect(response.statusCode).toEqual(400); + }); }); describe('with invalid user', () => { @@ -163,24 +250,38 @@ describe(`content element update content (api)`, () => { const column = columnNodeFactory.buildWithId({ parent: columnBoardNode }); const parentCard = cardNodeFactory.buildWithId({ parent: column }); - const element = richTextElementNodeFactory.buildWithId({ parent: parentCard }); + const richTextElement = richTextElementNodeFactory.buildWithId({ parent: parentCard }); + const submissionContainerElement = submissionContainerElementNodeFactory.buildWithId({ parent: parentCard }); - await em.persistAndFlush([parentCard, column, columnBoardNode, element]); + await em.persistAndFlush([parentCard, column, columnBoardNode, richTextElement, submissionContainerElement]); em.clear(); const loggedInClient = await testApiClient.login(invalidTeacherAccount); - return { loggedInClient, element }; + return { loggedInClient, richTextElement, submissionContainerElement }; }; - it('should return status 403', async () => { - const { loggedInClient, element } = await setup(); + it('should return status 403 for rich text element', async () => { + const { loggedInClient, richTextElement } = await setup(); - const response = await loggedInClient.patch(`${element.id}/content`, { + const response = await loggedInClient.patch(`${richTextElement.id}/content`, { data: { content: { text: 'hello world', inputFormat: InputFormat.RICH_TEXT_CK5 }, type: 'richText' }, }); expect(response.statusCode).toEqual(HttpStatus.FORBIDDEN); }); + + it('should return status 403 for submission container element', async () => { + const { loggedInClient, submissionContainerElement } = await setup(); + + const response = await loggedInClient.patch(`${submissionContainerElement.id}/content`, { + data: { + content: {}, + type: 'submissionContainer', + }, + }); + + expect(response.statusCode).toEqual(HttpStatus.FORBIDDEN); + }); }); }); diff --git a/apps/server/src/modules/board/controller/dto/card/card.response.ts b/apps/server/src/modules/board/controller/dto/card/card.response.ts index 8ff034ad093..44ee426fb6b 100644 --- a/apps/server/src/modules/board/controller/dto/card/card.response.ts +++ b/apps/server/src/modules/board/controller/dto/card/card.response.ts @@ -1,6 +1,6 @@ import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; -import { AnyContentElementResponse } from '../element'; +import { AnyContentElementResponse, FileElementResponse, SubmissionContainerElementResponse } from '../element'; import { RichTextElementResponse } from '../element/rich-text-element.response'; import { TimestampsResponse } from '../timestamps.response'; import { VisibilitySettingsResponse } from './visibility-settings.response'; @@ -31,7 +31,11 @@ export class CardResponse { @ApiProperty({ type: 'array', items: { - oneOf: [{ $ref: getSchemaPath(RichTextElementResponse) }], + oneOf: [ + { $ref: getSchemaPath(RichTextElementResponse) }, + { $ref: getSchemaPath(FileElementResponse) }, + { $ref: getSchemaPath(SubmissionContainerElementResponse) }, + ], }, }) elements: AnyContentElementResponse[]; diff --git a/apps/server/src/modules/board/controller/dto/element/submission-container-element.response.ts b/apps/server/src/modules/board/controller/dto/element/submission-container-element.response.ts index 642d5e44818..e6f0d1364ef 100644 --- a/apps/server/src/modules/board/controller/dto/element/submission-container-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/submission-container-element.response.ts @@ -7,8 +7,12 @@ export class SubmissionContainerElementContent { this.dueDate = dueDate; } - @ApiProperty() - dueDate: Date; + @ApiProperty({ + type: Date, + description: 'The dueDate as date string or null of not set', + example: '2023-08-17T14:17:51.958+00:00', + }) + dueDate: Date | null; } export class SubmissionContainerElementResponse { diff --git a/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts index 1f2a320119e..05856e9ef5f 100644 --- a/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts +++ b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts @@ -53,8 +53,12 @@ export class RichTextElementContentBody extends ElementContentBody { export class SubmissionContainerContentBody { @IsDate() - @ApiProperty() - dueDate!: Date; + @IsOptional() + @ApiPropertyOptional({ + required: false, + description: 'The point in time until when a submission can be handed in.', + }) + dueDate?: Date; } export class SubmissionContainerElementContentBody extends ElementContentBody { diff --git a/apps/server/src/modules/board/controller/mapper/submission-container-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/submission-container-element-response.mapper.ts index 30acafa298f..8b3dc6ae54f 100644 --- a/apps/server/src/modules/board/controller/mapper/submission-container-element-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/submission-container-element-response.mapper.ts @@ -18,9 +18,15 @@ export class SubmissionContainerElementResponseMapper implements BaseResponseMap id: element.id, timestamps: new TimestampsResponse({ lastUpdatedAt: element.updatedAt, createdAt: element.createdAt }), type: ContentElementType.SUBMISSION_CONTAINER, - content: new SubmissionContainerElementContent({ dueDate: element.dueDate }), + content: new SubmissionContainerElementContent({ + dueDate: element.dueDate || null, + }), }); + if (element.dueDate) { + result.content = new SubmissionContainerElementContent({ dueDate: element.dueDate }); + } + return result; } diff --git a/apps/server/src/modules/board/repo/board-do.builder-impl.ts b/apps/server/src/modules/board/repo/board-do.builder-impl.ts index 87dc4382798..af58280b33f 100644 --- a/apps/server/src/modules/board/repo/board-do.builder-impl.ts +++ b/apps/server/src/modules/board/repo/board-do.builder-impl.ts @@ -125,11 +125,15 @@ export class BoardDoBuilderImpl implements BoardDoBuilder { const element = new SubmissionContainerElement({ id: boardNode.id, - dueDate: boardNode.dueDate, children: elements, createdAt: boardNode.createdAt, updatedAt: boardNode.updatedAt, }); + + if (boardNode.dueDate) { + element.dueDate = boardNode.dueDate; + } + return element; } diff --git a/apps/server/src/modules/board/repo/recursive-save.visitor.ts b/apps/server/src/modules/board/repo/recursive-save.visitor.ts index 082cc73c4f4..5561e636267 100644 --- a/apps/server/src/modules/board/repo/recursive-save.visitor.ts +++ b/apps/server/src/modules/board/repo/recursive-save.visitor.ts @@ -128,11 +128,14 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { const boardNode = new SubmissionContainerElementNode({ id: submissionContainerElement.id, - dueDate: submissionContainerElement.dueDate, parent: parentData?.boardNode, position: parentData?.position, }); + if (submissionContainerElement.dueDate) { + boardNode.dueDate = submissionContainerElement.dueDate; + } + this.createOrUpdateBoardNode(boardNode); this.visitChildren(submissionContainerElement, boardNode); } diff --git a/apps/server/src/modules/board/service/content-element-update.visitor.ts b/apps/server/src/modules/board/service/content-element-update.visitor.ts index d660fbee98c..dfd430aa250 100644 --- a/apps/server/src/modules/board/service/content-element-update.visitor.ts +++ b/apps/server/src/modules/board/service/content-element-update.visitor.ts @@ -59,7 +59,7 @@ export class ContentElementUpdateVisitor implements BoardCompositeVisitor { visitSubmissionContainerElement(submissionContainerElement: SubmissionContainerElement): void { if (this.content instanceof SubmissionContainerContentBody) { - submissionContainerElement.dueDate = this.content.dueDate; + submissionContainerElement.dueDate = this.content.dueDate ?? undefined; } else { this.throwNotHandled(submissionContainerElement); } diff --git a/apps/server/src/shared/domain/domainobject/board/content-element.factory.ts b/apps/server/src/shared/domain/domainobject/board/content-element.factory.ts index ea268206559..fb476d2dbd0 100644 --- a/apps/server/src/shared/domain/domainobject/board/content-element.factory.ts +++ b/apps/server/src/shared/domain/domainobject/board/content-element.factory.ts @@ -63,10 +63,8 @@ export class ContentElementFactory { } private buildSubmissionContainer() { - const tomorrow = new Date(Date.now() + 86400000); const element = new SubmissionContainerElement({ id: new ObjectId().toHexString(), - dueDate: tomorrow, children: [], createdAt: new Date(), updatedAt: new Date(), diff --git a/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.ts b/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.ts index 3b9a85600c6..09756153a90 100644 --- a/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.ts +++ b/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.ts @@ -3,11 +3,11 @@ import { SubmissionItem } from './submission-item.do'; import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; export class SubmissionContainerElement extends BoardComposite { - get dueDate(): Date { + get dueDate(): Date | undefined { return this.props.dueDate; } - set dueDate(value: Date) { + set dueDate(value: Date | undefined) { this.props.dueDate = value; } @@ -26,7 +26,7 @@ export class SubmissionContainerElement extends BoardComposite(SubmissionContainerElementNode, () => { - const inThreeDays = new Date(Date.now() + 259200000); - return { - dueDate: inThreeDays, - }; + return {}; });