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-4983 - submission item children #4452

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
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,14 +1,32 @@
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 } from '@src/modules/authentication';
import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator';
import { SubmissionsResponse } from '@src/modules/board/controller/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 {
AnyContentElementResponse,
CreateContentElementBodyParams,
FileElementResponse,
RichTextElementResponse,
SubmissionContainerUrlParams,
SubmissionItemUrlParams,
UpdateSubmissionItemBodyParams,
} from './dto';
import { ContentElementResponseFactory, SubmissionItemResponseMapper } from './mapper';

@ApiTags('Board Submission')
@Authenticate('jwt')
Expand Down Expand Up @@ -57,4 +75,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
): Promise<AnyContentElementResponse> {
const { type } = bodyParams;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be not any content element response

const element = await this.submissionItemUc.createElement(currentUser.userId, urlParams.submissionItemId, type);
const response = ContentElementResponseFactory.mapToResponse(element);

return response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ export class RichTextContentBody {
text!: string;

@IsEnum(InputFormat)
@ApiProperty()
@ApiProperty({
enum: InputFormat,
})
inputFormat!: InputFormat;
}

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

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 +22,10 @@ export class SubmissionItemResponse {

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

@ApiProperty({
type: 'array',
// TODO add types
virgilchiriac marked this conversation as resolved.
Show resolved Hide resolved
})
elements: (RichTextElementResponse | FileElementResponse)[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class ElementController {
bodyParams.completed
);
const mapper = SubmissionItemResponseMapper.getInstance();
const response = mapper.mapSubmissionsToResponse(submissionItem);
const response = mapper.mapSubmissionItemToResponse(submissionItem);

return response;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { SubmissionItem, UserBoardRoles } from '@shared/domain';
import {
FileElement,
isFileElement,
isRichTextElement,
isSubmissionItemContent,
RichTextElement,
SubmissionItem,
UserBoardRoles,
} from '@shared/domain';
import { UnprocessableEntityException } from '@nestjs/common';
import { FileElementResponseMapper } from './file-element-response.mapper';
import { RichTextElementResponseMapper } from './rich-text-element-response.mapper';
import { SubmissionsResponse } from '../dto/submission-item/submissions.response';
import { SubmissionItemResponse, TimestampsResponse, UserDataResponse } from '../dto';

Expand All @@ -15,7 +26,7 @@ export class SubmissionItemResponseMapper {

public mapToResponse(submissionItems: SubmissionItem[], users: UserBoardRoles[]): SubmissionsResponse {
const submissionItemsResponse: SubmissionItemResponse[] = submissionItems.map((item) =>
this.mapSubmissionsToResponse(item)
this.mapSubmissionItemToResponse(item)
);
const usersResponse: UserDataResponse[] = users.map((user) => this.mapUsersToResponse(user));

Expand All @@ -24,7 +35,8 @@ export class SubmissionItemResponseMapper {
return response;
}

public mapSubmissionsToResponse(submissionItem: SubmissionItem): SubmissionItemResponse {
public mapSubmissionItemToResponse(submissionItem: SubmissionItem): SubmissionItemResponse {
const children: (FileElement | RichTextElement)[] = submissionItem.children.filter(isSubmissionItemContent);
const result = new SubmissionItemResponse({
completed: submissionItem.completed,
id: submissionItem.id,
Expand All @@ -33,6 +45,17 @@ export class SubmissionItemResponseMapper {
createdAt: submissionItem.createdAt,
}),
userId: submissionItem.userId,
elements: children.map((element) => {
if (isFileElement(element)) {
const mapper = FileElementResponseMapper.getInstance();
return mapper.mapToResponse(element);
}
if (isRichTextElement(element)) {
const mapper = RichTextElementResponseMapper.getInstance();
return mapper.mapToResponse(element);
}
throw new UnprocessableEntityException();
}),
});

return result;
Expand Down
8 changes: 6 additions & 2 deletions apps/server/src/modules/board/repo/board-do.builder-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,19 @@ export class BoardDoBuilderImpl implements BoardDoBuilder {
}

public buildSubmissionItem(boardNode: SubmissionItemNode): SubmissionItem {
this.ensureLeafNode(boardNode);
this.ensureBoardNodeType(this.getChildren(boardNode), [
BoardNodeType.FILE_ELEMENT,
BoardNodeType.RICH_TEXT_ELEMENT,
]);
const elements = this.buildChildren<RichTextElement | FileElement>(boardNode);

const element = new SubmissionItem({
id: boardNode.id,
createdAt: boardNode.createdAt,
updatedAt: boardNode.updatedAt,
completed: boardNode.completed,
userId: boardNode.userId,
children: [],
children: elements,
});
return element;
}
Expand Down
39 changes: 19 additions & 20 deletions apps/server/src/modules/board/repo/recursive-save.visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor {
context: columnBoard.context,
});

this.createOrUpdateBoardNode(boardNode);
this.visitChildren(columnBoard, boardNode);
this.saveRecursive(boardNode, columnBoard);
}

visitColumn(column: Column): void {
Expand All @@ -76,8 +75,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor {
position: parentData?.position,
});

this.createOrUpdateBoardNode(boardNode);
this.visitChildren(column, boardNode);
this.saveRecursive(boardNode, column);
}

visitCard(card: Card): void {
Expand All @@ -91,8 +89,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor {
position: parentData?.position,
});

this.createOrUpdateBoardNode(boardNode);
this.visitChildren(card, boardNode);
this.saveRecursive(boardNode, card);
}

visitFileElement(fileElement: FileElement): void {
Expand All @@ -106,8 +103,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor {
position: parentData?.position,
});

this.createOrUpdateBoardNode(boardNode);
this.visitChildren(fileElement, boardNode);
this.saveRecursive(boardNode, fileElement);
}

visitLinkElement(linkElement: LinkElement): void {
Expand Down Expand Up @@ -137,8 +133,7 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor {
position: parentData?.position,
});

this.createOrUpdateBoardNode(boardNode);
this.visitChildren(richTextElement, boardNode);
this.saveRecursive(boardNode, richTextElement);
}

