Skip to content

Commit

Permalink
BC-4374 - make submission due date optional (#4378)
Browse files Browse the repository at this point in the history
For the response FE needs the dueDate to be set 
so that vue can properly watch it.
Therefor, the response will set it to null if undefined.

Co-authored-by: virgilchiriac <[email protected]>
  • Loading branch information
MartinSchuhmacher and virgilchiriac authored Sep 25, 2023
1 parent ee43c77 commit 4e3b8b9
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -91,14 +91,15 @@ 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`, {
type: ContentElementType.SUBMISSION_CONTAINER,
});

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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ import {
FileElementNode,
InputFormat,
RichTextElementNode,
SubmissionContainerElementNode,
} from '@shared/domain';
import {
TestApiClient,
UserAndAccountTestFactory,
cardNodeFactory,
cleanupCollections,
columnBoardNodeFactory,
columnNodeFactory,
courseFactory,
fileElementNodeFactory,
richTextElementNodeFactory,
submissionContainerElementNodeFactory,
TestApiClient,
UserAndAccountTestFactory,
} from '@shared/testing';
import { ServerTestModule } from '@src/modules/server/server.module';

Expand Down Expand Up @@ -59,29 +61,44 @@ 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,
teacherUser,
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,
Expand All @@ -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 = '<iframe>rich text 1</iframe> 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);
});
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/modules/board/repo/board-do.builder-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
5 changes: 4 additions & 1 deletion apps/server/src/modules/board/repo/recursive-save.visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Loading

0 comments on commit 4e3b8b9

Please sign in to comment.