Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-4453 - submission item file #4484

Merged
merged 32 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ad71650
BC-4983 - board: POC elements as submission item children
virgilchiriac Sep 12, 2023
46bcb87
Merge branch 'main' into BC-4983-submission-item-children
virgilchiriac Sep 27, 2023
c488b6f
cleanup
virgilchiriac Sep 27, 2023
f331eb0
Merge branch 'main' into BC-4983-submission-item-children
virgilchiriac Oct 2, 2023
5e99894
BC-4983 - update sub element
virgilchiriac Oct 4, 2023
0152043
Merge branch 'main' into BC-4983-submission-item-children
virgilchiriac Oct 13, 2023
dbf2267
refactor UC and cleanup
virgilchiriac Oct 13, 2023
f01161a
Merge branch 'main' into BC-4983-submission-item-children
virgilchiriac Oct 17, 2023
4acf205
BC-4453 - more cleanup
virgilchiriac Oct 18, 2023
bb1405f
Merge branch 'main' into BC-4453-submission-item-file
virgilchiriac Oct 20, 2023
4db00a2
more refactoring - adds a column uc
virgilchiriac Oct 23, 2023
ba6ee8c
Merge branch 'main' into BC-4453-submission-item-file
virgilchiriac Oct 23, 2023
04bca14
fix linter
virgilchiriac Oct 23, 2023
eb12e3e
Merge branch 'main' into BC-4453-submission-item-file
virgilchiriac Oct 23, 2023
2b8cfd8
fix weird path making tests fail
virgilchiriac Oct 23, 2023
e2ec1f3
restrict returned response type
virgilchiriac Oct 23, 2023
9f13542
add uc unit tests
virgilchiriac Oct 23, 2023
1ebbf3e
Merge branch 'main' into BC-4453-submission-item-file
virgilchiriac Oct 23, 2023
73595f4
add tests
virgilchiriac Oct 24, 2023
ace6d5c
fix api test
virgilchiriac Oct 24, 2023
7878a95
more tests and some refactoring
virgilchiriac Oct 24, 2023
ad7e79c
fix lint
virgilchiriac Oct 24, 2023
22ada1b
Merge branch 'main' into BC-4453-submission-item-file
virgilchiriac Oct 26, 2023
34592eb
cleanup and adding missing api tests
virgilchiriac Oct 26, 2023
df70336
Merge branch 'main' into BC-4453-submission-item-file
virgilchiriac Oct 27, 2023
49cdaca
Merge branch 'main' into BC-4453-submission-item-file
virgilchiriac Oct 30, 2023
2a8eb54
Merge branch 'main' into BC-4453-submission-item-file
virgilchiriac Nov 1, 2023
58ef6ea
Merge branch 'main' into BC-4453-submission-item-file
virgilchiriac Nov 2, 2023
28c04fe
Merge branch 'main' into BC-4453-submission-item-file
virgilchiriac Nov 3, 2023
9e592d1
undo api prop which was breaking client tests
virgilchiriac Nov 3, 2023
92c2600
Merge branch 'main' into BC-4453-submission-item-file
virgilchiriac Nov 3, 2023
3dfb9db
Merge branch 'main' into BC-4453-submission-item-file
virgilchiriac Nov 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/server/src/modules/board/board-api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import {
ColumnController,
ElementController,
} from './controller';
import { BoardUc, CardUc } from './uc';
import { BoardUc, CardUc, ColumnUc } from './uc';
import { ElementUc } from './uc/element.uc';
import { SubmissionItemUc } from './uc/submission-item.uc';

@Module({
imports: [BoardModule, LoggerModule, forwardRef(() => AuthorizationModule)],
controllers: [BoardController, ColumnController, CardController, ElementController, BoardSubmissionController],
providers: [BoardUc, CardUc, ElementUc, SubmissionItemUc],
providers: [BoardUc, ColumnUc, CardUc, ElementUc, SubmissionItemUc],
})
export class BoardApiModule {}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ describe('submission create (api)', () => {
expect(response.status).toBe(201);

const response2 = await loggedInClient.post(`${submissionContainerNode.id}/submissions`, { completed: false });
expect(response2.status).toBe(406);
expect(response2.status).toBe(403);
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { EntityManager } from '@mikro-orm/mongodb';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { BoardExternalReferenceType } from '@shared/domain';
import { BoardExternalReferenceType, ContentElementType } from '@shared/domain';
import {
TestApiClient,
UserAndAccountTestFactory,
cardNodeFactory,
cleanupCollections,
columnBoardNodeFactory,
columnNodeFactory,
courseFactory,
fileElementNodeFactory,
richTextElementNodeFactory,
submissionContainerElementNodeFactory,
submissionItemNodeFactory,
TestApiClient,
UserAndAccountTestFactory,
userFactory,
} from '@shared/testing';
import { ServerTestModule } from '@modules/server';
Expand Down Expand Up @@ -257,4 +259,117 @@ describe('submission item lookup (api)', () => {
expect(response.status).toEqual(403);
});
});