visitSubmissionContainerElement(submissionContainerElement: SubmissionContainerElement): void {
Expand All @@ -151,22 +146,20 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor {
dueDate: submissionContainerElement.dueDate,
});

this.createOrUpdateBoardNode(boardNode);
this.visitChildren(submissionContainerElement, boardNode);
this.saveRecursive(boardNode, submissionContainerElement);
}

visitSubmissionItem(submission: SubmissionItem): void {
const parentData = this.parentsMap.get(submission.id);
visitSubmissionItem(submissionItem: SubmissionItem): void {
const parentData = this.parentsMap.get(submissionItem.id);
const boardNode = new SubmissionItemNode({
id: submission.id,
id: submissionItem.id,
parent: parentData?.boardNode,
position: parentData?.position,
completed: submission.completed,
userId: submission.userId,
completed: submissionItem.completed,
userId: submissionItem.userId,
});

this.createOrUpdateBoardNode(boardNode);
this.visitChildren(submission, boardNode);
this.saveRecursive(boardNode, submissionItem);
}

visitExternalToolElement(externalToolElement: ExternalToolElement): void {
Expand All @@ -192,14 +185,20 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor {
});
}

registerParentData(parent: AnyBoardDo, child: AnyBoardDo, parentNode: BoardNode) {
private registerParentData(parent: AnyBoardDo, child: AnyBoardDo, parentNode: BoardNode) {
const position = parent.children.findIndex((obj) => obj.id === child.id);
if (position === -1) {
throw new Error(`Cannot get child position. Child doesnt belong to parent`);
}
this.parentsMap.set(child.id, { boardNode: parentNode, position });
}

private saveRecursive(boardNode: BoardNode, anyBoardDo: AnyBoardDo): void {
this.createOrUpdateBoardNode(boardNode);
this.visitChildren(anyBoardDo, boardNode);
}

// TODO make private
createOrUpdateBoardNode(boardNode: BoardNode): void {
const existing = this.em.getUnitOfWork().getById<BoardNode>(BoardNode.name, boardNode.id);
if (existing) {
Expand Down
12 changes: 11 additions & 1 deletion apps/server/src/modules/board/service/content-element.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import {
AnyBoardDo,
AnyContentElementDo,
Card,
ContentElementFactory,
ContentElementType,
EntityId,
isAnyContentElement,
SubmissionItem,
} from '@shared/domain';
import { AnyElementContentBody } from '../controller/dto';
import { BoardDoRepo } from '../repo';
Expand All @@ -32,7 +34,15 @@ export class ContentElementService {
return element;
}

async create(parent: Card, type: ContentElementType): Promise<AnyContentElementDo> {
async findParentOfId(elementId: EntityId): Promise<AnyBoardDo> {
const parent = await this.boardDoRepo.findParentOfId(elementId);
if (!parent) {
throw new NotFoundException('There is no node with this id');
}
return parent;
}

async create(parent: Card | SubmissionItem, type: ContentElementType): Promise<AnyContentElementDo> {
const element = this.contentElementFactory.build(type);
parent.addChild(element);
await this.boardDoRepo.save(parent.children, parent);
Expand Down
51 changes: 51 additions & 0 deletions apps/server/src/modules/board/uc/base.uc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { AnyBoardDo, EntityId, SubmissionItem, UserRoleEnum } from '@shared/domain';
import { ForbiddenException, forwardRef, Inject } from '@nestjs/common';
import { Action, AuthorizationService } from '../../authorization';
import { BoardDoAuthorizableService } from '../service';

export abstract class BaseUc {
constructor(
@Inject(forwardRef(() => AuthorizationService))
protected readonly authorizationService: AuthorizationService,
protected readonly boardDoAuthorizableService: BoardDoAuthorizableService
) {}

protected async checkPermission(
userId: EntityId,
boardDo: AnyBoardDo,
action: Action,
requiredUserRole?: UserRoleEnum
): Promise<void> {
const user = await this.authorizationService.getUserWithPermissions(userId);
const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(boardDo);
if (requiredUserRole) {
boardDoAuthorizable.requiredUserRole = requiredUserRole;
}
const context = { action, requiredPermissions: [] };

return this.authorizationService.checkPermission(user, boardDoAuthorizable, context);
}

protected async isAuthorizedStudent(userId: EntityId, boardDo: AnyBoardDo): Promise<boolean> {
const boardDoAuthorizable = await this.boardDoAuthorizableService.getBoardAuthorizable(boardDo);
const userRoleEnum = boardDoAuthorizable.users.find((u) => u.userId === userId)?.userRoleEnum;

if (!userRoleEnum) {
throw new ForbiddenException('User not part of this board');
}

// TODO do this with permission instead of role and using authorizable rules
if (userRoleEnum === UserRoleEnum.STUDENT) {
return true;
}

return false;
}

protected async checkSubmissionItemEditPermission(userId: EntityId, submissionItem: SubmissionItem) {
if (submissionItem.userId !== userId) {
throw new ForbiddenException();
}
await this.checkPermission(userId, submissionItem, Action.read, UserRoleEnum.STUDENT);
}
}
Loading
Loading