Skip to content

Commit

Permalink
BC-8168 - Implementing video conferences in FE and remaining issues (#…
Browse files Browse the repository at this point in the history
…5420)

* implement feature check by board context

* implement board context features service

---------

Co-authored-by: Martin Schuhmacher <[email protected]>
Co-authored-by: virgilchiriac <[email protected]>
  • Loading branch information
3 people authored Jan 17, 2025
1 parent 0a87425 commit 8b2c27d
Show file tree
Hide file tree
Showing 25 changed files with 647 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { RoomModule } from '../room';
import { BoardContextApiHelperService } from './board-context-api-helper.service';
import { BoardModule } from '../board/board.module';
import { LearnroomModule } from '../learnroom';
import { LegacySchoolModule } from '../legacy-school';

@Module({
imports: [BoardModule, LearnroomModule, RoomModule],
imports: [BoardModule, LearnroomModule, RoomModule, LegacySchoolModule],
providers: [BoardContextApiHelperService],
exports: [BoardContextApiHelperService],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import { createMock } from '@golevelup/ts-jest';
import { AnyBoardNode, BoardExternalReferenceType, BoardNodeService } from '@modules/board';
import { CourseService } from '@modules/learnroom';
import { RoomService } from '@modules/room';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { CourseFeatures } from '@shared/domain/entity';
import { courseFactory } from '@testing/factory/course.factory';
import { schoolEntityFactory } from '@testing/factory/school-entity.factory';
import { setupEntities } from '@testing/setup-entities';
import { BoardFeature } from '../board/domain';
import { cardFactory, columnBoardFactory, columnFactory } from '../board/testing';
import { LegacySchoolService } from '../legacy-school';
import { roomFactory } from '../room/testing';
import { VideoConferenceConfig } from '../video-conference';
import { BoardContextApiHelperService } from './board-context-api-helper.service';

describe('BoardContextApiHelperService', () => {
Expand All @@ -16,6 +21,8 @@ describe('BoardContextApiHelperService', () => {
let courseService: jest.Mocked<CourseService>;
let roomService: jest.Mocked<RoomService>;
let boardNodeService: jest.Mocked<BoardNodeService>;
let legacySchoolService: jest.Mocked<LegacySchoolService>;
let configService: jest.Mocked<ConfigService<VideoConferenceConfig, true>>;

beforeEach(async () => {
await setupEntities();
Expand All @@ -34,13 +41,23 @@ describe('BoardContextApiHelperService', () => {
provide: BoardNodeService,
useValue: createMock<BoardNodeService>(),
},
{
provide: LegacySchoolService,
useValue: createMock<LegacySchoolService>(),
},
{
provide: ConfigService,
useValue: createMock<ConfigService>(),
},
],
}).compile();

service = module.get<BoardContextApiHelperService>(BoardContextApiHelperService);
courseService = module.get(CourseService);
roomService = module.get(RoomService);
boardNodeService = module.get(BoardNodeService);
legacySchoolService = module.get(LegacySchoolService);
configService = module.get(ConfigService);
});

afterAll(async () => {
Expand All @@ -55,16 +72,14 @@ describe('BoardContextApiHelperService', () => {
it('should return schoolId for course context', async () => {
const school = schoolEntityFactory.build();
const course = courseFactory.build({ school });
const cardNode = cardFactory.build();
const columnNode = columnFactory.build();
columnNode.addChild(cardNode);
const card = cardFactory.build();
const column = columnFactory.build({ children: [card] });
const columnBoard = columnBoardFactory.build({
context: { type: BoardExternalReferenceType.Course, id: 'course.id' },
});
columnBoard.addChild(columnNode);
columnBoard.addChild(column);

boardNodeService.findById.mockResolvedValueOnce(cardNode);
boardNodeService.findRoot.mockResolvedValueOnce(columnBoard);
boardNodeService.findById.mockResolvedValueOnce(card);
boardNodeService.findByClassAndId.mockResolvedValueOnce(columnBoard);
courseService.findById.mockResolvedValueOnce(course);

Expand All @@ -89,4 +104,151 @@ describe('BoardContextApiHelperService', () => {
expect(result).toBe(room.schoolId);
});
});

describe('getFeaturesForBoardNode', () => {
describe('when context is course', () => {
const setup = () => {
const course = courseFactory.build();
const column = columnFactory.build();
const columnBoard = columnBoardFactory.build({
context: { type: BoardExternalReferenceType.Course, id: 'course.id' },
children: [column],
});

courseService.findById.mockResolvedValueOnce(course);
boardNodeService.findById.mockResolvedValueOnce(column);
boardNodeService.findByClassAndId.mockResolvedValueOnce(columnBoard);

return { boardNode: column, course };
};

describe('when video conference is enabled for course', () => {
describe('and video conference is enabled for school and config', () => {
it('should return video conference feature', async () => {
const { boardNode, course } = setup();

course.features = [CourseFeatures.VIDEOCONFERENCE];
legacySchoolService.hasFeature.mockResolvedValueOnce(true);
configService.get.mockReturnValueOnce(true);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]);
});
});

describe('and video conference is disabled for school', () => {
it('should not return feature', async () => {
const { boardNode, course } = setup();

course.features = [CourseFeatures.VIDEOCONFERENCE];
legacySchoolService.hasFeature.mockResolvedValueOnce(false);
configService.get.mockReturnValueOnce(true);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([]);
});
});

describe('and video conference is disabled for config', () => {
it('should not return feature', async () => {
const { boardNode, course } = setup();

course.features = [CourseFeatures.VIDEOCONFERENCE];
legacySchoolService.hasFeature.mockResolvedValueOnce(true);
configService.get.mockReturnValueOnce(false);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([]);
});
});
});

describe('when video conference is disabled for course', () => {
it('should not return feature', async () => {
const { boardNode } = setup();

const course = courseFactory.build();
courseService.findById.mockResolvedValueOnce(course);
legacySchoolService.hasFeature.mockResolvedValueOnce(true);
configService.get.mockReturnValueOnce(true);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([]);
});
});
});

describe('when context is room', () => {
const setup = () => {
const room = roomFactory.build();
const column = columnFactory.build();
const columnBoard = columnBoardFactory.build({
context: { type: BoardExternalReferenceType.Room, id: 'room.id' },
children: [column],
});

roomService.getSingleRoom.mockResolvedValueOnce(room);
boardNodeService.findById.mockResolvedValueOnce(column);
boardNodeService.findByClassAndId.mockResolvedValueOnce(columnBoard);

return { boardNode: column, room };
};

describe('when video conference is enabled for school and config', () => {
it('should return video conference feature', async () => {
const { boardNode } = setup();

legacySchoolService.hasFeature.mockResolvedValueOnce(true);
configService.get.mockReturnValueOnce(true);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([BoardFeature.VIDEOCONFERENCE]);
});
});

describe('when video conference is disabled for school', () => {
it('should not return feature', async () => {
const { boardNode } = setup();

legacySchoolService.hasFeature.mockResolvedValueOnce(false);
configService.get.mockReturnValueOnce(true);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([]);
});
});

describe('when video conference is disabled for config', () => {
it('should not return feature', async () => {
const { boardNode } = setup();

legacySchoolService.hasFeature.mockResolvedValueOnce(true);
configService.get.mockReturnValueOnce(false);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([]);
});
});

describe('when video conference is disabled entirely', () => {
it('should not return feature', async () => {
const { boardNode } = setup();

legacySchoolService.hasFeature.mockResolvedValueOnce(false);
configService.get.mockReturnValueOnce(false);

const result = await service.getFeaturesForBoardNode(boardNode.id);

expect(result).toEqual([]);
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,26 +1,43 @@
import { BoardExternalReference, BoardExternalReferenceType, BoardNodeService, ColumnBoard } from '@modules/board';
import { CourseService } from '@modules/learnroom';
import { RoomService } from '@modules/room';
import { Injectable } from '@nestjs/common';
import { EntityId } from '@shared/domain/types';
import { BadRequestException, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { CourseFeatures } from '@shared/domain/entity';
import { EntityId, SchoolFeature } from '@shared/domain/types';
import { BoardFeature } from '../board/domain';
import { LegacySchoolService } from '../legacy-school';
import { VideoConferenceConfig } from '../video-conference';

@Injectable()
export class BoardContextApiHelperService {
constructor(
private readonly courseService: CourseService,
private readonly roomService: RoomService,
private readonly boardNodeService: BoardNodeService
private readonly boardNodeService: BoardNodeService,
private readonly legacySchoolService: LegacySchoolService,
private readonly configService: ConfigService<VideoConferenceConfig, true>
) {}

public async getSchoolIdForBoardNode(nodeId: EntityId): Promise<EntityId> {
const boardNode = await this.boardNodeService.findById(nodeId);
const board = await this.boardNodeService.findRoot(boardNode);
const columnBoard = await this.boardNodeService.findByClassAndId(ColumnBoard, board.id);
const schoolId = await this.getSchoolIdForBoard(columnBoard.context);
const boardContext = await this.getBoardContext(nodeId);
const schoolId = await this.getSchoolIdForBoardContext(boardContext);
return schoolId;
}

private async getSchoolIdForBoard(context: BoardExternalReference): Promise<EntityId> {
public async getFeaturesForBoardNode(nodeId: EntityId): Promise<BoardFeature[]> {
const boardContext = await this.getBoardContext(nodeId);
const features = await this.getFeaturesForBoardContext(boardContext);
return features;
}

private async getBoardContext(nodeId: EntityId): Promise<BoardExternalReference> {
const boardNode = await this.boardNodeService.findById(nodeId, 0);
const columnBoard = await this.boardNodeService.findByClassAndId(ColumnBoard, boardNode.rootId, 0);
return columnBoard.context;
}

private async getSchoolIdForBoardContext(context: BoardExternalReference): Promise<EntityId> {
if (context.type === BoardExternalReferenceType.Course) {
const course = await this.courseService.findById(context.id);

Expand All @@ -35,4 +52,47 @@ export class BoardContextApiHelperService {
/* istanbul ignore next */
throw new Error(`Unsupported board reference type ${context.type as string}`);
}

private async getFeaturesForBoardContext(context: BoardExternalReference): Promise<BoardFeature[]> {
const features: BoardFeature[] = [];

if (context.type === BoardExternalReferenceType.Course) {
const course = await this.courseService.findById(context.id);

if (
this.isVideoConferenceEnabledForConfig() &&
(await this.isVideoConferenceEnabledForSchool(course.school.id)) &&
this.isVideoConferenceEnabledForCourse(course.features)
) {
features.push(BoardFeature.VIDEOCONFERENCE);
}

return features;
}

if (context.type === BoardExternalReferenceType.Room) {
const room = await this.roomService.getSingleRoom(context.id);

if (this.isVideoConferenceEnabledForConfig() && (await this.isVideoConferenceEnabledForSchool(room.schoolId))) {
features.push(BoardFeature.VIDEOCONFERENCE);
}

return features;
}

/* istanbul ignore next */
throw new BadRequestException(`Unsupported board reference type ${context.type as string}`);
}

private isVideoConferenceEnabledForCourse(courseFeatures?: CourseFeatures[]): boolean {
return (courseFeatures ?? []).includes(CourseFeatures.VIDEOCONFERENCE);
}

private isVideoConferenceEnabledForSchool(schoolId: EntityId): Promise<boolean> {
return this.legacySchoolService.hasFeature(schoolId, SchoolFeature.VIDEOCONFERENCE);
}

private isVideoConferenceEnabledForConfig(): boolean {
return this.configService.get('FEATURE_VIDEOCONFERENCE_ENABLED');
}
}
10 changes: 9 additions & 1 deletion apps/server/src/modules/board/board-api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ import {
import { BoardNodePermissionService } from './service';
import { BoardUc, CardUc, ColumnUc, ElementUc, SubmissionItemUc } from './uc';
import { RoomModule } from '../room';
import { BoardContextApiHelperModule } from '../board-context';

@Module({
imports: [BoardModule, LoggerModule, RoomMembershipModule, RoomModule, forwardRef(() => AuthorizationModule)],
imports: [
BoardModule,
LoggerModule,
RoomMembershipModule,
RoomModule,
forwardRef(() => AuthorizationModule),
BoardContextApiHelperModule,
],
controllers: [BoardController, ColumnController, CardController, ElementController, BoardSubmissionController],
providers: [BoardUc, BoardNodePermissionService, ColumnUc, CardUc, ElementUc, SubmissionItemUc, CourseRepo],
})
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/modules/board/board-ws-api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { MetricsService } from './metrics/metrics.service';
import { BoardNodePermissionService } from './service';
import { BoardUc, CardUc, ColumnUc, ElementUc } from './uc';
import { RoomModule } from '../room';
import { BoardContextApiHelperModule } from '../board-context';

@Module({
imports: [
Expand All @@ -19,6 +20,7 @@ import { RoomModule } from '../room';
UserModule,
RoomMembershipModule,
RoomModule,
BoardContextApiHelperModule,
],
providers: [
BoardCollaborationGateway,
Expand Down
Loading

0 comments on commit 8b2c27d

Please sign in to comment.