describe('when submission item has child elements', () => {
describe('when submission item has a RICH_TEXT child element', () => {
const setup = async () => {
await cleanupCollections(em);

const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent();
const course = courseFactory.build({ teachers: [teacherUser], students: [studentUser] });
await em.persistAndFlush([studentAccount, studentUser, teacherAccount, teacherUser, course]);

const columnBoardNode = columnBoardNodeFactory.buildWithId({
context: { id: course.id, type: BoardExternalReferenceType.Course },
});

const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode });

const cardNode = cardNodeFactory.buildWithId({ parent: columnNode });

const submissionContainer = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode });
const submissionItem = submissionItemNodeFactory.buildWithId({
parent: submissionContainer,
userId: studentUser.id,
});
const richTextElement = richTextElementNodeFactory.buildWithId({ parent: submissionItem });

await em.persistAndFlush([
columnBoardNode,
columnNode,
cardNode,
submissionContainer,
submissionItem,
richTextElement,
]);

const loggedInClient = await testApiClient.login(studentAccount);

return {
loggedInClient,
submissionContainer,
submissionItem,
richTextElement,
};
};

it('should return all RICH_TEXT child elements', async () => {
const { loggedInClient, submissionContainer, richTextElement } = await setup();

const response = await loggedInClient.get(`${submissionContainer.id}`);
const submissionItemResponse = (response.body as SubmissionsResponse).submissionItemsResponse[0];
const richTextElementResponse = submissionItemResponse.elements.filter(
(element) => element.type === ContentElementType.RICH_TEXT
);

expect(richTextElementResponse[0].id).toEqual(richTextElement.id);
});
});

