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 {};
});