describe('when submission item has a FILE child element', () => {
const setup = async () => {
await cleanupCollections(em);

const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent();
const course = courseFactory.build({ teachers: [teacherUser], students: [studentUser] });
await em.persistAndFlush([studentAccount, studentUser, teacherAccount, teacherUser, course]);

const columnBoardNode = columnBoardNodeFactory.buildWithId({
context: { id: course.id, type: BoardExternalReferenceType.Course },
});

const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode });

const cardNode = cardNodeFactory.buildWithId({ parent: columnNode });

const submissionContainer = submissionContainerElementNodeFactory.buildWithId({ parent: cardNode });
const submissionItem = submissionItemNodeFactory.buildWithId({
parent: submissionContainer,
userId: studentUser.id,
});
const fileElement = fileElementNodeFactory.buildWithId({ parent: submissionItem });

await em.persistAndFlush([
columnBoardNode,
columnNode,
cardNode,
submissionContainer,
submissionItem,
fileElement,
]);

const loggedInClient = await testApiClient.login(studentAccount);

return {
loggedInClient,
submissionContainer,
submissionItem,
fileElement,
};
};
it('should return all FILE child elements', async () => {
const { loggedInClient, submissionContainer, fileElement } = await setup();

const response = await loggedInClient.get(`${submissionContainer.id}`);
const submissionItemResponse = (response.body as SubmissionsResponse).submissionItemsResponse[0];
const fileElementResponse = submissionItemResponse.elements.filter(
(element) => element.type === ContentElementType.FILE
);

expect(fileElementResponse[0].id).toEqual(fileElement.id);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import { Body, Controller, ForbiddenException, Get, HttpCode, NotFoundException, Param, Patch } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import {
Body,
Controller,
ForbiddenException,
Get,
HttpCode,
NotFoundException,
Param,
Patch,
Post,
} from '@nestjs/common';
import { ApiExtraModels, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from '@nestjs/swagger';
import { ApiValidationError } from '@shared/common';
import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication';
import { SubmissionsResponse } from './dto/submission-item/submissions.response';
import { CardUc } from '../uc';
import { ElementUc } from '../uc/element.uc';
import { SubmissionItemUc } from '../uc/submission-item.uc';
import { SubmissionContainerUrlParams, SubmissionItemUrlParams, UpdateSubmissionItemBodyParams } from './dto';
import { SubmissionItemResponseMapper } from './mapper';
import {
CreateContentElementBodyParams,
FileElementResponse,
RichTextElementResponse,
SubmissionContainerUrlParams,
SubmissionItemUrlParams,
UpdateSubmissionItemBodyParams,
} from './dto';
import { ContentElementResponseFactory, SubmissionItemResponseMapper } from './mapper';

@ApiTags('Board Submission')
@Authenticate('jwt')
Expand Down Expand Up @@ -56,4 +73,28 @@ export class BoardSubmissionController {
bodyParams.completed
);
}

@ApiOperation({ summary: 'Create a new element in a submission item.' })
@ApiExtraModels(RichTextElementResponse, FileElementResponse)
@ApiResponse({
status: 201,
schema: {
oneOf: [{ $ref: getSchemaPath(RichTextElementResponse) }, { $ref: getSchemaPath(FileElementResponse) }],
},
})
@ApiResponse({ status: 400, type: ApiValidationError })
@ApiResponse({ status: 403, type: ForbiddenException })
@ApiResponse({ status: 404, type: NotFoundException })
@Post(':submissionItemId/elements')
async createElement(
@Param() urlParams: SubmissionItemUrlParams,
@Body() bodyParams: CreateContentElementBodyParams,
@CurrentUser() currentUser: ICurrentUser
virgilchiriac marked this conversation as resolved.
Show resolved Hide resolved
): Promise<FileElementResponse | RichTextElementResponse> {
const { type } = bodyParams;
const element = await this.submissionItemUc.createElement(currentUser.userId, urlParams.submissionItemId, type);
const response = ContentElementResponseFactory.mapSubmissionContentToResponse(element);

return response;
}
}
12 changes: 6 additions & 6 deletions apps/server/src/modules/board/controller/card.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { ApiExtraModels, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from '@nestjs/swagger';
import { ApiValidationError } from '@shared/common';
import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication';
import { BoardUc, CardUc } from '../uc';
import { CardUc, ColumnUc } from '../uc';
import {
AnyContentElementResponse,
CardIdsParams,
Expand All @@ -37,7 +37,7 @@ import { CardResponseMapper, ContentElementResponseFactory } from './mapper';
@Authenticate('jwt')
@Controller('cards')
export class CardController {
constructor(private readonly boardUc: BoardUc, private readonly cardUc: CardUc) {}
constructor(private readonly columnUc: ColumnUc, private readonly cardUc: CardUc) {}

@ApiOperation({ summary: 'Get a list of cards by their ids.' })
@ApiResponse({ status: 200, type: CardListResponse })
Expand Down Expand Up @@ -70,7 +70,7 @@ export class CardController {
@Body() bodyParams: MoveCardBodyParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<void> {
await this.boardUc.moveCard(currentUser.userId, urlParams.cardId, bodyParams.toColumnId, bodyParams.toPosition);
await this.columnUc.moveCard(currentUser.userId, urlParams.cardId, bodyParams.toColumnId, bodyParams.toPosition);
}

@ApiOperation({ summary: 'Update the height of a single card.' })
Expand All @@ -85,7 +85,7 @@ export class CardController {
@Body() bodyParams: SetHeightBodyParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<void> {
await this.boardUc.updateCardHeight(currentUser.userId, urlParams.cardId, bodyParams.height);
await this.cardUc.updateCardHeight(currentUser.userId, urlParams.cardId, bodyParams.height);
}

@ApiOperation({ summary: 'Update the title of a single card.' })
Expand All @@ -100,7 +100,7 @@ export class CardController {
@Body() bodyParams: RenameBodyParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<void> {
await this.boardUc.updateCardTitle(currentUser.userId, urlParams.cardId, bodyParams.title);
await this.cardUc.updateCardTitle(currentUser.userId, urlParams.cardId, bodyParams.title);
}

@ApiOperation({ summary: 'Delete a single card.' })
Expand All @@ -111,7 +111,7 @@ export class CardController {
@HttpCode(204)
@Delete(':cardId')
async deleteCard(@Param() urlParams: CardUrlParams, @CurrentUser() currentUser: ICurrentUser): Promise<void> {
await this.boardUc.deleteCard(currentUser.userId, urlParams.cardId);
await this.cardUc.deleteCard(currentUser.userId, urlParams.cardId);
}

@ApiOperation({ summary: 'Create a new element on a card.' })
Expand Down
10 changes: 5 additions & 5 deletions apps/server/src/modules/board/controller/column.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ApiValidationError } from '@shared/common';
import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication';
import { BoardUc } from '../uc';
import { BoardUc, ColumnUc } from '../uc';
import { CardResponse, ColumnUrlParams, MoveColumnBodyParams, RenameBodyParams } from './dto';
import { CardResponseMapper } from './mapper';
import { CreateCardBodyParams } from './dto/card/create-card.body.params';
Expand All @@ -22,7 +22,7 @@ import { CreateCardBodyParams } from './dto/card/create-card.body.params';
@Authenticate('jwt')
@Controller('columns')
export class ColumnController {
constructor(private readonly boardUc: BoardUc) {}
constructor(private readonly boardUc: BoardUc, private readonly columnUc: ColumnUc) {}

@ApiOperation({ summary: 'Move a single column.' })
@ApiResponse({ status: 204 })
Expand Down Expand Up @@ -51,7 +51,7 @@ export class ColumnController {
@Body() bodyParams: RenameBodyParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<void> {
await this.boardUc.updateColumnTitle(currentUser.userId, urlParams.columnId, bodyParams.title);
await this.columnUc.updateColumnTitle(currentUser.userId, urlParams.columnId, bodyParams.title);
}

@ApiOperation({ summary: 'Delete a single column.' })
Expand All @@ -62,7 +62,7 @@ export class ColumnController {
@HttpCode(204)
@Delete(':columnId')
async deleteColumn(@Param() urlParams: ColumnUrlParams, @CurrentUser() currentUser: ICurrentUser): Promise<void> {
await this.boardUc.deleteColumn(currentUser.userId, urlParams.columnId);
await this.columnUc.deleteColumn(currentUser.userId, urlParams.columnId);
}

@ApiOperation({ summary: 'Create a new card on a column.' })
Expand All @@ -78,7 +78,7 @@ export class ColumnController {
@Body() createCardBodyParams?: CreateCardBodyParams
): Promise<CardResponse> {
const { requiredEmptyElements } = createCardBodyParams || {};
const card = await this.boardUc.createCard(currentUser.userId, urlParams.columnId, requiredEmptyElements);
const card = await this.columnUc.createCard(currentUser.userId, urlParams.columnId, requiredEmptyElements);

const response = CardResponseMapper.mapToResponse(card);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ export type AnyContentElementResponse =
| RichTextElementResponse
| SubmissionContainerElementResponse
| ExternalToolElementResponse;

export const isFileElementResponse = (element: AnyContentElementResponse): element is FileElementResponse =>
element instanceof FileElementResponse;

export const isRichTextElementResponse = (element: AnyContentElementResponse): element is RichTextElementResponse =>
element instanceof RichTextElementResponse;
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger';
import { ContentElementType, InputFormat } from '@shared/domain';
import { ContentElementType } from '@shared/domain';
import { InputFormat } from '@shared/domain/types';
import { Type } from 'class-transformer';
import { IsDate, IsEnum, IsMongoId, IsOptional, IsString, ValidateNested } from 'class-validator';

export abstract class ElementContentBody {
@IsEnum(ContentElementType)
@ApiProperty({
enum: ContentElementType,
description: 'the type of the updated element',
enumName: 'ContentElementType',
})
@IsEnum(ContentElementType)
type!: ContentElementType;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
import { TimestampsResponse } from '../timestamps.response';
import { FileElementResponse, RichTextElementResponse } from '../element';

@ApiExtraModels(FileElementResponse, RichTextElementResponse)
export class SubmissionItemResponse {
constructor({ id, timestamps, completed, userId }: SubmissionItemResponse) {
constructor({ id, timestamps, completed, userId, elements }: SubmissionItemResponse) {
this.id = id;
this.timestamps = timestamps;
this.completed = completed;
this.userId = userId;
this.elements = elements;
}

@ApiProperty({ pattern: '[a-f0-9]{24}' })
Expand All @@ -20,4 +23,12 @@ export class SubmissionItemResponse {

@ApiProperty({ pattern: '[a-f0-9]{24}' })
userId: string;

@ApiProperty({
type: 'array',
items: {
oneOf: [{ $ref: getSchemaPath(FileElementResponse) }, { $ref: getSchemaPath(RichTextElementResponse) }],
},
})
elements: (RichTextElementResponse | FileElementResponse)[];
}
Loading
Loading