From 11002da8045684632aa1cc0c96e9d5c884a1bf65 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Fri, 2 Aug 2024 10:51:10 +0200 Subject: [PATCH 01/64] add sync existing course --- .../controller/api-test/course.api.spec.ts | 108 ++++++++++++++++++ .../learnroom/controller/course.controller.ts | 22 +++- .../controller/dto/course-sync.body.params.ts | 12 ++ .../modules/learnroom/controller/dto/index.ts | 2 + ...dy-synchronized.loggable-exception.spec.ts | 31 +++++ ...already-synchronized.loggable-exception.ts | 21 ++++ .../modules/learnroom/domain/error/index.ts | 1 + .../src/modules/learnroom/domain/index.ts | 1 + .../src/modules/learnroom/learnroom.module.ts | 5 + .../service/course-do.service.spec.ts | 55 ++++++++- .../learnroom/service/course-do.service.ts | 11 ++ .../learnroom/uc/course-sync.uc.spec.ts | 50 +++++++- .../modules/learnroom/uc/course-sync.uc.ts | 17 ++- 13 files changed, 331 insertions(+), 5 deletions(-) create mode 100644 apps/server/src/modules/learnroom/controller/dto/course-sync.body.params.ts create mode 100644 apps/server/src/modules/learnroom/domain/error/course-already-synchronized.loggable-exception.spec.ts create mode 100644 apps/server/src/modules/learnroom/domain/error/course-already-synchronized.loggable-exception.ts diff --git a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts index c41abc21d77..e8c14e0f1a7 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts @@ -243,4 +243,112 @@ describe('Course Controller (API)', () => { expect(data[teacher.user.id].length).toBeGreaterThan(0); }); }); + + describe('[POST] /courses/:courseId/start-sync', () => { + describe('when a course is not synchronized', () => { + const setup = async () => { + const teacher = createTeacher(); + const group = groupEntityFactory.buildWithId(); + const course = courseFactory.build({ + teachers: [teacher.user], + }); + + await em.persistAndFlush([teacher.account, teacher.user, course, group]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacher.account); + + return { + loggedInClient, + course, + group, + }; + }; + + it('should start the synchronization', async () => { + const { loggedInClient, course, group } = await setup(); + const params = { groupId: group.id }; + + const response = await loggedInClient.post(`${course.id}/start-sync`).send(params); + + const result: CourseEntity = await em.findOneOrFail(CourseEntity, course.id); + expect(response.statusCode).toEqual(HttpStatus.NO_CONTENT); + expect(result.syncedWithGroup?.id).toBe(group.id); + }); + }); + + describe('when a course is already synchronized', () => { + const setup = async () => { + const teacher = createTeacher(); + const group = groupEntityFactory.buildWithId(); + const otherGroup = groupEntityFactory.buildWithId(); + const course = courseFactory.build({ + teachers: [teacher.user], + syncedWithGroup: otherGroup, + }); + + await em.persistAndFlush([teacher.account, teacher.user, course, group]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacher.account); + + return { + loggedInClient, + course, + group, + otherGroup, + }; + }; + + it('should not start the synchronization', async () => { + const { loggedInClient, course, group, otherGroup } = await setup(); + const params = { groupId: group.id }; + + const response = await loggedInClient.post(`${course.id}/start-sync`).send(params); + + const result: CourseEntity = await em.findOneOrFail(CourseEntity, course.id); + expect(response.statusCode).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body).toEqual({ + code: HttpStatus.UNPROCESSABLE_ENTITY, + message: 'Unprocessable Entity', + title: 'Course Already Synchronized', + type: 'COURSE_ALREADY_SYNCHRONIZED', + }); + expect(result.syncedWithGroup?.id).toBe(otherGroup.id); + }); + }); + + describe('when the user is unauthorized', () => { + const setup = async () => { + const teacher = createTeacher(); + const group = groupEntityFactory.buildWithId(); + const course = courseFactory.build({ + teachers: [teacher.user], + }); + + await em.persistAndFlush([teacher.account, teacher.user, course, group]); + em.clear(); + + return { + course, + group, + }; + }; + + it('should return unauthorized', async () => { + const { course, group } = await setup(); + const params = { groupId: group.id }; + + const response = await testApiClient.post(`${course.id}/start-sync`).send(params); + + expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); + expect(response.body).toEqual({ + code: HttpStatus.UNAUTHORIZED, + message: 'Unauthorized', + title: 'Unauthorized', + type: 'UNAUTHORIZED', + }); + }); + }); + }); }); diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 0a801a0aed2..3bf616c9a6e 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -30,8 +30,14 @@ import { Response } from 'express'; import { CourseMapper } from '../mapper/course.mapper'; import { CourseExportUc, CourseImportUc, CourseSyncUc, CourseUc } from '../uc'; import { CommonCartridgeFileValidatorPipe } from '../utils'; -import { CourseImportBodyParams, CourseMetadataListResponse, CourseQueryParams, CourseUrlParams } from './dto'; -import { CourseExportBodyParams } from './dto/course-export.body.params'; +import { + CourseExportBodyParams, + CourseImportBodyParams, + CourseMetadataListResponse, + CourseQueryParams, + CourseSyncBodyParams, + CourseUrlParams, +} from './dto'; @ApiTags('Courses') @Authenticate('jwt') @@ -129,4 +135,16 @@ export class CourseController { [currentUser.userId]: permissions, }; } + + @Post(':courseId/start-sync/') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: 'Start the synchronization of a course with a group.' }) + @ApiNoContentResponse({ description: 'The course was successfully connected to a group.' }) + public async startSynchronization( + @CurrentUser() currentUser: ICurrentUser, + @Param() params: CourseUrlParams, + @Body() bodyParams: CourseSyncBodyParams + ): Promise { + await this.courseSyncUc.startSynchronization(currentUser.userId, params.courseId, bodyParams.groupId); + } } diff --git a/apps/server/src/modules/learnroom/controller/dto/course-sync.body.params.ts b/apps/server/src/modules/learnroom/controller/dto/course-sync.body.params.ts new file mode 100644 index 00000000000..63fdffa5bf3 --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/course-sync.body.params.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsMongoId } from 'class-validator'; + +export class CourseSyncBodyParams { + @IsMongoId() + @ApiProperty({ + description: 'The id of the group', + required: true, + nullable: false, + }) + groupId!: string; +} diff --git a/apps/server/src/modules/learnroom/controller/dto/index.ts b/apps/server/src/modules/learnroom/controller/dto/index.ts index 3be2cba46f4..3ec85040e73 100644 --- a/apps/server/src/modules/learnroom/controller/dto/index.ts +++ b/apps/server/src/modules/learnroom/controller/dto/index.ts @@ -12,3 +12,5 @@ export * from './patch-visibility.params'; export * from './room-element.url.params'; export * from './room.url.params'; export * from './single-column-board'; +export * from './course-sync.body.params'; +export * from './course-export.body.params'; diff --git a/apps/server/src/modules/learnroom/domain/error/course-already-synchronized.loggable-exception.spec.ts b/apps/server/src/modules/learnroom/domain/error/course-already-synchronized.loggable-exception.spec.ts new file mode 100644 index 00000000000..15c37f4ce4a --- /dev/null +++ b/apps/server/src/modules/learnroom/domain/error/course-already-synchronized.loggable-exception.spec.ts @@ -0,0 +1,31 @@ +import { courseFactory } from '../../testing'; +import { CourseAlreadySynchronizedLoggableException } from './course-already-synchronized.loggable-exception'; + +describe(CourseAlreadySynchronizedLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const course = courseFactory.build(); + + const exception = new CourseAlreadySynchronizedLoggableException(course.id); + + return { + exception, + course, + }; + }; + + it('should log the correct message', () => { + const { exception, course } = setup(); + + const result = exception.getLogMessage(); + + expect(result).toEqual({ + type: 'COURSE_ALREADY_SYNCHRONIZED', + stack: expect.any(String), + data: { + courseId: course.id, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/learnroom/domain/error/course-already-synchronized.loggable-exception.ts b/apps/server/src/modules/learnroom/domain/error/course-already-synchronized.loggable-exception.ts new file mode 100644 index 00000000000..cd747ad5008 --- /dev/null +++ b/apps/server/src/modules/learnroom/domain/error/course-already-synchronized.loggable-exception.ts @@ -0,0 +1,21 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { ErrorLogMessage, Loggable } from '@src/core/logger'; + +export class CourseAlreadySynchronizedLoggableException extends UnprocessableEntityException implements Loggable { + constructor(private readonly courseId: EntityId) { + super(); + } + + getLogMessage(): ErrorLogMessage { + const message: ErrorLogMessage = { + type: 'COURSE_ALREADY_SYNCHRONIZED', + stack: this.stack, + data: { + courseId: this.courseId, + }, + }; + + return message; + } +} diff --git a/apps/server/src/modules/learnroom/domain/error/index.ts b/apps/server/src/modules/learnroom/domain/error/index.ts index 0f64cd09bd0..e6c6d6cc70a 100644 --- a/apps/server/src/modules/learnroom/domain/error/index.ts +++ b/apps/server/src/modules/learnroom/domain/error/index.ts @@ -1 +1,2 @@ export { CourseNotSynchronizedLoggableException } from './course-not-synchronized.loggable-exception'; +export { CourseAlreadySynchronizedLoggableException } from './course-already-synchronized.loggable-exception'; diff --git a/apps/server/src/modules/learnroom/domain/index.ts b/apps/server/src/modules/learnroom/domain/index.ts index 7ac280479fe..65edf954687 100644 --- a/apps/server/src/modules/learnroom/domain/index.ts +++ b/apps/server/src/modules/learnroom/domain/index.ts @@ -1,4 +1,5 @@ export { Course, CourseProps } from './do'; export { CourseRepo, COURSE_REPO } from './interface'; export { CourseNotSynchronizedLoggableException } from './error'; +export { CourseAlreadySynchronizedLoggableException } from './error'; export { CourseSynchronizationStoppedLoggable } from './loggable'; diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index 4ed7eae9fef..072d5923412 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -34,6 +34,8 @@ import { RoomsService, } from './service'; import { CommonCartridgeFileValidatorPipe } from './utils'; +import { GroupService } from '../group'; +import { GroupRepo } from '../group/repo'; @Module({ imports: [ @@ -67,6 +69,8 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; }, CourseService, CourseDoService, + GroupRepo, + GroupService, DashboardElementRepo, DashboardModelMapper, DashboardService, @@ -80,6 +84,7 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; CourseCopyService, CourseService, CourseDoService, + GroupService, RoomsService, CommonCartridgeExportService, CommonCartridgeImportService, diff --git a/apps/server/src/modules/learnroom/service/course-do.service.spec.ts b/apps/server/src/modules/learnroom/service/course-do.service.spec.ts index b015194b5ee..c5e028b281f 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.spec.ts @@ -4,9 +4,16 @@ import { Group } from '@modules/group'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { groupFactory } from '@shared/testing'; -import { Course, COURSE_REPO, CourseNotSynchronizedLoggableException, CourseRepo } from '../domain'; +import { + Course, + COURSE_REPO, + CourseAlreadySynchronizedLoggableException, + CourseNotSynchronizedLoggableException, + CourseRepo, +} from '../domain'; import { courseFactory } from '../testing'; import { CourseDoService } from './course-do.service'; +import { ro } from '@faker-js/faker'; describe(CourseDoService.name, () => { let module: TestingModule; @@ -169,4 +176,50 @@ describe(CourseDoService.name, () => { }); }); }); + + describe('startSynchronization', () => { + describe('when a course is нот synchronized with a group', () => { + const setup = () => { + const course: Course = courseFactory.build(); + const group: Group = groupFactory.build(); + + return { + course, + group, + }; + }; + + it('should save a course with a synchronized group', async () => { + const { course, group } = setup(); + + await service.startSynchronization(course, group); + + expect(courseRepo.save).toHaveBeenCalledWith( + new Course({ + ...course.getProps(), + syncedWithGroup: group.id, + }) + ); + }); + }); + + describe('when a course is synchronized with a group', () => { + const setup = () => { + const course: Course = courseFactory.build({ syncedWithGroup: new ObjectId().toHexString() }); + const group: Group = groupFactory.build(); + + return { + course, + group, + }; + }; + it('should throw an unprocessable entity exception', async () => { + const { course, group } = setup(); + + await expect(service.startSynchronization(course, group)).rejects.toThrow( + CourseAlreadySynchronizedLoggableException + ); + }); + }); + }); }); diff --git a/apps/server/src/modules/learnroom/service/course-do.service.ts b/apps/server/src/modules/learnroom/service/course-do.service.ts index 5bcba757d1f..e8417b0b002 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.ts @@ -3,6 +3,7 @@ import { type Group } from '@modules/group'; import { Inject, Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { type Course, COURSE_REPO, CourseNotSynchronizedLoggableException, CourseRepo } from '../domain'; +import { CourseAlreadySynchronizedLoggableException } from '../domain/error/course-already-synchronized.loggable-exception'; @Injectable() export class CourseDoService implements AuthorizationLoaderServiceGeneric { @@ -35,4 +36,14 @@ export class CourseDoService implements AuthorizationLoaderServiceGeneric { + if (course.syncedWithGroup) { + throw new CourseAlreadySynchronizedLoggableException(course.id); + } + + course.syncedWithGroup = group.id; + + await this.courseRepo.save(course); + } } diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts index f9171895b9d..b4ddccc186d 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts @@ -2,10 +2,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; -import { setupEntities, userFactory } from '@shared/testing'; +import { groupFactory, setupEntities, userFactory } from '@shared/testing'; import { CourseDoService } from '../service'; import { courseFactory } from '../testing'; import { CourseSyncUc } from './course-sync.uc'; +import { GroupService } from '../../group'; describe(CourseSyncUc.name, () => { let module: TestingModule; @@ -13,6 +14,7 @@ describe(CourseSyncUc.name, () => { let authorizationService: DeepMocked; let courseService: DeepMocked; + let groupService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -26,12 +28,17 @@ describe(CourseSyncUc.name, () => { provide: CourseDoService, useValue: createMock(), }, + { + provide: GroupService, + useValue: createMock(), + }, ], }).compile(); uc = module.get(CourseSyncUc); authorizationService = module.get(AuthorizationService); courseService = module.get(CourseDoService); + groupService = module.get(GroupService); await setupEntities(); }); @@ -79,4 +86,45 @@ describe(CourseSyncUc.name, () => { }); }); }); + + describe('startSynchronization', () => { + describe('when a user starts a synchronization of a course with a group', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const course = courseFactory.build(); + const group = groupFactory.build(); + + courseService.findById.mockResolvedValueOnce(course); + groupService.findById.mockResolvedValueOnce(group); + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + + return { + user, + course, + group, + }; + }; + + it('should check the users permission', async () => { + const { user, course, group } = setup(); + + await uc.startSynchronization(user.id, course.id, group.id); + + expect(authorizationService.checkPermission).toHaveBeenCalledWith( + user, + course, + AuthorizationContextBuilder.write([Permission.COURSE_EDIT]) + ); + }); + + it('should start the synchronization', async () => { + const { user, course, group } = setup(); + + await uc.startSynchronization(user.id, course.id, group.id); + + expect(courseService.startSynchronization).toHaveBeenCalledWith(course, group); + }); + }); + }); }); diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts index 5ffe55abe3e..a45e7a14c33 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { type User as UserEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; +import { Group, GroupService } from '../../group'; import { Course } from '../domain'; import { CourseDoService } from '../service'; @@ -10,7 +11,8 @@ import { CourseDoService } from '../service'; export class CourseSyncUc { constructor( private readonly authorizationService: AuthorizationService, - private readonly courseService: CourseDoService + private readonly courseService: CourseDoService, + private readonly groupService: GroupService ) {} public async stopSynchronization(userId: EntityId, courseId: EntityId): Promise { @@ -25,4 +27,17 @@ export class CourseSyncUc { await this.courseService.stopSynchronization(course); } + + public async startSynchronization(userId: string, courseId: string, groupId: string) { + const course: Course = await this.courseService.findById(courseId); + const group: Group = await this.groupService.findById(groupId); + const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); + this.authorizationService.checkPermission( + user, + course, + AuthorizationContextBuilder.write([Permission.COURSE_EDIT]) + ); + + await this.courseService.startSynchronization(course, group); + } } From b51c57488c8986a54c5ea99bbe5bcb406cf8fee1 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Fri, 2 Aug 2024 12:47:14 +0200 Subject: [PATCH 02/64] update imports; cleanup --- apps/server/src/modules/learnroom/learnroom.module.ts | 4 ++-- .../src/modules/learnroom/service/course-do.service.spec.ts | 1 - apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts | 2 +- apps/server/src/modules/learnroom/uc/course-sync.uc.ts | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index 072d5923412..caa321c2624 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -1,5 +1,7 @@ import { BoardModule } from '@modules/board'; import { CopyHelperModule } from '@modules/copy-helper'; +import { GroupService } from '@modules/group'; +import { GroupRepo } from '@modules/group/repo'; import { LessonModule } from '@modules/lesson'; import { TaskModule } from '@modules/task'; import { ContextExternalToolModule } from '@modules/tool/context-external-tool'; @@ -34,8 +36,6 @@ import { RoomsService, } from './service'; import { CommonCartridgeFileValidatorPipe } from './utils'; -import { GroupService } from '../group'; -import { GroupRepo } from '../group/repo'; @Module({ imports: [ diff --git a/apps/server/src/modules/learnroom/service/course-do.service.spec.ts b/apps/server/src/modules/learnroom/service/course-do.service.spec.ts index c5e028b281f..98c6b2eb47e 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.spec.ts @@ -13,7 +13,6 @@ import { } from '../domain'; import { courseFactory } from '../testing'; import { CourseDoService } from './course-do.service'; -import { ro } from '@faker-js/faker'; describe(CourseDoService.name, () => { let module: TestingModule; diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts index b4ddccc186d..13630167092 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts @@ -1,12 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { GroupService } from '@modules/group'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; import { groupFactory, setupEntities, userFactory } from '@shared/testing'; import { CourseDoService } from '../service'; import { courseFactory } from '../testing'; import { CourseSyncUc } from './course-sync.uc'; -import { GroupService } from '../../group'; describe(CourseSyncUc.name, () => { let module: TestingModule; diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts index a45e7a14c33..745daf063fd 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { type User as UserEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { Group, GroupService } from '../../group'; +import { Group, GroupService } from '@modules/group'; import { Course } from '../domain'; import { CourseDoService } from '../service'; From 1858e47ae82e5dbba7ec43d728be393466ad7f1a Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Mon, 5 Aug 2024 13:52:24 +0200 Subject: [PATCH 03/64] N21-2075 modify course metadata --- .../modules/learnroom/controller/course.controller.ts | 3 ++- .../learnroom/controller/dto/course-metadata.response.ts | 9 ++++++++- .../server/src/modules/learnroom/mapper/course.mapper.ts | 6 ++++-- apps/server/src/shared/domain/entity/course.entity.ts | 1 + apps/server/src/shared/domain/types/learnroom.types.ts | 3 +++ 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 3bf616c9a6e..9bec46f41d0 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -26,6 +26,7 @@ import { ApiUnprocessableEntityResponse, } from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller/'; +import { Course } from '@shared/domain/entity'; import { Response } from 'express'; import { CourseMapper } from '../mapper/course.mapper'; import { CourseExportUc, CourseImportUc, CourseSyncUc, CourseUc } from '../uc'; @@ -56,7 +57,7 @@ export class CourseController { @Query() pagination: PaginationParams ): Promise { const [courses, total] = await this.courseUc.findAllByUser(currentUser.userId, pagination); - const courseResponses = courses.map((course) => CourseMapper.mapToMetadataResponse(course)); + const courseResponses = courses.map((course: Course) => CourseMapper.mapToMetadataResponse(course)); const { skip, limit } = pagination; const result = new CourseMetadataListResponse(courseResponses, total, skip, limit); diff --git a/apps/server/src/modules/learnroom/controller/dto/course-metadata.response.ts b/apps/server/src/modules/learnroom/controller/dto/course-metadata.response.ts index b3615bcaf95..6dddadd4ae4 100644 --- a/apps/server/src/modules/learnroom/controller/dto/course-metadata.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/course-metadata.response.ts @@ -10,7 +10,8 @@ export class CourseMetadataResponse { displayColor: string, startDate?: Date, untilDate?: Date, - copyingSince?: Date + copyingSince?: Date, + syncedWithGroup?: string ) { this.id = id; this.title = title; @@ -19,6 +20,7 @@ export class CourseMetadataResponse { this.startDate = startDate; this.untilDate = untilDate; this.copyingSince = copyingSince; + this.syncedWithGroup = syncedWithGroup; } @ApiProperty({ @@ -56,6 +58,11 @@ export class CourseMetadataResponse { description: 'Start of the copying process if it is still ongoing - otherwise property is not set.', }) copyingSince?: Date; + + @ApiPropertyOptional({ + description: 'Synchronized with group', + }) + syncedWithGroup?: string; } export class CourseMetadataListResponse extends PaginationResponse { diff --git a/apps/server/src/modules/learnroom/mapper/course.mapper.ts b/apps/server/src/modules/learnroom/mapper/course.mapper.ts index 498922cc633..f8bdc054ae6 100644 --- a/apps/server/src/modules/learnroom/mapper/course.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/course.mapper.ts @@ -1,9 +1,10 @@ import { Course } from '@shared/domain/entity'; +import { LearnroomMetadata } from '@shared/domain/types'; import { CourseMetadataResponse } from '../controller/dto'; export class CourseMapper { static mapToMetadataResponse(course: Course): CourseMetadataResponse { - const courseMetadata = course.getMetadata(); + const courseMetadata: LearnroomMetadata = course.getMetadata(); const dto = new CourseMetadataResponse( courseMetadata.id, courseMetadata.title, @@ -11,7 +12,8 @@ export class CourseMapper { courseMetadata.displayColor, courseMetadata.startDate, courseMetadata.untilDate, - courseMetadata.copyingSince + courseMetadata.copyingSince, + courseMetadata.syncedWithGroup?.id ); return dto; } diff --git a/apps/server/src/shared/domain/entity/course.entity.ts b/apps/server/src/shared/domain/entity/course.entity.ts index 84a9005e48e..f272a846e0f 100644 --- a/apps/server/src/shared/domain/entity/course.entity.ts +++ b/apps/server/src/shared/domain/entity/course.entity.ts @@ -227,6 +227,7 @@ export class Course extends BaseEntityWithTimestamps implements Learnroom, Entit startDate: this.startDate, copyingSince: this.copyingSince, isSynchronized: !!this.syncedWithGroup, + syncedWithGroup: this.syncedWithGroup, }; } diff --git a/apps/server/src/shared/domain/types/learnroom.types.ts b/apps/server/src/shared/domain/types/learnroom.types.ts index cc81a291728..1ebeeb3b98e 100644 --- a/apps/server/src/shared/domain/types/learnroom.types.ts +++ b/apps/server/src/shared/domain/types/learnroom.types.ts @@ -1,3 +1,5 @@ +import { GroupEntity } from '@modules/group/entity'; + export enum LearnroomTypes { 'Course' = 'course', } @@ -12,4 +14,5 @@ export type LearnroomMetadata = { untilDate?: Date; copyingSince?: Date; isSynchronized: boolean; + syncedWithGroup?: GroupEntity; }; From d6c84a11768a198d5ce3bf9df47d015df9754f5c Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Mon, 5 Aug 2024 17:51:44 +0200 Subject: [PATCH 04/64] update tests course entity mapping --- .../controller/api-test/course.api.spec.ts | 7 +++++-- .../src/shared/domain/entity/course.entity.spec.ts | 13 +++++++++++-- .../src/shared/domain/entity/course.entity.ts | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts index e8c14e0f1a7..0a04390a4b8 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts @@ -52,12 +52,14 @@ describe('Course Controller (API)', () => { const setup = () => { const student = createStudent(); const teacher = createTeacher(); + const group = groupEntityFactory.buildWithId(); const course = courseFactory.buildWithId({ teachers: [teacher.user], students: [student.user], + syncedWithGroup: group, }); - return { student, course, teacher }; + return { student, course, teacher, group }; }; it('should find courses as student', async () => { @@ -76,7 +78,7 @@ describe('Course Controller (API)', () => { }); it('should find courses as teacher', async () => { - const { teacher, course } = setup(); + const { teacher, course, group } = setup(); await em.persistAndFlush([teacher.account, teacher.user, course]); em.clear(); @@ -86,6 +88,7 @@ describe('Course Controller (API)', () => { const { data } = response.body as CourseMetadataListResponse; expect(response.statusCode).toBe(200); expect(typeof data[0].title).toBe('string'); + expect(data[0].syncedWithGroup).toBe(group.id); expect(data[0].startDate).toBe(course.startDate); expect(data[0].untilDate).toBe(course.untilDate); }); diff --git a/apps/server/src/shared/domain/entity/course.entity.spec.ts b/apps/server/src/shared/domain/entity/course.entity.spec.ts index 0b7f7f432eb..5aa9805c75a 100644 --- a/apps/server/src/shared/domain/entity/course.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/course.entity.spec.ts @@ -1,6 +1,13 @@ import { MikroORM } from '@mikro-orm/core'; import { InternalServerErrorException } from '@nestjs/common'; -import { courseFactory, courseGroupFactory, schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; +import { + courseFactory, + courseGroupFactory, + groupEntityFactory, + schoolEntityFactory, + setupEntities, + userFactory, +} from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; import { Course } from './course.entity'; import { CourseGroup } from './coursegroup.entity'; @@ -43,7 +50,8 @@ describe('CourseEntity', () => { describe('getMetadata', () => { it('should return a metadata object', () => { - const course = courseFactory.build({ name: 'History', color: '#445566' }); + const group = groupEntityFactory.build(); + const course = courseFactory.build({ name: 'History', color: '#445566', syncedWithGroup: group }); const result = course.getMetadata(); @@ -51,6 +59,7 @@ describe('CourseEntity', () => { expect(result.shortTitle).toEqual('Hi'); expect(result.displayColor).toEqual('#445566'); expect(result.id).toEqual(course.id); + expect(result.syncedWithGroup?.id).toEqual(group.id); }); it('should return only emoji as shortTitle if used as first character', () => { diff --git a/apps/server/src/shared/domain/entity/course.entity.ts b/apps/server/src/shared/domain/entity/course.entity.ts index f272a846e0f..f6bd1227e72 100644 --- a/apps/server/src/shared/domain/entity/course.entity.ts +++ b/apps/server/src/shared/domain/entity/course.entity.ts @@ -120,7 +120,7 @@ export class Course extends BaseEntityWithTimestamps implements Learnroom, Entit if (props.features) this.features = props.features; this.classes.set(props.classes || []); this.groups.set(props.groups || []); - this.syncedWithGroup = props.syncedWithGroup; + if (props.syncedWithGroup) this.syncedWithGroup = props.syncedWithGroup; } public getStudentIds(): EntityId[] { From 5baad4295e1130008ee6259b5fa184cc0a4be956 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Tue, 6 Aug 2024 10:26:37 +0200 Subject: [PATCH 05/64] N21-2075 feature flag --- apps/server/src/modules/server/api/dto/config.response.ts | 4 ++++ apps/server/src/modules/server/api/test/server.api.spec.ts | 1 + apps/server/src/modules/server/server.config.ts | 2 ++ config/default.schema.json | 5 +++++ 4 files changed, 12 insertions(+) diff --git a/apps/server/src/modules/server/api/dto/config.response.ts b/apps/server/src/modules/server/api/dto/config.response.ts index 87fa2771d38..42cbcc7b44b 100644 --- a/apps/server/src/modules/server/api/dto/config.response.ts +++ b/apps/server/src/modules/server/api/dto/config.response.ts @@ -32,6 +32,9 @@ export class ConfigResponse { @ApiProperty() FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED: boolean; + @ApiProperty() + FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED: boolean; + @ApiProperty() FEATURE_CTL_TOOLS_COPY_ENABLED: boolean; @@ -269,6 +272,7 @@ export class ConfigResponse { this.FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION = config.FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION; this.CTL_TOOLS_RELOAD_TIME_MS = config.CTL_TOOLS_RELOAD_TIME_MS; this.FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED = config.FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED; + this.FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED = config.FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED; this.FEATURE_CTL_TOOLS_COPY_ENABLED = config.FEATURE_CTL_TOOLS_COPY_ENABLED; this.FEATURE_SHOW_MIGRATION_WIZARD = config.FEATURE_SHOW_MIGRATION_WIZARD; this.MIGRATION_WIZARD_DOCUMENTATION_LINK = config.MIGRATION_WIZARD_DOCUMENTATION_LINK; diff --git a/apps/server/src/modules/server/api/test/server.api.spec.ts b/apps/server/src/modules/server/api/test/server.api.spec.ts index a0a844e3f99..910fdfc05d2 100644 --- a/apps/server/src/modules/server/api/test/server.api.spec.ts +++ b/apps/server/src/modules/server/api/test/server.api.spec.ts @@ -70,6 +70,7 @@ describe('Server Controller (API)', () => { 'FEATURE_SCHOOL_TERMS_OF_USE_ENABLED', 'FEATURE_SHOW_MIGRATION_WIZARD', 'FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED', + 'FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED', 'FEATURE_SHOW_OUTDATED_USERS', 'FEATURE_TASK_SHARE', 'FEATURE_TEAMS_ENABLED', diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index d4d813e3836..6eb24f69758 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -103,6 +103,7 @@ export interface ServerConfig FEATURE_NEW_SCHOOL_ADMINISTRATION_PAGE_AS_DEFAULT_ENABLED: boolean; FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION: boolean; FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED: boolean; + FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED: boolean; FEATURE_SHOW_MIGRATION_WIZARD: boolean; MIGRATION_WIZARD_DOCUMENTATION_LINK?: string; FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED: boolean; @@ -216,6 +217,7 @@ const config: ServerConfig = { FEATURE_SHOW_OUTDATED_USERS: Configuration.get('FEATURE_SHOW_OUTDATED_USERS') as boolean, FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION: Configuration.get('FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION') as boolean, FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED: Configuration.get('FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED') as boolean, + FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED: Configuration.get('FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED') as boolean, FEATURE_SHOW_MIGRATION_WIZARD: Configuration.get('FEATURE_SHOW_MIGRATION_WIZARD') as boolean, FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_ENABLED: Configuration.get( 'FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_ENABLED' diff --git a/config/default.schema.json b/config/default.schema.json index c3cd6b52b43..97e99e605cf 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1388,6 +1388,11 @@ "default": false, "description": "Enables the new class list view" }, + "FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED": { + "type": "boolean", + "default": false, + "description": "Enables the new course list view" + }, "FEATURE_GROUPS_IN_COURSE_ENABLED": { "type": "boolean", "default": false, From 5c9f559c196123d902fa2cc2544942f48aca5ee1 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Fri, 9 Aug 2024 11:41:27 +0200 Subject: [PATCH 06/64] N21-2075 CLEAN UP --- .../modules/learnroom/controller/course.controller.ts | 3 +-- .../learnroom/controller/dto/course-metadata.response.ts | 9 +-------- .../server/src/modules/learnroom/mapper/course.mapper.ts | 6 ++---- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 9bec46f41d0..3bf616c9a6e 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -26,7 +26,6 @@ import { ApiUnprocessableEntityResponse, } from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller/'; -import { Course } from '@shared/domain/entity'; import { Response } from 'express'; import { CourseMapper } from '../mapper/course.mapper'; import { CourseExportUc, CourseImportUc, CourseSyncUc, CourseUc } from '../uc'; @@ -57,7 +56,7 @@ export class CourseController { @Query() pagination: PaginationParams ): Promise { const [courses, total] = await this.courseUc.findAllByUser(currentUser.userId, pagination); - const courseResponses = courses.map((course: Course) => CourseMapper.mapToMetadataResponse(course)); + const courseResponses = courses.map((course) => CourseMapper.mapToMetadataResponse(course)); const { skip, limit } = pagination; const result = new CourseMetadataListResponse(courseResponses, total, skip, limit); diff --git a/apps/server/src/modules/learnroom/controller/dto/course-metadata.response.ts b/apps/server/src/modules/learnroom/controller/dto/course-metadata.response.ts index 6dddadd4ae4..b3615bcaf95 100644 --- a/apps/server/src/modules/learnroom/controller/dto/course-metadata.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/course-metadata.response.ts @@ -10,8 +10,7 @@ export class CourseMetadataResponse { displayColor: string, startDate?: Date, untilDate?: Date, - copyingSince?: Date, - syncedWithGroup?: string + copyingSince?: Date ) { this.id = id; this.title = title; @@ -20,7 +19,6 @@ export class CourseMetadataResponse { this.startDate = startDate; this.untilDate = untilDate; this.copyingSince = copyingSince; - this.syncedWithGroup = syncedWithGroup; } @ApiProperty({ @@ -58,11 +56,6 @@ export class CourseMetadataResponse { description: 'Start of the copying process if it is still ongoing - otherwise property is not set.', }) copyingSince?: Date; - - @ApiPropertyOptional({ - description: 'Synchronized with group', - }) - syncedWithGroup?: string; } export class CourseMetadataListResponse extends PaginationResponse { diff --git a/apps/server/src/modules/learnroom/mapper/course.mapper.ts b/apps/server/src/modules/learnroom/mapper/course.mapper.ts index f8bdc054ae6..498922cc633 100644 --- a/apps/server/src/modules/learnroom/mapper/course.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/course.mapper.ts @@ -1,10 +1,9 @@ import { Course } from '@shared/domain/entity'; -import { LearnroomMetadata } from '@shared/domain/types'; import { CourseMetadataResponse } from '../controller/dto'; export class CourseMapper { static mapToMetadataResponse(course: Course): CourseMetadataResponse { - const courseMetadata: LearnroomMetadata = course.getMetadata(); + const courseMetadata = course.getMetadata(); const dto = new CourseMetadataResponse( courseMetadata.id, courseMetadata.title, @@ -12,8 +11,7 @@ export class CourseMapper { courseMetadata.displayColor, courseMetadata.startDate, courseMetadata.untilDate, - courseMetadata.copyingSince, - courseMetadata.syncedWithGroup?.id + courseMetadata.copyingSince ); return dto; } From be0443083af5beaad2751f681220306987455be4 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Fri, 9 Aug 2024 11:45:58 +0200 Subject: [PATCH 07/64] N21-2075 CLEAN UP --- .../learnroom/controller/api-test/course.api.spec.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts index 0a04390a4b8..e8c14e0f1a7 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts @@ -52,14 +52,12 @@ describe('Course Controller (API)', () => { const setup = () => { const student = createStudent(); const teacher = createTeacher(); - const group = groupEntityFactory.buildWithId(); const course = courseFactory.buildWithId({ teachers: [teacher.user], students: [student.user], - syncedWithGroup: group, }); - return { student, course, teacher, group }; + return { student, course, teacher }; }; it('should find courses as student', async () => { @@ -78,7 +76,7 @@ describe('Course Controller (API)', () => { }); it('should find courses as teacher', async () => { - const { teacher, course, group } = setup(); + const { teacher, course } = setup(); await em.persistAndFlush([teacher.account, teacher.user, course]); em.clear(); @@ -88,7 +86,6 @@ describe('Course Controller (API)', () => { const { data } = response.body as CourseMetadataListResponse; expect(response.statusCode).toBe(200); expect(typeof data[0].title).toBe('string'); - expect(data[0].syncedWithGroup).toBe(group.id); expect(data[0].startDate).toBe(course.startDate); expect(data[0].untilDate).toBe(course.untilDate); }); From 97b1ad32030da9b088ff0284c935785dc12dc095 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Fri, 9 Aug 2024 11:46:04 +0200 Subject: [PATCH 08/64] N21-2075 CLEAN UP --- .../src/shared/domain/entity/course.entity.spec.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/apps/server/src/shared/domain/entity/course.entity.spec.ts b/apps/server/src/shared/domain/entity/course.entity.spec.ts index 5aa9805c75a..0b7f7f432eb 100644 --- a/apps/server/src/shared/domain/entity/course.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/course.entity.spec.ts @@ -1,13 +1,6 @@ import { MikroORM } from '@mikro-orm/core'; import { InternalServerErrorException } from '@nestjs/common'; -import { - courseFactory, - courseGroupFactory, - groupEntityFactory, - schoolEntityFactory, - setupEntities, - userFactory, -} from '@shared/testing'; +import { courseFactory, courseGroupFactory, schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; import { Course } from './course.entity'; import { CourseGroup } from './coursegroup.entity'; @@ -50,8 +43,7 @@ describe('CourseEntity', () => { describe('getMetadata', () => { it('should return a metadata object', () => { - const group = groupEntityFactory.build(); - const course = courseFactory.build({ name: 'History', color: '#445566', syncedWithGroup: group }); + const course = courseFactory.build({ name: 'History', color: '#445566' }); const result = course.getMetadata(); @@ -59,7 +51,6 @@ describe('CourseEntity', () => { expect(result.shortTitle).toEqual('Hi'); expect(result.displayColor).toEqual('#445566'); expect(result.id).toEqual(course.id); - expect(result.syncedWithGroup?.id).toEqual(group.id); }); it('should return only emoji as shortTitle if used as first character', () => { From b122a30ec285ae0ac24e26121c655447f3365078 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Fri, 9 Aug 2024 16:12:56 +0200 Subject: [PATCH 09/64] N21-2075 WIP endpoint --- .../learnroom/controller/course.controller.ts | 44 ++++++++++++ .../modules/learnroom/controller/dto/index.ts | 1 + .../interface/course-request-context.enum.ts | 3 + .../interface/course-sort-query-type.enum.ts | 6 ++ .../interface/school-year-query-type.enum.ts | 4 ++ .../dto/request/course-caller-params.ts | 10 +++ .../dto/request/course-filter-params.ts | 10 +++ .../dto/request/course-sourt-params.ts | 11 +++ .../dto/response/class-info.reponse.ts | 15 +++++ .../dto/response/course-info-list.response.ts | 13 ++++ .../dto/response/course-info.response.ts | 36 ++++++++++ .../controller/dto/response/index.ts | 3 + .../learnroom/controller/dto/school.params.ts | 12 ++++ .../mapper/course-info-response.mapper.ts | 47 +++++++++++++ .../learnroom/mapper/course-info.mapper.ts | 16 +++++ .../learnroom/repo/mikro-orm/course.repo.ts | 8 +++ .../learnroom/service/course-do.service.ts | 6 ++ .../src/modules/learnroom/uc/course.uc.ts | 67 +++++++++++++++++-- .../learnroom/uc/dto/class-info.dto.ts | 12 ++++ .../learnroom/uc/dto/course-info.dto.ts | 28 ++++++++ .../src/modules/learnroom/uc/dto/index.ts | 2 + .../src/shared/repo/course/course.repo.ts | 6 ++ 22 files changed, 355 insertions(+), 5 deletions(-) create mode 100644 apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts create mode 100644 apps/server/src/modules/learnroom/controller/dto/interface/course-sort-query-type.enum.ts create mode 100644 apps/server/src/modules/learnroom/controller/dto/interface/school-year-query-type.enum.ts create mode 100644 apps/server/src/modules/learnroom/controller/dto/request/course-caller-params.ts create mode 100644 apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts create mode 100644 apps/server/src/modules/learnroom/controller/dto/request/course-sourt-params.ts create mode 100644 apps/server/src/modules/learnroom/controller/dto/response/class-info.reponse.ts create mode 100644 apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts create mode 100644 apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts create mode 100644 apps/server/src/modules/learnroom/controller/dto/response/index.ts create mode 100644 apps/server/src/modules/learnroom/controller/dto/school.params.ts create mode 100644 apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts create mode 100644 apps/server/src/modules/learnroom/mapper/course-info.mapper.ts create mode 100644 apps/server/src/modules/learnroom/uc/dto/class-info.dto.ts create mode 100644 apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts create mode 100644 apps/server/src/modules/learnroom/uc/dto/index.ts diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 3bf616c9a6e..b6a5eadfd07 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -22,13 +22,20 @@ import { ApiInternalServerErrorResponse, ApiNoContentResponse, ApiOperation, + ApiResponse, ApiTags, ApiUnprocessableEntityResponse, } from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller/'; +import { Page } from '@shared/domain/domainobject'; +import { Course } from '@shared/domain/entity'; import { Response } from 'express'; +import { ErrorResponse } from '../../../core/error/dto'; +import { ClassInfoSearchListResponse } from '../../group/controller/dto'; +import { CourseInfoResponseMapper } from '../mapper/course-info-response.mapper'; import { CourseMapper } from '../mapper/course.mapper'; import { CourseExportUc, CourseImportUc, CourseSyncUc, CourseUc } from '../uc'; +import { CourseInfoDto } from '../uc/dto/course-info.dto'; import { CommonCartridgeFileValidatorPipe } from '../utils'; import { CourseExportBodyParams, @@ -37,7 +44,12 @@ import { CourseQueryParams, CourseSyncBodyParams, CourseUrlParams, + SchoolParams, } from './dto'; +import { CourseCallerParams } from './dto/request/course-caller-params'; +import { CourseFilterParams } from './dto/request/course-filter-params'; +import { CourseSortParams } from './dto/request/course-sourt-params'; +import { CourseInfoListResponse } from './dto/response/course-info-list.response'; @ApiTags('Courses') @Authenticate('jwt') @@ -147,4 +159,36 @@ export class CourseController { ): Promise { await this.courseSyncUc.startSynchronization(currentUser.userId, params.courseId, bodyParams.groupId); } + + @Get('/all') + @ApiOperation({ summary: 'Get a list of all courses.' }) + @ApiResponse({ status: HttpStatus.OK, type: ClassInfoSearchListResponse }) + @ApiResponse({ status: '4XX', type: ErrorResponse }) + @ApiResponse({ status: '5XX', type: ErrorResponse }) + async getAllCourses( + @CurrentUser() currentUser: ICurrentUser, + @Query() schoolParams: SchoolParams, + @Query() pagination: PaginationParams, + @Query() sortingQuery: CourseSortParams, + @Query() filterParams: CourseFilterParams, + @Query() callerParams: CourseCallerParams + ): Promise { + const board: Page = await this.courseUc.findAllCourses( + currentUser.userId, + currentUser.schoolId, + filterParams.type, + callerParams.calledFrom, + pagination, + sortingQuery.sortBy, + sortingQuery.sortOrder + ); + + const response: CourseInfoListResponse = CourseInfoResponseMapper.mapToCourseInfoListResponse( + board, + pagination.skip, + pagination.limit + ); + + return response; + } } diff --git a/apps/server/src/modules/learnroom/controller/dto/index.ts b/apps/server/src/modules/learnroom/controller/dto/index.ts index 3ec85040e73..3d5b7013656 100644 --- a/apps/server/src/modules/learnroom/controller/dto/index.ts +++ b/apps/server/src/modules/learnroom/controller/dto/index.ts @@ -14,3 +14,4 @@ export * from './room.url.params'; export * from './single-column-board'; export * from './course-sync.body.params'; export * from './course-export.body.params'; +export * from './school.params'; diff --git a/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts b/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts new file mode 100644 index 00000000000..4176b8e1bad --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts @@ -0,0 +1,3 @@ +export enum CourseRequestContext { + COURSE_ADMIN_OVERVIEW = 'course-admin-overview', +} diff --git a/apps/server/src/modules/learnroom/controller/dto/interface/course-sort-query-type.enum.ts b/apps/server/src/modules/learnroom/controller/dto/interface/course-sort-query-type.enum.ts new file mode 100644 index 00000000000..f1bfcd99528 --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/interface/course-sort-query-type.enum.ts @@ -0,0 +1,6 @@ +export enum CourseSortQueryType { + NAME = 'name', + SYNCHRONIZED_GROUP = 'synchronizedGroup', + TEACHER_NAMES = 'teacherNames', + CLASS_NAME = 'className', +} diff --git a/apps/server/src/modules/learnroom/controller/dto/interface/school-year-query-type.enum.ts b/apps/server/src/modules/learnroom/controller/dto/interface/school-year-query-type.enum.ts new file mode 100644 index 00000000000..a0fade12250 --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/interface/school-year-query-type.enum.ts @@ -0,0 +1,4 @@ +export enum SchoolYearQueryType { + CURRENT_YEAR = 'currentYear', + PREVIOUS_YEARS = 'previousYears', +} diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-caller-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-caller-params.ts new file mode 100644 index 00000000000..6ce30904bfe --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/request/course-caller-params.ts @@ -0,0 +1,10 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; +import { CourseRequestContext } from '../interface/course-request-context.enum'; + +export class CourseCallerParams { + @IsOptional() + @IsEnum(CourseRequestContext) + @ApiPropertyOptional({ enum: CourseRequestContext, enumName: 'CourseRequestContext' }) + calledFrom?: CourseRequestContext; +} diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts new file mode 100644 index 00000000000..dc076ddfd89 --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts @@ -0,0 +1,10 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; +import { SchoolYearQueryType } from '../interface/school-year-query-type.enum'; + +export class CourseFilterParams { + @IsOptional() + @IsEnum(SchoolYearQueryType) + @ApiPropertyOptional({ enum: SchoolYearQueryType, enumName: 'SchoolYearQueryType' }) + type?: SchoolYearQueryType; +} diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-sourt-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-sourt-params.ts new file mode 100644 index 00000000000..52eee6289c4 --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/request/course-sourt-params.ts @@ -0,0 +1,11 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { SortingParams } from '@shared/controller'; +import { IsEnum, IsOptional } from 'class-validator'; +import { CourseSortQueryType } from '../interface/course-sort-query-type.enum'; + +export class CourseSortParams extends SortingParams { + @IsOptional() + @IsEnum(CourseSortQueryType) + @ApiPropertyOptional({ enum: CourseSortQueryType, enumName: 'CourseSortQueryType' }) + sortBy?: CourseSortQueryType; +} diff --git a/apps/server/src/modules/learnroom/controller/dto/response/class-info.reponse.ts b/apps/server/src/modules/learnroom/controller/dto/response/class-info.reponse.ts new file mode 100644 index 00000000000..db7a2d76676 --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/response/class-info.reponse.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { EntityId } from '@shared/domain/types'; + +export class ClassInfoResponse { + @ApiProperty() + id: EntityId; + + @ApiProperty() + name: string; + + constructor(props: ClassInfoResponse) { + this.id = props.id; + this.name = props.name; + } +} diff --git a/apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts b/apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts new file mode 100644 index 00000000000..0cdb8b3cc0c --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PaginationResponse } from '@shared/controller'; +import { CourseInfoResponse } from './course-info.response'; + +export class CourseInfoListResponse extends PaginationResponse { + constructor(data: CourseInfoResponse[], total: number, skip?: number, limit?: number) { + super(total, skip, limit); + this.data = data; + } + + @ApiProperty({ type: [CourseInfoResponse] }) + data: CourseInfoResponse[]; +} diff --git a/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts b/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts new file mode 100644 index 00000000000..8b1e8ee0ef1 --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts @@ -0,0 +1,36 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { EntityId } from '@shared/domain/types'; +import { ClassInfoResponse } from './class-info.reponse'; + +export class CourseInfoResponse { + @ApiProperty() + id: EntityId; + + @ApiProperty() + name: string; + + @ApiProperty({ type: [String] }) + teacherNames: string[]; + + @ApiProperty() + classes: string[]; + + @ApiPropertyOptional() + schoolYear?: string; + + @ApiProperty() + studentCount: number; + + @ApiPropertyOptional() + syncedGroup?: ClassInfoResponse; + + constructor(props: CourseInfoResponse) { + this.id = props.id; + this.name = props.name; + this.classes = props.classes; + this.teacherNames = props.teacherNames; + this.schoolYear = props.schoolYear; + this.studentCount = props.studentCount; + this.syncedGroup = props.syncedGroup; + } +} diff --git a/apps/server/src/modules/learnroom/controller/dto/response/index.ts b/apps/server/src/modules/learnroom/controller/dto/response/index.ts new file mode 100644 index 00000000000..2bf424446be --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/response/index.ts @@ -0,0 +1,3 @@ +export { CourseInfoListResponse } from './course-info-list.response'; +export { CourseInfoResponse } from './course-info.response'; +export { ClassInfoResponse } from './class-info.reponse'; diff --git a/apps/server/src/modules/learnroom/controller/dto/school.params.ts b/apps/server/src/modules/learnroom/controller/dto/school.params.ts new file mode 100644 index 00000000000..dfd863ee1b5 --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/school.params.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsMongoId } from 'class-validator'; + +export class SchoolParams { + @IsMongoId() + @ApiProperty({ + description: 'The id of the school', + required: true, + nullable: false, + }) + schoolId!: string; +} diff --git a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts new file mode 100644 index 00000000000..f1c81585a0d --- /dev/null +++ b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts @@ -0,0 +1,47 @@ +import { Page } from '@shared/domain/domainobject'; +import { ClassInfoResponse, CourseInfoListResponse, CourseInfoResponse } from '../controller/dto/response'; +import { ClassInfoDto, CourseInfoDto } from '../uc/dto'; + +export class CourseInfoResponseMapper { + public static mapToCourseInfoListResponse( + courseInfos: Page, + skip?: number, + limit?: number + ): CourseInfoListResponse { + const courseInfoResponses: CourseInfoResponse[] = courseInfos.data.map((courseInfo) => + this.mapToCourseInfoResponse(courseInfo) + ); + + const response: CourseInfoListResponse = new CourseInfoListResponse( + courseInfoResponses, + courseInfos.total, + skip, + limit + ); + + return response; + } + + private static mapToCourseInfoResponse(courseInfo: CourseInfoDto): CourseInfoResponse { + const courseInfoResponse: CourseInfoResponse = new CourseInfoResponse({ + id: courseInfo.id, + name: courseInfo.name, + classes: courseInfo.classes, + teacherNames: courseInfo.teacherNames, + schoolYear: courseInfo.schoolYear, + studentCount: courseInfo.studentCount, + syncedGroup: courseInfo.syncedGroup ? this.mapToClassInfoResponse(courseInfo.syncedGroup) : undefined, + }); + + return courseInfoResponse; + } + + private static mapToClassInfoResponse(classInfoDto: ClassInfoDto): ClassInfoResponse { + const classInfoResponse: ClassInfoResponse = new ClassInfoResponse({ + id: classInfoDto.id, + name: classInfoDto.name, + }); + + return classInfoResponse; + } +} diff --git a/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts new file mode 100644 index 00000000000..b653048124a --- /dev/null +++ b/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts @@ -0,0 +1,16 @@ +import { Course } from '../domain'; +import { CourseInfoDto } from '../uc/dto'; + +export class CourseInfoMapper { +public static mapCourseToCourseInfoDto( + course: Course, +): CourseInfoDto { + const mapped: CourseInfoDto = new CourseInfoDto({ + id: course.id, + name: course.name, + teacherNames: course.teachers, + classes: course.getProps().classIds, + + } +}) +} diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts index f4efdf13bfb..e88adeaed8f 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts @@ -44,4 +44,12 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo { + const entities: CourseEntity[] = await this.em.find(CourseEntity, { school: id }); + + const courses: Course[] = entities.map((entity: CourseEntity): Course => CourseEntityMapper.mapEntityToDo(entity)); + + return courses; + } } diff --git a/apps/server/src/modules/learnroom/service/course-do.service.ts b/apps/server/src/modules/learnroom/service/course-do.service.ts index e8417b0b002..490102320a6 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.ts @@ -46,4 +46,10 @@ export class CourseDoService implements AuthorizationLoaderServiceGeneric { + const courses: Course = await this.courseRepo.findCourseById(schoolId); + + return courses; + } } diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index ed7abf98a1d..6134155d156 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -1,13 +1,20 @@ -import { AuthorizationService } from '@modules/authorization'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { RoleService } from '@modules/role'; import { Injectable } from '@nestjs/common'; +import { SortHelper } from '@shared/common'; import { PaginationParams } from '@shared/controller/'; -import { Course } from '@shared/domain/entity'; -import { SortOrder } from '@shared/domain/interface'; +import { Page } from '@shared/domain/domainobject'; +import { Course, User } from '@shared/domain/entity'; +import { Pagination, Permission, SortOrder } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; +import { ClassInfoDto } from '../../group/uc/dto'; +import { School, SchoolService } from '../../school'; +import { CourseRequestContext } from '../controller/dto/interface/course-request-context.enum'; +import { SchoolYearQueryType } from '../controller/dto/interface/school-year-query-type.enum'; import { RoleNameMapper } from '../mapper/rolename.mapper'; -import { CourseService } from '../service'; +import { CourseDoService, CourseService } from '../service'; +import { CourseInfoDto } from './dto'; @Injectable() export class CourseUc { @@ -15,7 +22,9 @@ export class CourseUc { private readonly courseRepo: CourseRepo, private readonly courseService: CourseService, private readonly authService: AuthorizationService, - private readonly roleService: RoleService + private readonly roleService: RoleService, + private readonly schoolService: SchoolService, + private readonly courseDoService: CourseDoService ) {} public findAllByUser(userId: EntityId, options?: PaginationParams): Promise> { @@ -30,4 +39,52 @@ export class CourseUc { return role.permissions ?? []; } + + public async findAllCourses( + userId: EntityId, + schoolId: EntityId, + schoolYearQueryType?: SchoolYearQueryType, + calledFrom?: CourseRequestContext, + pagination?: Pagination, + sortBy: keyof CourseInfoDto = 'name', + sortOrder: SortOrder = SortOrder.asc + ): Promise> { + const school: School = await this.schoolService.getSchoolById(schoolId); + + const user: User = await this.authService.getUserWithPermissions(userId); + this.authService.checkPermission(user, school, AuthorizationContextBuilder.read([Permission.ADMIN_VIEW])); + + const courses = await this.courseDoService.findCoursesBySchool(schoolId); + + const courseInfosFromCourses: CourseInfoDto[] = this.getCourseInfosFromCourses(courses, schoolYearQueryType); + + courses.sort((a: CourseInfoDto, b: CourseInfoDto): number => + SortHelper.genericSortFunction(a[sortBy], b[sortBy], sortOrder) + ); + + const pageContent: ClassInfoDto[] = this.applyPagination(combinedClassInfo, pagination?.skip, pagination?.limit); + + const page: Page = new Page(pageContent, combinedClassInfo.length); + + return page; + } + + private getCourseInfosFromCourses(courses: Course[], schoolYearQueryType: SchoolYearQueryType | undefined) { + const now = new Date(); + let untilDate; + const allCourses = courses; + if (!schoolYearQueryType || schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR) { + allCourses.filter((course: Course) => { + untilDate = course.untilDate ?? now.getDate() + 1; + return now < untilDate; + }); + } else { + allCourses.filter((course) => { + untilDate = course.untilDate ?? now.getDate() + 1; + return now > untilDate; + }); + } + + return allCourses; + } } diff --git a/apps/server/src/modules/learnroom/uc/dto/class-info.dto.ts b/apps/server/src/modules/learnroom/uc/dto/class-info.dto.ts new file mode 100644 index 00000000000..d2666f121f4 --- /dev/null +++ b/apps/server/src/modules/learnroom/uc/dto/class-info.dto.ts @@ -0,0 +1,12 @@ +import { EntityId } from '@shared/domain/types'; + +export class ClassInfoDto { + id: EntityId; + + name: string; + + constructor(props: ClassInfoDto) { + this.id = props.id; + this.name = props.name; + } +} diff --git a/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts b/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts new file mode 100644 index 00000000000..aa8bd0f8b76 --- /dev/null +++ b/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts @@ -0,0 +1,28 @@ +import { EntityId } from '@shared/domain/types'; +import { ClassInfoDto } from './class-info.dto'; + +export class CourseInfoDto { + id: EntityId; + + name: string; + + teacherNames: string[]; + + classes: string[]; + + schoolYear?: string; + + studentCount: number; + + syncedGroup?: ClassInfoDto; + + constructor(props: CourseInfoDto) { + this.id = props.id; + this.name = props.name; + this.classes = props.classes; + this.teacherNames = props.teacherNames; + this.schoolYear = props.schoolYear; + this.studentCount = props.studentCount; + this.syncedGroup = props.syncedGroup; + } +} diff --git a/apps/server/src/modules/learnroom/uc/dto/index.ts b/apps/server/src/modules/learnroom/uc/dto/index.ts new file mode 100644 index 00000000000..70457f56689 --- /dev/null +++ b/apps/server/src/modules/learnroom/uc/dto/index.ts @@ -0,0 +1,2 @@ +export { CourseInfoDto } from './course-info.dto'; +export { ClassInfoDto } from './class-info.dto'; diff --git a/apps/server/src/shared/repo/course/course.repo.ts b/apps/server/src/shared/repo/course/course.repo.ts index fb52b8c115e..43142fea4ae 100644 --- a/apps/server/src/shared/repo/course/course.repo.ts +++ b/apps/server/src/shared/repo/course/course.repo.ts @@ -1,4 +1,5 @@ import { FilterQuery, QueryOrderMap } from '@mikro-orm/core'; +import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { Course } from '@shared/domain/entity'; @@ -50,6 +51,11 @@ class CourseScope extends Scope { this.addQuery({ id: courseId }); return this; } + + forSchoolId(schoolId: EntityId): CourseScope { + this.addQuery({ school: schoolId }); + return this; + } } @Injectable() From 591130fc41ba951d7c08c8fa23033a06514551e0 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Mon, 12 Aug 2024 09:24:19 +0200 Subject: [PATCH 10/64] N21-2075 WIP mapper --- .../learnroom/controller/course.controller.ts | 4 +- .../domain/interface/course.repo.interface.ts | 2 + .../learnroom/mapper/course-info.mapper.ts | 38 +++++++++++++------ .../learnroom/repo/mikro-orm/course.repo.ts | 2 +- .../learnroom/service/course-do.service.ts | 4 +- .../learnroom/service/course.service.ts | 7 ++++ .../src/modules/learnroom/uc/course.uc.ts | 17 ++++++--- .../learnroom/uc/dto/course-info.dto.ts | 4 +- .../src/shared/repo/course/course.repo.ts | 14 ++++--- 9 files changed, 62 insertions(+), 30 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index b6a5eadfd07..a1954aa5b5a 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -173,7 +173,7 @@ export class CourseController { @Query() filterParams: CourseFilterParams, @Query() callerParams: CourseCallerParams ): Promise { - const board: Page = await this.courseUc.findAllCourses( + const courses: Page = await this.courseUc.findAllCourses( currentUser.userId, currentUser.schoolId, filterParams.type, @@ -184,7 +184,7 @@ export class CourseController { ); const response: CourseInfoListResponse = CourseInfoResponseMapper.mapToCourseInfoListResponse( - board, + courses, pagination.skip, pagination.limit ); diff --git a/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts b/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts index 2e40c8bc2a2..721490c6c17 100644 --- a/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts +++ b/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts @@ -7,6 +7,8 @@ export interface CourseRepo extends BaseDomainObjectRepoInterface { findCourseById(id: EntityId): Promise; findBySyncedGroup(group: Group): Promise; + + findCoursesBySchoolId(schoolId: EntityId): Promise; } export const COURSE_REPO = Symbol('COURSE_REPO'); diff --git a/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts index b653048124a..874cd0bba97 100644 --- a/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts @@ -1,16 +1,32 @@ -import { Course } from '../domain'; -import { CourseInfoDto } from '../uc/dto'; +import { Course } from '@shared/domain/entity'; +import { ClassEntity } from '../../class/entity'; +import { ClassInfoDto, CourseInfoDto } from '../uc/dto'; export class CourseInfoMapper { -public static mapCourseToCourseInfoDto( - course: Course, -): CourseInfoDto { - const mapped: CourseInfoDto = new CourseInfoDto({ - id: course.id, - name: course.name, - teacherNames: course.teachers, - classes: course.getProps().classIds, + public static mapCourseToCourseInfoDto(course: Course): CourseInfoDto { + const mapped: CourseInfoDto = new CourseInfoDto({ + id: course.id, + name: course.name, + classes: this.mapToClassesInfos(course.classes), + teachers: course.teachers, + syncedWithGroup = { name: course.syncedWithGroup?.name, id: course.syncedWithGroup?.id }, + }); + return mapped; } -}) + + private static mapToClassesInfos(clazzes) { + const mapped = clazzes.map((clazz) => this.mapToClassInfo(clazz)); + + return mapped; + } + + private static mapToClassInfo(clazz: ClassEntity) { + const mapped = new ClassInfoDto({ + id: clazz.id, + name: clazz.name, + }); + } + + private static mapToTeacherNames() {} } diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts index e88adeaed8f..1ae9bd9bfdc 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts @@ -45,7 +45,7 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo { + public async findCoursesBySchoolId(id: EntityId): Promise { const entities: CourseEntity[] = await this.em.find(CourseEntity, { school: id }); const courses: Course[] = entities.map((entity: CourseEntity): Course => CourseEntityMapper.mapEntityToDo(entity)); diff --git a/apps/server/src/modules/learnroom/service/course-do.service.ts b/apps/server/src/modules/learnroom/service/course-do.service.ts index 490102320a6..5a6660ff2a4 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.ts @@ -47,8 +47,8 @@ export class CourseDoService implements AuthorizationLoaderServiceGeneric { - const courses: Course = await this.courseRepo.findCourseById(schoolId); + public async findCoursesBySchool(schoolId: EntityId): Promise { + const courses: Course[] = await this.courseRepo.findCoursesBySchoolId(schoolId); return courses; } diff --git a/apps/server/src/modules/learnroom/service/course.service.ts b/apps/server/src/modules/learnroom/service/course.service.ts index cf7cc52db2d..405401c6ac4 100644 --- a/apps/server/src/modules/learnroom/service/course.service.ts +++ b/apps/server/src/modules/learnroom/service/course.service.ts @@ -17,6 +17,7 @@ import { Course as CourseEntity } from '@shared/domain/entity'; import { Counted, EntityId } from '@shared/domain/types'; import { CourseRepo as LegacyCourseRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; +import type { Course } from '../domain'; @Injectable() @EventsHandler(UserDeletedEvent) @@ -97,4 +98,10 @@ export class CourseService implements DeletionService, IEventHandler { + const courses = await this.repo.findCoursesBySchoolId(schoolId); + + return courses; + } } diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index 6134155d156..9dd082f150f 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -11,7 +11,9 @@ import { CourseRepo } from '@shared/repo'; import { ClassInfoDto } from '../../group/uc/dto'; import { School, SchoolService } from '../../school'; import { CourseRequestContext } from '../controller/dto/interface/course-request-context.enum'; +import { CourseSortQueryType } from '../controller/dto/interface/course-sort-query-type.enum'; import { SchoolYearQueryType } from '../controller/dto/interface/school-year-query-type.enum'; +import { CourseInfoMapper } from '../mapper/course-info.mapper'; import { RoleNameMapper } from '../mapper/rolename.mapper'; import { CourseDoService, CourseService } from '../service'; import { CourseInfoDto } from './dto'; @@ -43,22 +45,25 @@ export class CourseUc { public async findAllCourses( userId: EntityId, schoolId: EntityId, + sortBy: CourseSortQueryType = CourseSortQueryType.NAME, schoolYearQueryType?: SchoolYearQueryType, calledFrom?: CourseRequestContext, pagination?: Pagination, - sortBy: keyof CourseInfoDto = 'name', - sortOrder: SortOrder = SortOrder.asc + sortOrder?: SortOrder ): Promise> { const school: School = await this.schoolService.getSchoolById(schoolId); const user: User = await this.authService.getUserWithPermissions(userId); this.authService.checkPermission(user, school, AuthorizationContextBuilder.read([Permission.ADMIN_VIEW])); - const courses = await this.courseDoService.findCoursesBySchool(schoolId); + // const courses: Course[] = await this.courseDoService.findCoursesBySchool(schoolId); + const courses: Course[] = await this.courseService.findCoursesBySchool(schoolId); - const courseInfosFromCourses: CourseInfoDto[] = this.getCourseInfosFromCourses(courses, schoolYearQueryType); + const courseInfosFromCourses: Course[] = this.getCourseInfosFromCourses(courses, schoolYearQueryType); - courses.sort((a: CourseInfoDto, b: CourseInfoDto): number => + const coursesInfo = courseInfosFromCourses.map((course) => CourseInfoMapper.mapCourseToCourseInfoDto(course)); + + coursesInfo.sort((a: CourseInfoDto, b: CourseInfoDto): number => SortHelper.genericSortFunction(a[sortBy], b[sortBy], sortOrder) ); @@ -69,7 +74,7 @@ export class CourseUc { return page; } - private getCourseInfosFromCourses(courses: Course[], schoolYearQueryType: SchoolYearQueryType | undefined) { + private getCourseInfosFromCourses(courses: Course[], schoolYearQueryType: SchoolYearQueryType | undefined): Course[] { const now = new Date(); let untilDate; const allCourses = courses; diff --git a/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts b/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts index aa8bd0f8b76..e7d1199448d 100644 --- a/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts +++ b/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts @@ -14,7 +14,7 @@ export class CourseInfoDto { studentCount: number; - syncedGroup?: ClassInfoDto; + syncedWithGroup?: ClassInfoDto; constructor(props: CourseInfoDto) { this.id = props.id; @@ -23,6 +23,6 @@ export class CourseInfoDto { this.teacherNames = props.teacherNames; this.schoolYear = props.schoolYear; this.studentCount = props.studentCount; - this.syncedGroup = props.syncedGroup; + this.syncedWithGroup = props.syncedWithGroup; } } diff --git a/apps/server/src/shared/repo/course/course.repo.ts b/apps/server/src/shared/repo/course/course.repo.ts index 43142fea4ae..de6df00ea61 100644 --- a/apps/server/src/shared/repo/course/course.repo.ts +++ b/apps/server/src/shared/repo/course/course.repo.ts @@ -1,8 +1,9 @@ import { FilterQuery, QueryOrderMap } from '@mikro-orm/core'; import { ObjectId } from '@mikro-orm/mongodb'; +import { CourseEntityMapper } from '@modules/learnroom/repo/mikro-orm/mapper/course.entity.mapper'; import { Injectable } from '@nestjs/common'; -import { Course } from '@shared/domain/entity'; +import { Course as CourseEntity, Course } from '@shared/domain/entity'; import { IFindOptions } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { BaseRepo } from '../base.repo'; @@ -51,11 +52,6 @@ class CourseScope extends Scope { this.addQuery({ id: courseId }); return this; } - - forSchoolId(schoolId: EntityId): CourseScope { - this.addQuery({ school: schoolId }); - return this; - } } @Injectable() @@ -143,4 +139,10 @@ export class CourseRepo extends BaseRepo { return course; } + + public async findCoursesBySchoolId(id: EntityId): Promise { + const courses: Course[] = await this._em.find(Course, { school: id }); + + return courses; + } } From 4d11d7975c5dec34f7b48eb2e88f8b9ef19836f5 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Mon, 12 Aug 2024 11:03:29 +0200 Subject: [PATCH 11/64] N21-2075 WIP --- .../learnroom/mapper/course-info.mapper.ts | 46 ++++++++++++++++--- .../src/modules/learnroom/uc/course.uc.ts | 7 +-- .../learnroom/uc/dto/course-info.dto.ts | 7 +-- .../learnroom/uc/dto/group-info.dto.ts | 12 +++++ 4 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 apps/server/src/modules/learnroom/uc/dto/group-info.dto.ts diff --git a/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts index 874cd0bba97..71862d0fc77 100644 --- a/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts @@ -1,32 +1,64 @@ -import { Course } from '@shared/domain/entity'; +import { Course, Role, User } from '@shared/domain/entity'; import { ClassEntity } from '../../class/entity'; +import { GroupEntity } from '../../group/entity'; +import { UserDto } from '../../user/uc/dto/user.dto'; import { ClassInfoDto, CourseInfoDto } from '../uc/dto'; +import { GroupInfoDto } from '../uc/dto/group-info.dto'; export class CourseInfoMapper { public static mapCourseToCourseInfoDto(course: Course): CourseInfoDto { const mapped: CourseInfoDto = new CourseInfoDto({ id: course.id, name: course.name, - classes: this.mapToClassesInfos(course.classes), - teachers: course.teachers, - syncedWithGroup = { name: course.syncedWithGroup?.name, id: course.syncedWithGroup?.id }, + classes: this.mapToClassesInfos(course.classes.getItems()), + teachers: this.mapToUserDtos(course.teachers.getItems()), + syncedWithGroup: course.syncedWithGroup ? this.mapToSyncedWithGroupInfo(course.syncedWithGroup) : undefined, }); return mapped; } - private static mapToClassesInfos(clazzes) { + private static mapToClassesInfos(clazzes: ClassEntity[]): ClassInfoDto[] { const mapped = clazzes.map((clazz) => this.mapToClassInfo(clazz)); return mapped; } - private static mapToClassInfo(clazz: ClassEntity) { + private static mapToClassInfo(clazz: ClassEntity): ClassInfoDto { const mapped = new ClassInfoDto({ id: clazz.id, name: clazz.name, }); + + return mapped; + } + + private static mapToUserDtos(users: User[]) { + const mapped = users.map((user) => this.mapToUserDto(user)); + + return mapped; } - private static mapToTeacherNames() {} + private static mapToUserDto(user: User): UserDto { + return new UserDto({ + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + roleIds: user.roles.getItems().map((role: Role) => role.id), + schoolId: user.school.id, + ldapDn: user.ldapDn, + externalId: user.externalId, + language: user.language, + forcePasswordChange: user.forcePasswordChange, + preferences: user.preferences, + lastLoginSystemChange: user.lastLoginSystemChange, + outdatedSince: user.outdatedSince, + }); + } + + private static mapToSyncedWithGroupInfo(group: GroupEntity): GroupInfoDto { + const mapped = new GroupInfoDto({ id: group.id, name: group.name }); + return mapped; + } } diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index 9dd082f150f..ff955ad9882 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -4,7 +4,7 @@ import { Injectable } from '@nestjs/common'; import { SortHelper } from '@shared/common'; import { PaginationParams } from '@shared/controller/'; import { Page } from '@shared/domain/domainobject'; -import { Course, User } from '@shared/domain/entity'; +import { Course as CourseEntity, User } from '@shared/domain/entity'; import { Pagination, Permission, SortOrder } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; @@ -13,6 +13,7 @@ import { School, SchoolService } from '../../school'; import { CourseRequestContext } from '../controller/dto/interface/course-request-context.enum'; import { CourseSortQueryType } from '../controller/dto/interface/course-sort-query-type.enum'; import { SchoolYearQueryType } from '../controller/dto/interface/school-year-query-type.enum'; +import { Course } from '../domain'; import { CourseInfoMapper } from '../mapper/course-info.mapper'; import { RoleNameMapper } from '../mapper/rolename.mapper'; import { CourseDoService, CourseService } from '../service'; @@ -29,7 +30,7 @@ export class CourseUc { private readonly courseDoService: CourseDoService ) {} - public findAllByUser(userId: EntityId, options?: PaginationParams): Promise> { + public findAllByUser(userId: EntityId, options?: PaginationParams): Promise> { return this.courseRepo.findAllByUserId(userId, {}, { pagination: options, order: { updatedAt: SortOrder.desc } }); } @@ -57,7 +58,7 @@ export class CourseUc { this.authService.checkPermission(user, school, AuthorizationContextBuilder.read([Permission.ADMIN_VIEW])); // const courses: Course[] = await this.courseDoService.findCoursesBySchool(schoolId); - const courses: Course[] = await this.courseService.findCoursesBySchool(schoolId); + const courses: Course[] = await this.courseDoService.findCoursesBySchool(schoolId); const courseInfosFromCourses: Course[] = this.getCourseInfosFromCourses(courses, schoolYearQueryType); diff --git a/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts b/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts index e7d1199448d..5283ba2451e 100644 --- a/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts +++ b/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts @@ -1,4 +1,5 @@ import { EntityId } from '@shared/domain/types'; +import { UserDto } from '../../../user/uc/dto/user.dto'; import { ClassInfoDto } from './class-info.dto'; export class CourseInfoDto { @@ -6,9 +7,9 @@ export class CourseInfoDto { name: string; - teacherNames: string[]; + teachers: UserDto[]; - classes: string[]; + classes: ClassInfoDto[]; schoolYear?: string; @@ -20,7 +21,7 @@ export class CourseInfoDto { this.id = props.id; this.name = props.name; this.classes = props.classes; - this.teacherNames = props.teacherNames; + this.teachers = props.teachers; this.schoolYear = props.schoolYear; this.studentCount = props.studentCount; this.syncedWithGroup = props.syncedWithGroup; diff --git a/apps/server/src/modules/learnroom/uc/dto/group-info.dto.ts b/apps/server/src/modules/learnroom/uc/dto/group-info.dto.ts new file mode 100644 index 00000000000..95823f20a86 --- /dev/null +++ b/apps/server/src/modules/learnroom/uc/dto/group-info.dto.ts @@ -0,0 +1,12 @@ +import { EntityId } from '@shared/domain/types'; + +export class GroupInfoDto { + id: EntityId; + + name: string; + + constructor(props: GroupInfoDto) { + this.id = props.id; + this.name = props.name; + } +} From 37b05f40b888bedee567f12df6cea5a305913e9c Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Tue, 13 Aug 2024 12:39:15 +0200 Subject: [PATCH 12/64] update courses /all endpoint --- .../src/modules/class/repo/classes.repo.ts | 12 ++ .../modules/class/repo/mapper/class.mapper.ts | 2 +- .../modules/class/service/class.service.ts | 9 ++ .../learnroom/controller/course.controller.ts | 12 +- .../course-status-query-type.enum.ts | 4 + .../dto/request/course-filter-params.ts | 8 +- ...-sourt-params.ts => course-sort-params.ts} | 0 .../dto/response/course-info.response.ts | 11 +- .../modules/learnroom/learnroom-api.module.ts | 10 ++ .../src/modules/learnroom/learnroom.module.ts | 26 ++++ .../mapper/course-info-response.mapper.ts | 20 +--- .../learnroom/mapper/course-info.mapper.ts | 64 ---------- .../learnroom/service/course.service.ts | 1 - .../src/modules/learnroom/uc/course.uc.ts | 111 ++++++++++++++---- .../learnroom/uc/dto/course-info.dto.ts | 15 +-- 15 files changed, 174 insertions(+), 131 deletions(-) create mode 100644 apps/server/src/modules/learnroom/controller/dto/interface/course-status-query-type.enum.ts rename apps/server/src/modules/learnroom/controller/dto/request/{course-sourt-params.ts => course-sort-params.ts} (100%) delete mode 100644 apps/server/src/modules/learnroom/mapper/course-info.mapper.ts diff --git a/apps/server/src/modules/class/repo/classes.repo.ts b/apps/server/src/modules/class/repo/classes.repo.ts index 7b3c4784d16..5f744cd1793 100644 --- a/apps/server/src/modules/class/repo/classes.repo.ts +++ b/apps/server/src/modules/class/repo/classes.repo.ts @@ -55,4 +55,16 @@ export class ClassesRepo { await this.em.persistAndFlush(existingEntities); } + + public async findClassById(id: EntityId): Promise { + const clazz = await this.em.findOne(ClassEntity, { id }); + + if (!clazz) { + return null; + } + + const domainObject: Class = ClassMapper.mapToDO(clazz); + + return domainObject; + } } diff --git a/apps/server/src/modules/class/repo/mapper/class.mapper.ts b/apps/server/src/modules/class/repo/mapper/class.mapper.ts index 8ae5e3b79b9..bd904a39bd6 100644 --- a/apps/server/src/modules/class/repo/mapper/class.mapper.ts +++ b/apps/server/src/modules/class/repo/mapper/class.mapper.ts @@ -4,7 +4,7 @@ import { ClassSourceOptions } from '../../domain/class-source-options.do'; import { ClassEntity } from '../../entity'; export class ClassMapper { - private static mapToDO(entity: ClassEntity): Class { + static mapToDO(entity: ClassEntity): Class { return new Class({ id: entity.id, name: entity.name, diff --git a/apps/server/src/modules/class/service/class.service.ts b/apps/server/src/modules/class/service/class.service.ts index e650e4bee38..c4b19d1c0ab 100644 --- a/apps/server/src/modules/class/service/class.service.ts +++ b/apps/server/src/modules/class/service/class.service.ts @@ -13,6 +13,7 @@ import { } from '@modules/deletion'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { EventBus, EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { EntityId } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; import { Class } from '../domain'; @@ -100,4 +101,12 @@ export class ClassService implements DeletionService, IEventHandler item.id); } + + public async findById(id: EntityId): Promise { + const clazz: Class | null = await this.classesRepo.findClassById(id); + if (!clazz) { + throw new NotFoundLoggableException(Class.name, { id }); + } + return clazz; + } } diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index a1954aa5b5a..c77185f98c7 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -28,7 +28,6 @@ import { } from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller/'; import { Page } from '@shared/domain/domainobject'; -import { Course } from '@shared/domain/entity'; import { Response } from 'express'; import { ErrorResponse } from '../../../core/error/dto'; import { ClassInfoSearchListResponse } from '../../group/controller/dto'; @@ -46,9 +45,8 @@ import { CourseUrlParams, SchoolParams, } from './dto'; -import { CourseCallerParams } from './dto/request/course-caller-params'; import { CourseFilterParams } from './dto/request/course-filter-params'; -import { CourseSortParams } from './dto/request/course-sourt-params'; +import { CourseSortParams } from './dto/request/course-sort-params'; import { CourseInfoListResponse } from './dto/response/course-info-list.response'; @ApiTags('Courses') @@ -162,7 +160,7 @@ export class CourseController { @Get('/all') @ApiOperation({ summary: 'Get a list of all courses.' }) - @ApiResponse({ status: HttpStatus.OK, type: ClassInfoSearchListResponse }) + @ApiResponse({ status: HttpStatus.OK, type: CourseInfoListResponse }) @ApiResponse({ status: '4XX', type: ErrorResponse }) @ApiResponse({ status: '5XX', type: ErrorResponse }) async getAllCourses( @@ -170,16 +168,14 @@ export class CourseController { @Query() schoolParams: SchoolParams, @Query() pagination: PaginationParams, @Query() sortingQuery: CourseSortParams, - @Query() filterParams: CourseFilterParams, - @Query() callerParams: CourseCallerParams + @Query() filterParams: CourseFilterParams ): Promise { const courses: Page = await this.courseUc.findAllCourses( currentUser.userId, currentUser.schoolId, + sortingQuery.sortBy, filterParams.type, - callerParams.calledFrom, pagination, - sortingQuery.sortBy, sortingQuery.sortOrder ); diff --git a/apps/server/src/modules/learnroom/controller/dto/interface/course-status-query-type.enum.ts b/apps/server/src/modules/learnroom/controller/dto/interface/course-status-query-type.enum.ts new file mode 100644 index 00000000000..8d419726c17 --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/interface/course-status-query-type.enum.ts @@ -0,0 +1,4 @@ +export enum CourseStatusQueryType { + ARCHIVE = 'archive', + CURRENT = 'current', +} diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts index dc076ddfd89..067905f19f1 100644 --- a/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts +++ b/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts @@ -1,10 +1,10 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; -import { SchoolYearQueryType } from '../interface/school-year-query-type.enum'; +import { CourseStatusQueryType } from '../interface/course-status-query-type.enum'; export class CourseFilterParams { @IsOptional() - @IsEnum(SchoolYearQueryType) - @ApiPropertyOptional({ enum: SchoolYearQueryType, enumName: 'SchoolYearQueryType' }) - type?: SchoolYearQueryType; + @IsEnum(CourseStatusQueryType) + @ApiPropertyOptional({ enum: CourseStatusQueryType, enumName: 'CourseStatusQueryType' }) + type?: CourseStatusQueryType; } diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-sourt-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts similarity index 100% rename from apps/server/src/modules/learnroom/controller/dto/request/course-sourt-params.ts rename to apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts diff --git a/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts b/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts index 8b1e8ee0ef1..7df5a302a9b 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts @@ -1,6 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { EntityId } from '@shared/domain/types'; -import { ClassInfoResponse } from './class-info.reponse'; export class CourseInfoResponse { @ApiProperty() @@ -16,21 +15,17 @@ export class CourseInfoResponse { classes: string[]; @ApiPropertyOptional() - schoolYear?: string; - - @ApiProperty() - studentCount: number; + courseStatus?: string; @ApiPropertyOptional() - syncedGroup?: ClassInfoResponse; + syncedGroup?: string; constructor(props: CourseInfoResponse) { this.id = props.id; this.name = props.name; this.classes = props.classes; this.teacherNames = props.teacherNames; - this.schoolYear = props.schoolYear; - this.studentCount = props.studentCount; + this.courseStatus = props.courseStatus; this.syncedGroup = props.syncedGroup; } } diff --git a/apps/server/src/modules/learnroom/learnroom-api.module.ts b/apps/server/src/modules/learnroom/learnroom-api.module.ts index f0ecaa1855b..01613754d42 100644 --- a/apps/server/src/modules/learnroom/learnroom-api.module.ts +++ b/apps/server/src/modules/learnroom/learnroom-api.module.ts @@ -1,8 +1,13 @@ import { AuthorizationModule } from '@modules/authorization'; import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module'; import { CopyHelperModule } from '@modules/copy-helper'; +import { GroupModule } from '@modules/group'; +import { ClassModule } from '@modules/class'; + import { LessonModule } from '@modules/lesson'; import { RoleModule } from '@modules/role'; +import { SchoolModule } from '@modules/school'; +import { UserModule } from '@modules/user'; import { Module } from '@nestjs/common'; import { CourseRepo, DashboardModelMapper, DashboardRepo, LegacyBoardRepo, UserRepo } from '@shared/repo'; import { CourseController } from './controller/course.controller'; @@ -10,6 +15,7 @@ import { DashboardController } from './controller/dashboard.controller'; import { RoomsController } from './controller/rooms.controller'; import { LearnroomModule } from './learnroom.module'; import { RoomBoardResponseMapper } from './mapper/room-board-response.mapper'; + import { CourseCopyUC, CourseExportUc, @@ -31,6 +37,10 @@ import { LearnroomModule, AuthorizationReferenceModule, RoleModule, + SchoolModule, + GroupModule, + UserModule, + ClassModule, ], controllers: [DashboardController, CourseController, RoomsController], providers: [ diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index caa321c2624..0206fb53c6f 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -1,10 +1,18 @@ +import { AccountModule } from '@modules/account'; import { BoardModule } from '@modules/board'; +import { ClassModule } from '@modules/class'; import { CopyHelperModule } from '@modules/copy-helper'; import { GroupService } from '@modules/group'; import { GroupRepo } from '@modules/group/repo'; import { LessonModule } from '@modules/lesson'; +import { RegistrationPinModule } from '@modules/registration-pin'; +import { RoleModule } from '@modules/role'; +import { SchoolService } from '@modules/school'; +import { SCHOOL_REPO } from '@modules/school/domain/interface'; +import { SystemModule } from '@modules/system'; import { TaskModule } from '@modules/task'; import { ContextExternalToolModule } from '@modules/tool/context-external-tool'; +import { UserModule, UserService } from '@modules/user'; import { forwardRef, Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { @@ -16,8 +24,11 @@ import { LegacyBoardRepo, UserRepo, } from '@shared/repo'; +import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { LoggerModule } from '@src/core/logger'; +import { CalendarModule } from '../../infra/calendar'; import { BoardNodeRepo } from '../board/repo'; +import { SchoolMikroOrmRepo } from '../school/repo/mikro-orm/school.repo'; import { COURSE_REPO } from './domain'; import { CommonCartridgeExportMapper } from './mapper/common-cartridge-export.mapper'; import { CommonCartridgeImportMapper } from './mapper/common-cartridge-import.mapper'; @@ -46,6 +57,13 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; LoggerModule, TaskModule, CqrsModule, + SystemModule, + UserModule, + RoleModule, + ClassModule, + AccountModule, + RegistrationPinModule, + CalendarModule, ], providers: [ { @@ -79,6 +97,14 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; UserRepo, GroupDeletedHandlerService, ColumnBoardNodeRepo, + SchoolService, + { + provide: SCHOOL_REPO, + useClass: SchoolMikroOrmRepo, + }, + UserService, + UserRepo, + UserDORepo, ], exports: [ CourseCopyService, diff --git a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts index f1c81585a0d..4148553a417 100644 --- a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts @@ -1,6 +1,6 @@ import { Page } from '@shared/domain/domainobject'; -import { ClassInfoResponse, CourseInfoListResponse, CourseInfoResponse } from '../controller/dto/response'; -import { ClassInfoDto, CourseInfoDto } from '../uc/dto'; +import { CourseInfoListResponse, CourseInfoResponse } from '../controller/dto/response'; +import { CourseInfoDto } from '../uc/dto'; export class CourseInfoResponseMapper { public static mapToCourseInfoListResponse( @@ -27,21 +27,11 @@ export class CourseInfoResponseMapper { id: courseInfo.id, name: courseInfo.name, classes: courseInfo.classes, - teacherNames: courseInfo.teacherNames, - schoolYear: courseInfo.schoolYear, - studentCount: courseInfo.studentCount, - syncedGroup: courseInfo.syncedGroup ? this.mapToClassInfoResponse(courseInfo.syncedGroup) : undefined, + teacherNames: courseInfo.teachers, + courseStatus: courseInfo.courseStatus, + syncedGroup: courseInfo.syncedWithGroup ? courseInfo.syncedWithGroup : undefined, }); return courseInfoResponse; } - - private static mapToClassInfoResponse(classInfoDto: ClassInfoDto): ClassInfoResponse { - const classInfoResponse: ClassInfoResponse = new ClassInfoResponse({ - id: classInfoDto.id, - name: classInfoDto.name, - }); - - return classInfoResponse; - } } diff --git a/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts deleted file mode 100644 index 71862d0fc77..00000000000 --- a/apps/server/src/modules/learnroom/mapper/course-info.mapper.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Course, Role, User } from '@shared/domain/entity'; -import { ClassEntity } from '../../class/entity'; -import { GroupEntity } from '../../group/entity'; -import { UserDto } from '../../user/uc/dto/user.dto'; -import { ClassInfoDto, CourseInfoDto } from '../uc/dto'; -import { GroupInfoDto } from '../uc/dto/group-info.dto'; - -export class CourseInfoMapper { - public static mapCourseToCourseInfoDto(course: Course): CourseInfoDto { - const mapped: CourseInfoDto = new CourseInfoDto({ - id: course.id, - name: course.name, - classes: this.mapToClassesInfos(course.classes.getItems()), - teachers: this.mapToUserDtos(course.teachers.getItems()), - syncedWithGroup: course.syncedWithGroup ? this.mapToSyncedWithGroupInfo(course.syncedWithGroup) : undefined, - }); - - return mapped; - } - - private static mapToClassesInfos(clazzes: ClassEntity[]): ClassInfoDto[] { - const mapped = clazzes.map((clazz) => this.mapToClassInfo(clazz)); - - return mapped; - } - - private static mapToClassInfo(clazz: ClassEntity): ClassInfoDto { - const mapped = new ClassInfoDto({ - id: clazz.id, - name: clazz.name, - }); - - return mapped; - } - - private static mapToUserDtos(users: User[]) { - const mapped = users.map((user) => this.mapToUserDto(user)); - - return mapped; - } - - private static mapToUserDto(user: User): UserDto { - return new UserDto({ - id: user.id, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - roleIds: user.roles.getItems().map((role: Role) => role.id), - schoolId: user.school.id, - ldapDn: user.ldapDn, - externalId: user.externalId, - language: user.language, - forcePasswordChange: user.forcePasswordChange, - preferences: user.preferences, - lastLoginSystemChange: user.lastLoginSystemChange, - outdatedSince: user.outdatedSince, - }); - } - - private static mapToSyncedWithGroupInfo(group: GroupEntity): GroupInfoDto { - const mapped = new GroupInfoDto({ id: group.id, name: group.name }); - return mapped; - } -} diff --git a/apps/server/src/modules/learnroom/service/course.service.ts b/apps/server/src/modules/learnroom/service/course.service.ts index 405401c6ac4..3248c73e37d 100644 --- a/apps/server/src/modules/learnroom/service/course.service.ts +++ b/apps/server/src/modules/learnroom/service/course.service.ts @@ -17,7 +17,6 @@ import { Course as CourseEntity } from '@shared/domain/entity'; import { Counted, EntityId } from '@shared/domain/types'; import { CourseRepo as LegacyCourseRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; -import type { Course } from '../domain'; @Injectable() @EventsHandler(UserDeletedEvent) diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index ff955ad9882..e4d85037b0d 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -1,20 +1,20 @@ import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { ClassService } from '@modules/class'; +import { Group, GroupService } from '@modules/group'; import { RoleService } from '@modules/role'; +import { School, SchoolService } from '@modules/school'; +import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; import { SortHelper } from '@shared/common'; import { PaginationParams } from '@shared/controller/'; -import { Page } from '@shared/domain/domainobject'; +import { Page, UserDO } from '@shared/domain/domainobject'; import { Course as CourseEntity, User } from '@shared/domain/entity'; import { Pagination, Permission, SortOrder } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; -import { ClassInfoDto } from '../../group/uc/dto'; -import { School, SchoolService } from '../../school'; -import { CourseRequestContext } from '../controller/dto/interface/course-request-context.enum'; import { CourseSortQueryType } from '../controller/dto/interface/course-sort-query-type.enum'; -import { SchoolYearQueryType } from '../controller/dto/interface/school-year-query-type.enum'; +import { CourseStatusQueryType } from '../controller/dto/interface/course-status-query-type.enum'; import { Course } from '../domain'; -import { CourseInfoMapper } from '../mapper/course-info.mapper'; import { RoleNameMapper } from '../mapper/rolename.mapper'; import { CourseDoService, CourseService } from '../service'; import { CourseInfoDto } from './dto'; @@ -27,7 +27,10 @@ export class CourseUc { private readonly authService: AuthorizationService, private readonly roleService: RoleService, private readonly schoolService: SchoolService, - private readonly courseDoService: CourseDoService + private readonly courseDoService: CourseDoService, + private readonly groupService: GroupService, + private readonly userService: UserService, + private readonly classService: ClassService ) {} public findAllByUser(userId: EntityId, options?: PaginationParams): Promise> { @@ -47,39 +50,45 @@ export class CourseUc { userId: EntityId, schoolId: EntityId, sortBy: CourseSortQueryType = CourseSortQueryType.NAME, - schoolYearQueryType?: SchoolYearQueryType, - calledFrom?: CourseRequestContext, + courseStatusQueryType?: CourseStatusQueryType, pagination?: Pagination, sortOrder?: SortOrder - ): Promise> { + ): Promise> { const school: School = await this.schoolService.getSchoolById(schoolId); const user: User = await this.authService.getUserWithPermissions(userId); this.authService.checkPermission(user, school, AuthorizationContextBuilder.read([Permission.ADMIN_VIEW])); - // const courses: Course[] = await this.courseDoService.findCoursesBySchool(schoolId); const courses: Course[] = await this.courseDoService.findCoursesBySchool(schoolId); - const courseInfosFromCourses: Course[] = this.getCourseInfosFromCourses(courses, schoolYearQueryType); - - const coursesInfo = courseInfosFromCourses.map((course) => CourseInfoMapper.mapCourseToCourseInfoDto(course)); + const courseInfosFromCourses: CourseInfoDto[] = await this.getCourseInfosFromCourses( + courses, + courseStatusQueryType + ); - coursesInfo.sort((a: CourseInfoDto, b: CourseInfoDto): number => + courseInfosFromCourses.sort((a: CourseInfoDto, b: CourseInfoDto): number => SortHelper.genericSortFunction(a[sortBy], b[sortBy], sortOrder) ); - const pageContent: ClassInfoDto[] = this.applyPagination(combinedClassInfo, pagination?.skip, pagination?.limit); + const pageContent: CourseInfoDto[] = this.applyPagination( + courseInfosFromCourses, + pagination?.skip, + pagination?.limit + ); - const page: Page = new Page(pageContent, combinedClassInfo.length); + const page: Page = new Page(pageContent, courseInfosFromCourses.length); return page; } - private getCourseInfosFromCourses(courses: Course[], schoolYearQueryType: SchoolYearQueryType | undefined): Course[] { + private async getCourseInfosFromCourses( + courses: Course[], + courseStatusQueryType: CourseStatusQueryType | undefined + ): Promise { const now = new Date(); let untilDate; const allCourses = courses; - if (!schoolYearQueryType || schoolYearQueryType === SchoolYearQueryType.CURRENT_YEAR) { + if (!courseStatusQueryType || courseStatusQueryType === CourseStatusQueryType.CURRENT) { allCourses.filter((course: Course) => { untilDate = course.untilDate ?? now.getDate() + 1; return now < untilDate; @@ -90,7 +99,69 @@ export class CourseUc { return now > untilDate; }); } + const courseInfos: CourseInfoDto[] = await Promise.all[]>( + allCourses.map(async (course): Promise => { + const groupName = await this.getSyncedGroupName(course); + const teachers = await this.getCourseTeachers(course); + const classes = await this.getCourseClasses(course); + const mapped = new CourseInfoDto({ + id: course.id, + name: course.name, + classes, + teachers, + syncedWithGroup: groupName, + }); + return mapped; + }) + ); + + return courseInfos; + } + + private applyPagination(courseInfo: CourseInfoDto[], skip = 0, limit?: number): CourseInfoDto[] { + let page: CourseInfoDto[]; + + if (limit === -1) { + page = courseInfo.slice(skip); + } else { + page = courseInfo.slice(skip, limit ? skip + limit : courseInfo.length); + } + + return page; + } + + private async getSyncedGroupName(course: Course): Promise { + let grouName = ''; + const { syncedWithGroup } = course.getProps(); + + if (syncedWithGroup) { + const group: Group = await this.groupService.findById(syncedWithGroup); + grouName = group.name; + } + return grouName; + } - return allCourses; + private async getCourseTeachers(course: Course): Promise { + const { teacherIds } = course.getProps(); + const teacherNames: string[] = await Promise.all[]>( + teacherIds.map(async (teacherId): Promise => { + const teacher: UserDO = await this.userService.findById(teacherId); + const fullName = teacher.firstName.concat('').concat(teacher.lastName); + + return fullName; + }) + ); + return teacherNames; + } + + private async getCourseClasses(course: Course): Promise { + const { classIds } = course.getProps(); + const classNames: string[] = await Promise.all[]>( + classIds.map(async (classId): Promise => { + const clazz = await this.classService.findById(classId); + return clazz.name; + }) + ); + return classNames; } } diff --git a/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts b/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts index 5283ba2451e..20cc9189494 100644 --- a/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts +++ b/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts @@ -1,29 +1,24 @@ import { EntityId } from '@shared/domain/types'; -import { UserDto } from '../../../user/uc/dto/user.dto'; -import { ClassInfoDto } from './class-info.dto'; export class CourseInfoDto { id: EntityId; name: string; - teachers: UserDto[]; + teachers: string[]; - classes: ClassInfoDto[]; + classes: string[]; - schoolYear?: string; + courseStatus?: string; - studentCount: number; - - syncedWithGroup?: ClassInfoDto; + syncedWithGroup?: string; constructor(props: CourseInfoDto) { this.id = props.id; this.name = props.name; this.classes = props.classes; this.teachers = props.teachers; - this.schoolYear = props.schoolYear; - this.studentCount = props.studentCount; + this.courseStatus = props.courseStatus; this.syncedWithGroup = props.syncedWithGroup; } } From cab5fdd54b1e7d6253c0b065f24c46fbefa50e33 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Thu, 15 Aug 2024 09:04:33 +0200 Subject: [PATCH 13/64] N21-2075 WIP + Clean up --- .../learnroom/controller/course.controller.ts | 8 +-- .../interface/school-year-query-type.enum.ts | 4 -- .../dto/response/class-info.reponse.ts | 15 ----- .../controller/dto/response/index.ts | 1 - .../src/modules/learnroom/domain/do/course.ts | 4 ++ .../learnroom/service/course.service.ts | 6 -- .../src/modules/learnroom/uc/course.uc.ts | 59 ++++++++++--------- .../learnroom/uc/dto/class-info.dto.ts | 12 ---- .../learnroom/uc/dto/group-info.dto.ts | 12 ---- .../src/modules/learnroom/uc/dto/index.ts | 1 - .../src/shared/domain/entity/course.entity.ts | 1 - .../shared/domain/types/learnroom.types.ts | 3 - .../src/shared/repo/course/course.repo.ts | 10 +--- 13 files changed, 38 insertions(+), 98 deletions(-) delete mode 100644 apps/server/src/modules/learnroom/controller/dto/interface/school-year-query-type.enum.ts delete mode 100644 apps/server/src/modules/learnroom/controller/dto/response/class-info.reponse.ts delete mode 100644 apps/server/src/modules/learnroom/uc/dto/class-info.dto.ts delete mode 100644 apps/server/src/modules/learnroom/uc/dto/group-info.dto.ts diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index c77185f98c7..8403a0f504f 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -29,12 +29,11 @@ import { import { PaginationParams } from '@shared/controller/'; import { Page } from '@shared/domain/domainobject'; import { Response } from 'express'; -import { ErrorResponse } from '../../../core/error/dto'; -import { ClassInfoSearchListResponse } from '../../group/controller/dto'; +import { ErrorResponse } from '@src/core/error/dto'; import { CourseInfoResponseMapper } from '../mapper/course-info-response.mapper'; import { CourseMapper } from '../mapper/course.mapper'; import { CourseExportUc, CourseImportUc, CourseSyncUc, CourseUc } from '../uc'; -import { CourseInfoDto } from '../uc/dto/course-info.dto'; +import { CourseInfoDto } from '../uc/dto'; import { CommonCartridgeFileValidatorPipe } from '../utils'; import { CourseExportBodyParams, @@ -47,7 +46,7 @@ import { } from './dto'; import { CourseFilterParams } from './dto/request/course-filter-params'; import { CourseSortParams } from './dto/request/course-sort-params'; -import { CourseInfoListResponse } from './dto/response/course-info-list.response'; +import { CourseInfoListResponse } from './dto/response'; @ApiTags('Courses') @Authenticate('jwt') @@ -165,7 +164,6 @@ export class CourseController { @ApiResponse({ status: '5XX', type: ErrorResponse }) async getAllCourses( @CurrentUser() currentUser: ICurrentUser, - @Query() schoolParams: SchoolParams, @Query() pagination: PaginationParams, @Query() sortingQuery: CourseSortParams, @Query() filterParams: CourseFilterParams diff --git a/apps/server/src/modules/learnroom/controller/dto/interface/school-year-query-type.enum.ts b/apps/server/src/modules/learnroom/controller/dto/interface/school-year-query-type.enum.ts deleted file mode 100644 index a0fade12250..00000000000 --- a/apps/server/src/modules/learnroom/controller/dto/interface/school-year-query-type.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum SchoolYearQueryType { - CURRENT_YEAR = 'currentYear', - PREVIOUS_YEARS = 'previousYears', -} diff --git a/apps/server/src/modules/learnroom/controller/dto/response/class-info.reponse.ts b/apps/server/src/modules/learnroom/controller/dto/response/class-info.reponse.ts deleted file mode 100644 index db7a2d76676..00000000000 --- a/apps/server/src/modules/learnroom/controller/dto/response/class-info.reponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { EntityId } from '@shared/domain/types'; - -export class ClassInfoResponse { - @ApiProperty() - id: EntityId; - - @ApiProperty() - name: string; - - constructor(props: ClassInfoResponse) { - this.id = props.id; - this.name = props.name; - } -} diff --git a/apps/server/src/modules/learnroom/controller/dto/response/index.ts b/apps/server/src/modules/learnroom/controller/dto/response/index.ts index 2bf424446be..7406186c124 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/index.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/index.ts @@ -1,3 +1,2 @@ export { CourseInfoListResponse } from './course-info-list.response'; export { CourseInfoResponse } from './course-info.response'; -export { ClassInfoResponse } from './class-info.reponse'; diff --git a/apps/server/src/modules/learnroom/domain/do/course.ts b/apps/server/src/modules/learnroom/domain/do/course.ts index 067c5aa899f..9b133666873 100644 --- a/apps/server/src/modules/learnroom/domain/do/course.ts +++ b/apps/server/src/modules/learnroom/domain/do/course.ts @@ -65,6 +65,10 @@ export class Course extends DomainObject { return this.props.substitutionTeacherIds; } + get classes(): EntityId[] { + return this.props.classIds; + } + set startDate(value: Date | undefined) { this.props.startDate = value; } diff --git a/apps/server/src/modules/learnroom/service/course.service.ts b/apps/server/src/modules/learnroom/service/course.service.ts index 3248c73e37d..cf7cc52db2d 100644 --- a/apps/server/src/modules/learnroom/service/course.service.ts +++ b/apps/server/src/modules/learnroom/service/course.service.ts @@ -97,10 +97,4 @@ export class CourseService implements DeletionService, IEventHandler { - const courses = await this.repo.findCoursesBySchoolId(schoolId); - - return courses; - } } diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index e4d85037b0d..dd092c5fb3f 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -99,23 +99,10 @@ export class CourseUc { return now > untilDate; }); } - const courseInfos: CourseInfoDto[] = await Promise.all[]>( - allCourses.map(async (course): Promise => { - const groupName = await this.getSyncedGroupName(course); - const teachers = await this.getCourseTeachers(course); - const classes = await this.getCourseClasses(course); - const mapped = new CourseInfoDto({ - id: course.id, - name: course.name, - classes, - teachers, - syncedWithGroup: groupName, - }); - return mapped; - }) - ); - return courseInfos; + const resolvedCourses = await this.getCourseData(allCourses); + + return resolvedCourses; } private applyPagination(courseInfo: CourseInfoDto[], skip = 0, limit?: number): CourseInfoDto[] { @@ -130,20 +117,14 @@ export class CourseUc { return page; } - private async getSyncedGroupName(course: Course): Promise { - let grouName = ''; - const { syncedWithGroup } = course.getProps(); + private async getSyncedGroupName(groupId: EntityId): Promise { + const group: Group = await this.groupService.findById(groupId); - if (syncedWithGroup) { - const group: Group = await this.groupService.findById(syncedWithGroup); - grouName = group.name; - } - return grouName; + return group.name; } - private async getCourseTeachers(course: Course): Promise { - const { teacherIds } = course.getProps(); - const teacherNames: string[] = await Promise.all[]>( + private async getCourseTeachers(teacherIds: EntityId[]): Promise { + const teacherNames: string[] = await Promise.all( teacherIds.map(async (teacherId): Promise => { const teacher: UserDO = await this.userService.findById(teacherId); const fullName = teacher.firstName.concat('').concat(teacher.lastName); @@ -154,8 +135,7 @@ export class CourseUc { return teacherNames; } - private async getCourseClasses(course: Course): Promise { - const { classIds } = course.getProps(); + private async getCourseClasses(classIds: EntityId[]): Promise { const classNames: string[] = await Promise.all[]>( classIds.map(async (classId): Promise => { const clazz = await this.classService.findById(classId); @@ -164,4 +144,25 @@ export class CourseUc { ); return classNames; } + + private async getCourseData(courses: Course[]) { + const courseInfos: CourseInfoDto[] = await Promise.all( + courses.map(async (course) => { + const groupName = course.syncedWithGroup ? await this.getSyncedGroupName(course.syncedWithGroup) : undefined; + const teachers = await this.getCourseTeachers(course.teachers); + const classes = await this.getCourseClasses(course.classes); + + const mapped = new CourseInfoDto({ + id: course.id, + name: course.name, + classes, + teachers, + syncedWithGroup: groupName, + }); + return mapped; + }) + ); + + return courseInfos; + } } diff --git a/apps/server/src/modules/learnroom/uc/dto/class-info.dto.ts b/apps/server/src/modules/learnroom/uc/dto/class-info.dto.ts deleted file mode 100644 index d2666f121f4..00000000000 --- a/apps/server/src/modules/learnroom/uc/dto/class-info.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { EntityId } from '@shared/domain/types'; - -export class ClassInfoDto { - id: EntityId; - - name: string; - - constructor(props: ClassInfoDto) { - this.id = props.id; - this.name = props.name; - } -} diff --git a/apps/server/src/modules/learnroom/uc/dto/group-info.dto.ts b/apps/server/src/modules/learnroom/uc/dto/group-info.dto.ts deleted file mode 100644 index 95823f20a86..00000000000 --- a/apps/server/src/modules/learnroom/uc/dto/group-info.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { EntityId } from '@shared/domain/types'; - -export class GroupInfoDto { - id: EntityId; - - name: string; - - constructor(props: GroupInfoDto) { - this.id = props.id; - this.name = props.name; - } -} diff --git a/apps/server/src/modules/learnroom/uc/dto/index.ts b/apps/server/src/modules/learnroom/uc/dto/index.ts index 70457f56689..3632d17b639 100644 --- a/apps/server/src/modules/learnroom/uc/dto/index.ts +++ b/apps/server/src/modules/learnroom/uc/dto/index.ts @@ -1,2 +1 @@ export { CourseInfoDto } from './course-info.dto'; -export { ClassInfoDto } from './class-info.dto'; diff --git a/apps/server/src/shared/domain/entity/course.entity.ts b/apps/server/src/shared/domain/entity/course.entity.ts index f6bd1227e72..3bad3a7e62a 100644 --- a/apps/server/src/shared/domain/entity/course.entity.ts +++ b/apps/server/src/shared/domain/entity/course.entity.ts @@ -227,7 +227,6 @@ export class Course extends BaseEntityWithTimestamps implements Learnroom, Entit startDate: this.startDate, copyingSince: this.copyingSince, isSynchronized: !!this.syncedWithGroup, - syncedWithGroup: this.syncedWithGroup, }; } diff --git a/apps/server/src/shared/domain/types/learnroom.types.ts b/apps/server/src/shared/domain/types/learnroom.types.ts index 1ebeeb3b98e..cc81a291728 100644 --- a/apps/server/src/shared/domain/types/learnroom.types.ts +++ b/apps/server/src/shared/domain/types/learnroom.types.ts @@ -1,5 +1,3 @@ -import { GroupEntity } from '@modules/group/entity'; - export enum LearnroomTypes { 'Course' = 'course', } @@ -14,5 +12,4 @@ export type LearnroomMetadata = { untilDate?: Date; copyingSince?: Date; isSynchronized: boolean; - syncedWithGroup?: GroupEntity; }; diff --git a/apps/server/src/shared/repo/course/course.repo.ts b/apps/server/src/shared/repo/course/course.repo.ts index de6df00ea61..fb52b8c115e 100644 --- a/apps/server/src/shared/repo/course/course.repo.ts +++ b/apps/server/src/shared/repo/course/course.repo.ts @@ -1,9 +1,7 @@ import { FilterQuery, QueryOrderMap } from '@mikro-orm/core'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { CourseEntityMapper } from '@modules/learnroom/repo/mikro-orm/mapper/course.entity.mapper'; import { Injectable } from '@nestjs/common'; -import { Course as CourseEntity, Course } from '@shared/domain/entity'; +import { Course } from '@shared/domain/entity'; import { IFindOptions } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { BaseRepo } from '../base.repo'; @@ -139,10 +137,4 @@ export class CourseRepo extends BaseRepo { return course; } - - public async findCoursesBySchoolId(id: EntityId): Promise { - const courses: Course[] = await this._em.find(Course, { school: id }); - - return courses; - } } From 02d9a3981dd056a67435b290052e8523d3bcdf23 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Thu, 15 Aug 2024 15:24:03 +0200 Subject: [PATCH 14/64] N21-2075 change to classNames --- .../controller/dto/response/course-info.response.ts | 4 ++-- .../learnroom/mapper/course-info-response.mapper.ts | 2 +- .../src/modules/learnroom/repo/mikro-orm/course.repo.ts | 8 ++++++++ apps/server/src/modules/learnroom/uc/course.uc.ts | 1 + 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts b/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts index 7df5a302a9b..5eb6ec4945e 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts @@ -12,7 +12,7 @@ export class CourseInfoResponse { teacherNames: string[]; @ApiProperty() - classes: string[]; + classNames: string[]; @ApiPropertyOptional() courseStatus?: string; @@ -23,7 +23,7 @@ export class CourseInfoResponse { constructor(props: CourseInfoResponse) { this.id = props.id; this.name = props.name; - this.classes = props.classes; + this.classNames = props.classNames; this.teacherNames = props.teacherNames; this.courseStatus = props.courseStatus; this.syncedGroup = props.syncedGroup; diff --git a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts index 4148553a417..56f6636b05b 100644 --- a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts @@ -26,7 +26,7 @@ export class CourseInfoResponseMapper { const courseInfoResponse: CourseInfoResponse = new CourseInfoResponse({ id: courseInfo.id, name: courseInfo.name, - classes: courseInfo.classes, + classNames: courseInfo.classes, teacherNames: courseInfo.teachers, courseStatus: courseInfo.courseStatus, syncedGroup: courseInfo.syncedWithGroup ? courseInfo.syncedWithGroup : undefined, diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts index 1ae9bd9bfdc..88dd8ee8889 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts @@ -48,6 +48,14 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo { const entities: CourseEntity[] = await this.em.find(CourseEntity, { school: id }); + await Promise.all( + entities.map(async (entity: CourseEntity): Promise => { + if (!entity.courseGroups.isInitialized()) { + await entity.courseGroups.init(); + } + }) + ); + const courses: Course[] = entities.map((entity: CourseEntity): Course => CourseEntityMapper.mapEntityToDo(entity)); return courses; diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index dd092c5fb3f..4d6c8615871 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -159,6 +159,7 @@ export class CourseUc { teachers, syncedWithGroup: groupName, }); + return mapped; }) ); From a18e3a13fe9ea15aed05b9d94ed67cf011a44a8b Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Thu, 15 Aug 2024 17:05:21 +0200 Subject: [PATCH 15/64] N21-2075 fix teacherName --- apps/server/src/modules/learnroom/uc/course.uc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index 4d6c8615871..3306f8496d7 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -127,7 +127,7 @@ export class CourseUc { const teacherNames: string[] = await Promise.all( teacherIds.map(async (teacherId): Promise => { const teacher: UserDO = await this.userService.findById(teacherId); - const fullName = teacher.firstName.concat('').concat(teacher.lastName); + const fullName = teacher.firstName.concat(' ').concat(teacher.lastName); return fullName; }) From 2a1970669ac37488806a02f7068f04cb0db70402 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Thu, 15 Aug 2024 17:44:35 +0200 Subject: [PATCH 16/64] update get course infos --- apps/server/src/modules/learnroom/uc/course.uc.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index 3306f8496d7..f712a7a87f7 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -87,14 +87,14 @@ export class CourseUc { ): Promise { const now = new Date(); let untilDate; - const allCourses = courses; + let allCourses: Course[]; if (!courseStatusQueryType || courseStatusQueryType === CourseStatusQueryType.CURRENT) { - allCourses.filter((course: Course) => { + allCourses = courses.filter((course: Course) => { untilDate = course.untilDate ?? now.getDate() + 1; return now < untilDate; }); } else { - allCourses.filter((course) => { + allCourses = courses.filter((course) => { untilDate = course.untilDate ?? now.getDate() + 1; return now > untilDate; }); From 152746997ac98d99eefad09268538b230b38068f Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Fri, 16 Aug 2024 12:02:39 +0200 Subject: [PATCH 17/64] update course DO --- apps/server/src/modules/learnroom/domain/do/course.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/server/src/modules/learnroom/domain/do/course.ts b/apps/server/src/modules/learnroom/domain/do/course.ts index 9b133666873..f1c2991ad12 100644 --- a/apps/server/src/modules/learnroom/domain/do/course.ts +++ b/apps/server/src/modules/learnroom/domain/do/course.ts @@ -77,6 +77,10 @@ export class Course extends DomainObject { this.props.untilDate = value; } + get untilDate(): Date | undefined { + return this.props.untilDate; + } + set syncedWithGroup(value: EntityId | undefined) { this.props.syncedWithGroup = value; } From d316d7fa20c2b838a1c8f67ab3ad0f08ff64b10f Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Fri, 16 Aug 2024 12:28:37 +0200 Subject: [PATCH 18/64] N21-2075 fix classNames --- .../learnroom/controller/course.controller.ts | 1 - .../interface/course-sort-query-type.enum.ts | 2 +- .../src/modules/learnroom/domain/do/course.ts | 4 +++ .../domain/interface/course.repo.interface.ts | 2 +- .../learnroom/repo/mikro-orm/course.repo.ts | 2 +- .../learnroom/service/course-do.service.ts | 6 +---- .../modules/learnroom/uc/course-sync.uc.ts | 4 +-- .../src/modules/learnroom/uc/course.uc.ts | 25 ++++++++++++++----- .../domain/interface/permission.enum.ts | 1 + 9 files changed, 30 insertions(+), 17 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 8403a0f504f..9fc96f099ef 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -42,7 +42,6 @@ import { CourseQueryParams, CourseSyncBodyParams, CourseUrlParams, - SchoolParams, } from './dto'; import { CourseFilterParams } from './dto/request/course-filter-params'; import { CourseSortParams } from './dto/request/course-sort-params'; diff --git a/apps/server/src/modules/learnroom/controller/dto/interface/course-sort-query-type.enum.ts b/apps/server/src/modules/learnroom/controller/dto/interface/course-sort-query-type.enum.ts index f1bfcd99528..13e092feb9e 100644 --- a/apps/server/src/modules/learnroom/controller/dto/interface/course-sort-query-type.enum.ts +++ b/apps/server/src/modules/learnroom/controller/dto/interface/course-sort-query-type.enum.ts @@ -2,5 +2,5 @@ export enum CourseSortQueryType { NAME = 'name', SYNCHRONIZED_GROUP = 'synchronizedGroup', TEACHER_NAMES = 'teacherNames', - CLASS_NAME = 'className', + CLASS_NAMES = 'classNames', } diff --git a/apps/server/src/modules/learnroom/domain/do/course.ts b/apps/server/src/modules/learnroom/domain/do/course.ts index 9b133666873..a6e8eb84bce 100644 --- a/apps/server/src/modules/learnroom/domain/do/course.ts +++ b/apps/server/src/modules/learnroom/domain/do/course.ts @@ -69,6 +69,10 @@ export class Course extends DomainObject { return this.props.classIds; } + get groups(): EntityId[] { + return this.props.groupIds; + } + set startDate(value: Date | undefined) { this.props.startDate = value; } diff --git a/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts b/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts index 721490c6c17..de8ae036491 100644 --- a/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts +++ b/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts @@ -8,7 +8,7 @@ export interface CourseRepo extends BaseDomainObjectRepoInterface { findBySyncedGroup(group: Group): Promise; - findCoursesBySchoolId(schoolId: EntityId): Promise; + findBySchoolId(schoolId: EntityId): Promise; } export const COURSE_REPO = Symbol('COURSE_REPO'); diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts index 88dd8ee8889..90f3acf54c0 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts @@ -45,7 +45,7 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo { + public async findBySchoolId(id: EntityId): Promise { const entities: CourseEntity[] = await this.em.find(CourseEntity, { school: id }); await Promise.all( diff --git a/apps/server/src/modules/learnroom/service/course-do.service.ts b/apps/server/src/modules/learnroom/service/course-do.service.ts index 5a6660ff2a4..399b7ec2221 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.ts @@ -38,17 +38,13 @@ export class CourseDoService implements AuthorizationLoaderServiceGeneric { - if (course.syncedWithGroup) { - throw new CourseAlreadySynchronizedLoggableException(course.id); - } - course.syncedWithGroup = group.id; await this.courseRepo.save(course); } public async findCoursesBySchool(schoolId: EntityId): Promise { - const courses: Course[] = await this.courseRepo.findCoursesBySchoolId(schoolId); + const courses: Course[] = await this.courseRepo.findBySchoolId(schoolId); return courses; } diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts index 745daf063fd..d58c99d3613 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts @@ -1,9 +1,9 @@ import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { Group, GroupService } from '@modules/group'; import { Injectable } from '@nestjs/common'; import { type User as UserEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { Group, GroupService } from '@modules/group'; import { Course } from '../domain'; import { CourseDoService } from '../service'; @@ -35,7 +35,7 @@ export class CourseSyncUc { this.authorizationService.checkPermission( user, course, - AuthorizationContextBuilder.write([Permission.COURSE_EDIT]) + AuthorizationContextBuilder.write([Permission.COURSE_EDIT, Permission.COURSE_LIST]) ); await this.courseService.startSynchronization(course, group); diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index f712a7a87f7..7dd3e339da0 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -136,26 +136,39 @@ export class CourseUc { } private async getCourseClasses(classIds: EntityId[]): Promise { - const classNames: string[] = await Promise.all[]>( + const classes: string[] = await Promise.all[]>( classIds.map(async (classId): Promise => { const clazz = await this.classService.findById(classId); - return clazz.name; + + return clazz.gradeLevel ? clazz.gradeLevel?.toString().concat(clazz.name) : clazz.name; + }) + ); + return classes; + } + + private async getCourseGroups(groupIds: EntityId[]): Promise { + const groups: string[] = await Promise.all[]>( + groupIds.map(async (groupId): Promise => { + const group = await this.groupService.findById(groupId); + + return group.name; }) ); - return classNames; + return groups; } private async getCourseData(courses: Course[]) { const courseInfos: CourseInfoDto[] = await Promise.all( courses.map(async (course) => { const groupName = course.syncedWithGroup ? await this.getSyncedGroupName(course.syncedWithGroup) : undefined; - const teachers = await this.getCourseTeachers(course.teachers); - const classes = await this.getCourseClasses(course.classes); + const teachers: string[] = await this.getCourseTeachers(course.teachers); + const classes: string[] = await this.getCourseClasses(course.classes); + const groups: string[] = await this.getCourseGroups(course.groups); const mapped = new CourseInfoDto({ id: course.id, name: course.name, - classes, + classes: [...classes, ...groups], teachers, syncedWithGroup: groupName, }); diff --git a/apps/server/src/shared/domain/interface/permission.enum.ts b/apps/server/src/shared/domain/interface/permission.enum.ts index d85ed328e55..25ba66dd868 100644 --- a/apps/server/src/shared/domain/interface/permission.enum.ts +++ b/apps/server/src/shared/domain/interface/permission.enum.ts @@ -29,6 +29,7 @@ export enum Permission { COURSE_CREATE = 'COURSE_CREATE', COURSE_DELETE = 'COURSE_DELETE', COURSE_EDIT = 'COURSE_EDIT', + COURSE_LIST = 'COURSE_LIST', COURSE_REMOVE = 'COURSE_REMOVE', COURSE_VIEW = 'COURSE_VIEW', CREATE_SUPPORT_JWT = 'CREATE_SUPPORT_JWT', From 19f68603cd94144bb90912d661691964227eb163 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Fri, 16 Aug 2024 15:58:47 +0200 Subject: [PATCH 19/64] N21-2075 permission check --- apps/server/src/modules/learnroom/uc/course-sync.uc.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts index d58c99d3613..e2f8009f98f 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts @@ -4,6 +4,7 @@ import { Injectable } from '@nestjs/common'; import { type User as UserEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; +import { School, SchoolService } from '../../school'; import { Course } from '../domain'; import { CourseDoService } from '../service'; @@ -12,7 +13,8 @@ export class CourseSyncUc { constructor( private readonly authorizationService: AuthorizationService, private readonly courseService: CourseDoService, - private readonly groupService: GroupService + private readonly groupService: GroupService, + private readonly schoolService: SchoolService ) {} public async stopSynchronization(userId: EntityId, courseId: EntityId): Promise { @@ -32,10 +34,12 @@ export class CourseSyncUc { const course: Course = await this.courseService.findById(courseId); const group: Group = await this.groupService.findById(groupId); const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); + const school: School = await this.schoolService.getSchoolById(user.school.id); + this.authorizationService.checkPermission( user, - course, - AuthorizationContextBuilder.write([Permission.COURSE_EDIT, Permission.COURSE_LIST]) + school, + AuthorizationContextBuilder.write([Permission.COURSE_LIST]) ); await this.courseService.startSynchronization(course, group); From 51d8801be431b6c092e6af3e9edec5f1066b20cb Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Mon, 19 Aug 2024 08:50:20 +0200 Subject: [PATCH 20/64] update course synchronization --- .../learnroom/controller/course.controller.ts | 11 ++++++-- .../interface/course-request-context.enum.ts | 1 + .../learnroom/service/course-do.service.ts | 1 - .../modules/learnroom/uc/course-sync.uc.ts | 27 ++++++++++++++----- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 9fc96f099ef..39bc8ce29e1 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -46,6 +46,7 @@ import { import { CourseFilterParams } from './dto/request/course-filter-params'; import { CourseSortParams } from './dto/request/course-sort-params'; import { CourseInfoListResponse } from './dto/response'; +import { CourseCallerParams } from './dto/request/course-caller-params'; @ApiTags('Courses') @Authenticate('jwt') @@ -151,9 +152,15 @@ export class CourseController { public async startSynchronization( @CurrentUser() currentUser: ICurrentUser, @Param() params: CourseUrlParams, - @Body() bodyParams: CourseSyncBodyParams + @Body() bodyParams: CourseSyncBodyParams, + @Query() callerParams: CourseCallerParams ): Promise { - await this.courseSyncUc.startSynchronization(currentUser.userId, params.courseId, bodyParams.groupId); + await this.courseSyncUc.startSynchronization( + currentUser.userId, + params.courseId, + bodyParams.groupId, + callerParams.calledFrom + ); } @Get('/all') diff --git a/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts b/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts index 4176b8e1bad..7ca602b8e41 100644 --- a/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts +++ b/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts @@ -1,3 +1,4 @@ export enum CourseRequestContext { + COURSE_OVERVIEW = 'course-overview', COURSE_ADMIN_OVERVIEW = 'course-admin-overview', } diff --git a/apps/server/src/modules/learnroom/service/course-do.service.ts b/apps/server/src/modules/learnroom/service/course-do.service.ts index 399b7ec2221..e33b16cbb62 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.ts @@ -3,7 +3,6 @@ import { type Group } from '@modules/group'; import { Inject, Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { type Course, COURSE_REPO, CourseNotSynchronizedLoggableException, CourseRepo } from '../domain'; -import { CourseAlreadySynchronizedLoggableException } from '../domain/error/course-already-synchronized.loggable-exception'; @Injectable() export class CourseDoService implements AuthorizationLoaderServiceGeneric { diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts index e2f8009f98f..b6eec5b2ebe 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts @@ -5,6 +5,7 @@ import { type User as UserEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { School, SchoolService } from '../../school'; +import { CourseRequestContext } from '../controller/dto/interface/course-request-context.enum'; import { Course } from '../domain'; import { CourseDoService } from '../service'; @@ -30,17 +31,29 @@ export class CourseSyncUc { await this.courseService.stopSynchronization(course); } - public async startSynchronization(userId: string, courseId: string, groupId: string) { + public async startSynchronization( + userId: string, + courseId: string, + groupId: string, + calledFrom?: CourseRequestContext + ) { const course: Course = await this.courseService.findById(courseId); const group: Group = await this.groupService.findById(groupId); const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); const school: School = await this.schoolService.getSchoolById(user.school.id); - - this.authorizationService.checkPermission( - user, - school, - AuthorizationContextBuilder.write([Permission.COURSE_LIST]) - ); + if (!calledFrom || calledFrom === CourseRequestContext.COURSE_ADMIN_OVERVIEW) { + this.authorizationService.checkPermission( + user, + school, + AuthorizationContextBuilder.write([Permission.COURSE_LIST]) + ); + } else if (calledFrom === CourseRequestContext.COURSE_OVERVIEW) { + this.authorizationService.checkPermission( + user, + course, + AuthorizationContextBuilder.write([Permission.COURSE_EDIT]) + ); + } await this.courseService.startSynchronization(course, group); } From cbf29e3ded480c3c0b104837888d66c65f558d45 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Mon, 19 Aug 2024 11:35:37 +0200 Subject: [PATCH 21/64] N21-2075 fix permission check --- .../authorization/domain/rules/course.rule.ts | 23 ++++++++++++------- .../domain/interface/permission.enum.ts | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/server/src/modules/authorization/domain/rules/course.rule.ts b/apps/server/src/modules/authorization/domain/rules/course.rule.ts index f4c3b51f84a..2659d720350 100644 --- a/apps/server/src/modules/authorization/domain/rules/course.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/course.rule.ts @@ -1,6 +1,7 @@ import { Course } from '@modules/learnroom/domain'; import { Injectable } from '@nestjs/common'; import { Course as CourseEntity, User } from '@shared/domain/entity'; +import { Permission } from '@shared/domain/interface'; import { AuthorizationHelper } from '../service/authorization.helper'; import { Action, AuthorizationContext, Rule } from '../type'; @@ -16,14 +17,20 @@ export class CourseRule implements Rule { public hasPermission(user: User, object: CourseEntity | Course, context: AuthorizationContext): boolean { const { action, requiredPermissions } = context; - const hasPermission = - this.authorizationHelper.hasAllPermissions(user, requiredPermissions) && - this.authorizationHelper.hasAccessToEntity( - user, - object, - action === Action.read ? ['teachers', 'substitutionTeachers', 'students'] : ['teachers', 'substitutionTeachers'] - ); - return hasPermission; + const hasRequiredPermission = this.authorizationHelper.hasAllPermissions(user, requiredPermissions); + const hasAdminPermission = this.authorizationHelper.hasAllPermissions(user, [Permission.COURSE_ADMINISTRATION]); + + const hasAccessToEntity = hasAdminPermission + ? true + : this.authorizationHelper.hasAccessToEntity( + user, + object, + action === Action.read + ? ['teachers', 'substitutionTeachers', 'students'] + : ['teachers', 'substitutionTeachers'] + ); + + return hasAccessToEntity && hasRequiredPermission; } } diff --git a/apps/server/src/shared/domain/interface/permission.enum.ts b/apps/server/src/shared/domain/interface/permission.enum.ts index 25ba66dd868..70d57507164 100644 --- a/apps/server/src/shared/domain/interface/permission.enum.ts +++ b/apps/server/src/shared/domain/interface/permission.enum.ts @@ -26,10 +26,10 @@ export enum Permission { CONTEXT_TOOL_USER = 'CONTEXT_TOOL_USER', COURSEGROUP_CREATE = 'COURSEGROUP_CREATE', COURSEGROUP_EDIT = 'COURSEGROUP_EDIT', + COURSE_ADMINISTRATION = 'COURSE_ADMINISTRATION', COURSE_CREATE = 'COURSE_CREATE', COURSE_DELETE = 'COURSE_DELETE', COURSE_EDIT = 'COURSE_EDIT', - COURSE_LIST = 'COURSE_LIST', COURSE_REMOVE = 'COURSE_REMOVE', COURSE_VIEW = 'COURSE_VIEW', CREATE_SUPPORT_JWT = 'CREATE_SUPPORT_JWT', From 05743f2b1a3cfd2a276de198964a24d1076bc4b1 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Tue, 20 Aug 2024 11:01:16 +0200 Subject: [PATCH 22/64] update course sync start --- .../learnroom/controller/course.controller.ts | 13 ++------- .../learnroom/uc/course-sync.uc.spec.ts | 16 ++++++++-- .../modules/learnroom/uc/course-sync.uc.ts | 29 +++++-------------- 3 files changed, 24 insertions(+), 34 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 39bc8ce29e1..c65b46c8271 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -28,8 +28,8 @@ import { } from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller/'; import { Page } from '@shared/domain/domainobject'; -import { Response } from 'express'; import { ErrorResponse } from '@src/core/error/dto'; +import { Response } from 'express'; import { CourseInfoResponseMapper } from '../mapper/course-info-response.mapper'; import { CourseMapper } from '../mapper/course.mapper'; import { CourseExportUc, CourseImportUc, CourseSyncUc, CourseUc } from '../uc'; @@ -46,7 +46,6 @@ import { import { CourseFilterParams } from './dto/request/course-filter-params'; import { CourseSortParams } from './dto/request/course-sort-params'; import { CourseInfoListResponse } from './dto/response'; -import { CourseCallerParams } from './dto/request/course-caller-params'; @ApiTags('Courses') @Authenticate('jwt') @@ -152,15 +151,9 @@ export class CourseController { public async startSynchronization( @CurrentUser() currentUser: ICurrentUser, @Param() params: CourseUrlParams, - @Body() bodyParams: CourseSyncBodyParams, - @Query() callerParams: CourseCallerParams + @Body() bodyParams: CourseSyncBodyParams ): Promise { - await this.courseSyncUc.startSynchronization( - currentUser.userId, - params.courseId, - bodyParams.groupId, - callerParams.calledFrom - ); + await this.courseSyncUc.startSynchronization(currentUser.userId, params.courseId, bodyParams.groupId); } @Get('/all') diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts index 13630167092..29b2bd6b1d9 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts @@ -1,12 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { GroupService } from '@modules/group'; +import { SchoolService } from '@modules/school'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; import { groupFactory, setupEntities, userFactory } from '@shared/testing'; import { CourseDoService } from '../service'; import { courseFactory } from '../testing'; import { CourseSyncUc } from './course-sync.uc'; +import { schoolFactory } from '../../school/testing'; describe(CourseSyncUc.name, () => { let module: TestingModule; @@ -15,6 +17,7 @@ describe(CourseSyncUc.name, () => { let authorizationService: DeepMocked; let courseService: DeepMocked; let groupService: DeepMocked; + let schoolService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -32,6 +35,10 @@ describe(CourseSyncUc.name, () => { provide: GroupService, useValue: createMock(), }, + { + provide: SchoolService, + useValue: createMock(), + }, ], }).compile(); @@ -39,6 +46,7 @@ describe(CourseSyncUc.name, () => { authorizationService = module.get(AuthorizationService); courseService = module.get(CourseDoService); groupService = module.get(GroupService); + schoolService = module.get(SchoolService); await setupEntities(); }); @@ -93,27 +101,29 @@ describe(CourseSyncUc.name, () => { const user = userFactory.buildWithId(); const course = courseFactory.build(); const group = groupFactory.build(); + const school = schoolFactory.build(); courseService.findById.mockResolvedValueOnce(course); groupService.findById.mockResolvedValueOnce(group); - + schoolService.getSchoolById.mockResolvedValueOnce(school); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); return { user, course, group, + school, }; }; it('should check the users permission', async () => { - const { user, course, group } = setup(); + const { user, course, group, school } = setup(); await uc.startSynchronization(user.id, course.id, group.id); expect(authorizationService.checkPermission).toHaveBeenCalledWith( user, - course, + school, AuthorizationContextBuilder.write([Permission.COURSE_EDIT]) ); }); diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts index b6eec5b2ebe..d3e81dcbb92 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts @@ -1,11 +1,10 @@ import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { Group, GroupService } from '@modules/group'; +import { School, SchoolService } from '@modules/school'; import { Injectable } from '@nestjs/common'; import { type User as UserEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { School, SchoolService } from '../../school'; -import { CourseRequestContext } from '../controller/dto/interface/course-request-context.enum'; import { Course } from '../domain'; import { CourseDoService } from '../service'; @@ -31,29 +30,17 @@ export class CourseSyncUc { await this.courseService.stopSynchronization(course); } - public async startSynchronization( - userId: string, - courseId: string, - groupId: string, - calledFrom?: CourseRequestContext - ) { + public async startSynchronization(userId: string, courseId: string, groupId: string) { const course: Course = await this.courseService.findById(courseId); const group: Group = await this.groupService.findById(groupId); const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); const school: School = await this.schoolService.getSchoolById(user.school.id); - if (!calledFrom || calledFrom === CourseRequestContext.COURSE_ADMIN_OVERVIEW) { - this.authorizationService.checkPermission( - user, - school, - AuthorizationContextBuilder.write([Permission.COURSE_LIST]) - ); - } else if (calledFrom === CourseRequestContext.COURSE_OVERVIEW) { - this.authorizationService.checkPermission( - user, - course, - AuthorizationContextBuilder.write([Permission.COURSE_EDIT]) - ); - } + + this.authorizationService.checkPermission( + user, + school, + AuthorizationContextBuilder.write([Permission.COURSE_EDIT]) + ); await this.courseService.startSynchronization(course, group); } From 44be93ed8bb7e112825363297d9fbd80ac93f199 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Tue, 20 Aug 2024 13:58:04 +0200 Subject: [PATCH 23/64] N21-2075 fix untilDate --- .../dto/interface/course-request-context.enum.ts | 4 ---- .../dto/request/course-caller-params.ts | 10 ---------- .../modules/learnroom/uc/course-sync.uc.spec.ts | 15 ++------------- .../src/modules/learnroom/uc/course-sync.uc.ts | 7 ++----- apps/server/src/modules/learnroom/uc/course.uc.ts | 8 ++++---- 5 files changed, 8 insertions(+), 36 deletions(-) delete mode 100644 apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts delete mode 100644 apps/server/src/modules/learnroom/controller/dto/request/course-caller-params.ts diff --git a/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts b/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts deleted file mode 100644 index 7ca602b8e41..00000000000 --- a/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum CourseRequestContext { - COURSE_OVERVIEW = 'course-overview', - COURSE_ADMIN_OVERVIEW = 'course-admin-overview', -} diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-caller-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-caller-params.ts deleted file mode 100644 index 6ce30904bfe..00000000000 --- a/apps/server/src/modules/learnroom/controller/dto/request/course-caller-params.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsEnum, IsOptional } from 'class-validator'; -import { CourseRequestContext } from '../interface/course-request-context.enum'; - -export class CourseCallerParams { - @IsOptional() - @IsEnum(CourseRequestContext) - @ApiPropertyOptional({ enum: CourseRequestContext, enumName: 'CourseRequestContext' }) - calledFrom?: CourseRequestContext; -} diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts index 29b2bd6b1d9..76dff1cf9f0 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts @@ -1,14 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { GroupService } from '@modules/group'; -import { SchoolService } from '@modules/school'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; import { groupFactory, setupEntities, userFactory } from '@shared/testing'; import { CourseDoService } from '../service'; import { courseFactory } from '../testing'; import { CourseSyncUc } from './course-sync.uc'; -import { schoolFactory } from '../../school/testing'; describe(CourseSyncUc.name, () => { let module: TestingModule; @@ -17,7 +15,6 @@ describe(CourseSyncUc.name, () => { let authorizationService: DeepMocked; let courseService: DeepMocked; let groupService: DeepMocked; - let schoolService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -35,10 +32,6 @@ describe(CourseSyncUc.name, () => { provide: GroupService, useValue: createMock(), }, - { - provide: SchoolService, - useValue: createMock(), - }, ], }).compile(); @@ -46,7 +39,6 @@ describe(CourseSyncUc.name, () => { authorizationService = module.get(AuthorizationService); courseService = module.get(CourseDoService); groupService = module.get(GroupService); - schoolService = module.get(SchoolService); await setupEntities(); }); @@ -101,29 +93,26 @@ describe(CourseSyncUc.name, () => { const user = userFactory.buildWithId(); const course = courseFactory.build(); const group = groupFactory.build(); - const school = schoolFactory.build(); courseService.findById.mockResolvedValueOnce(course); groupService.findById.mockResolvedValueOnce(group); - schoolService.getSchoolById.mockResolvedValueOnce(school); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); return { user, course, group, - school, }; }; it('should check the users permission', async () => { - const { user, course, group, school } = setup(); + const { user, course, group } = setup(); await uc.startSynchronization(user.id, course.id, group.id); expect(authorizationService.checkPermission).toHaveBeenCalledWith( user, - school, + course, AuthorizationContextBuilder.write([Permission.COURSE_EDIT]) ); }); diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts index d3e81dcbb92..53a4fe2e173 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.ts @@ -1,6 +1,5 @@ import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { Group, GroupService } from '@modules/group'; -import { School, SchoolService } from '@modules/school'; import { Injectable } from '@nestjs/common'; import { type User as UserEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; @@ -13,8 +12,7 @@ export class CourseSyncUc { constructor( private readonly authorizationService: AuthorizationService, private readonly courseService: CourseDoService, - private readonly groupService: GroupService, - private readonly schoolService: SchoolService + private readonly groupService: GroupService ) {} public async stopSynchronization(userId: EntityId, courseId: EntityId): Promise { @@ -34,11 +32,10 @@ export class CourseSyncUc { const course: Course = await this.courseService.findById(courseId); const group: Group = await this.groupService.findById(groupId); const user: UserEntity = await this.authorizationService.getUserWithPermissions(userId); - const school: School = await this.schoolService.getSchoolById(user.school.id); this.authorizationService.checkPermission( user, - school, + course, AuthorizationContextBuilder.write([Permission.COURSE_EDIT]) ); diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index 7dd3e339da0..565d2fa0f69 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -90,13 +90,13 @@ export class CourseUc { let allCourses: Course[]; if (!courseStatusQueryType || courseStatusQueryType === CourseStatusQueryType.CURRENT) { allCourses = courses.filter((course: Course) => { - untilDate = course.untilDate ?? now.getDate() + 1; - return now < untilDate; + untilDate = course.untilDate; + return now < untilDate || untilDate === undefined; }); } else { allCourses = courses.filter((course) => { - untilDate = course.untilDate ?? now.getDate() + 1; - return now > untilDate; + untilDate = course.untilDate; + return now > untilDate && untilDate !== undefined; }); } From 27cd924174872286881f3510e181479a1c44f383 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Tue, 20 Aug 2024 14:02:26 +0200 Subject: [PATCH 24/64] add migration + cleanup --- .../mikro-orm/Migration20240819123013.ts | 37 +++++++++++++++++++ .../interface/course-request-context.enum.ts | 4 -- .../dto/request/course-caller-params.ts | 10 ----- 3 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 apps/server/src/migrations/mikro-orm/Migration20240819123013.ts delete mode 100644 apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts delete mode 100644 apps/server/src/modules/learnroom/controller/dto/request/course-caller-params.ts diff --git a/apps/server/src/migrations/mikro-orm/Migration20240819123013.ts b/apps/server/src/migrations/mikro-orm/Migration20240819123013.ts new file mode 100644 index 00000000000..1bdd05ec471 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20240819123013.ts @@ -0,0 +1,37 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20240819123013 extends Migration { + async up(): Promise { + const adminRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'administrator' }, + { + $addToSet: { + permissions: { + $each: ['COURSE_ADMINISTRATION'], + }, + }, + } + ); + + if (adminRoleUpdate.modifiedCount > 0) { + console.info('Permission COURSE_ADMINISTRATION was added to role administrator.'); + } + } + + async down(): Promise { + const adminRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'administrator' }, + { + $pull: { + permissions: { + $in: ['COURSE_ADMINISTRATION'], + }, + }, + } + ); + + if (adminRoleUpdate.modifiedCount > 0) { + console.info('Rollback: Removed permission COURSE_ADMINISTRATION from role administrator.'); + } + } +} diff --git a/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts b/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts deleted file mode 100644 index 7ca602b8e41..00000000000 --- a/apps/server/src/modules/learnroom/controller/dto/interface/course-request-context.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum CourseRequestContext { - COURSE_OVERVIEW = 'course-overview', - COURSE_ADMIN_OVERVIEW = 'course-admin-overview', -} diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-caller-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-caller-params.ts deleted file mode 100644 index 6ce30904bfe..00000000000 --- a/apps/server/src/modules/learnroom/controller/dto/request/course-caller-params.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsEnum, IsOptional } from 'class-validator'; -import { CourseRequestContext } from '../interface/course-request-context.enum'; - -export class CourseCallerParams { - @IsOptional() - @IsEnum(CourseRequestContext) - @ApiPropertyOptional({ enum: CourseRequestContext, enumName: 'CourseRequestContext' }) - calledFrom?: CourseRequestContext; -} From 813de58a88726f3bd12ed897cf6c938576b9dbc7 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Wed, 21 Aug 2024 16:57:04 +0200 Subject: [PATCH 25/64] update course find all --- .../interface/course-sort-query-type.enum.ts | 6 - .../dto/request/course-filter-params.ts | 2 +- .../dto/request/course-sort-params.ts | 2 +- .../domain/interface/course-filter.ts | 7 ++ .../interface/course-sort-query-type.enum.ts | 6 + .../course-status-query-type.enum.ts | 0 .../domain/interface/course.repo.interface.ts | 5 +- .../learnroom/repo/mikro-orm/course.repo.ts | 42 ++++++- .../learnroom/service/course-do.service.ts | 32 ++++-- .../src/modules/learnroom/uc/course.uc.ts | 106 +++++------------- .../src/shared/repo/course/course.repo.ts | 19 +++- 11 files changed, 125 insertions(+), 102 deletions(-) delete mode 100644 apps/server/src/modules/learnroom/controller/dto/interface/course-sort-query-type.enum.ts create mode 100644 apps/server/src/modules/learnroom/domain/interface/course-filter.ts create mode 100644 apps/server/src/modules/learnroom/domain/interface/course-sort-query-type.enum.ts rename apps/server/src/modules/learnroom/{controller/dto => domain}/interface/course-status-query-type.enum.ts (100%) diff --git a/apps/server/src/modules/learnroom/controller/dto/interface/course-sort-query-type.enum.ts b/apps/server/src/modules/learnroom/controller/dto/interface/course-sort-query-type.enum.ts deleted file mode 100644 index 13e092feb9e..00000000000 --- a/apps/server/src/modules/learnroom/controller/dto/interface/course-sort-query-type.enum.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum CourseSortQueryType { - NAME = 'name', - SYNCHRONIZED_GROUP = 'synchronizedGroup', - TEACHER_NAMES = 'teacherNames', - CLASS_NAMES = 'classNames', -} diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts index 067905f19f1..d3af51409bf 100644 --- a/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts +++ b/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts @@ -1,6 +1,6 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; -import { CourseStatusQueryType } from '../interface/course-status-query-type.enum'; +import { CourseStatusQueryType } from '../../../domain/interface/course-status-query-type.enum'; export class CourseFilterParams { @IsOptional() diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts index 52eee6289c4..964e16b2656 100644 --- a/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts +++ b/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts @@ -1,7 +1,7 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { SortingParams } from '@shared/controller'; import { IsEnum, IsOptional } from 'class-validator'; -import { CourseSortQueryType } from '../interface/course-sort-query-type.enum'; +import { CourseSortQueryType } from '../../../domain/interface/course-sort-query-type.enum'; export class CourseSortParams extends SortingParams { @IsOptional() diff --git a/apps/server/src/modules/learnroom/domain/interface/course-filter.ts b/apps/server/src/modules/learnroom/domain/interface/course-filter.ts new file mode 100644 index 00000000000..13adc757b96 --- /dev/null +++ b/apps/server/src/modules/learnroom/domain/interface/course-filter.ts @@ -0,0 +1,7 @@ +import { EntityId } from '@shared/domain/types'; +import { CourseStatusQueryType } from './course-status-query-type.enum'; + +export interface CourseFilter { + schoolId?: EntityId; + courseStatusQueryType?: CourseStatusQueryType; +} diff --git a/apps/server/src/modules/learnroom/domain/interface/course-sort-query-type.enum.ts b/apps/server/src/modules/learnroom/domain/interface/course-sort-query-type.enum.ts new file mode 100644 index 00000000000..a70b0cd35b4 --- /dev/null +++ b/apps/server/src/modules/learnroom/domain/interface/course-sort-query-type.enum.ts @@ -0,0 +1,6 @@ +export enum CourseSortQueryType { + NAME = 'name', + // SYNCHRONIZED_GROUP = 'syncedWithGroup', + // TEACHER_NAMES = 'teacherNames', + // CLASS_NAMES = 'classNames', +} diff --git a/apps/server/src/modules/learnroom/controller/dto/interface/course-status-query-type.enum.ts b/apps/server/src/modules/learnroom/domain/interface/course-status-query-type.enum.ts similarity index 100% rename from apps/server/src/modules/learnroom/controller/dto/interface/course-status-query-type.enum.ts rename to apps/server/src/modules/learnroom/domain/interface/course-status-query-type.enum.ts diff --git a/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts b/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts index de8ae036491..741d917badc 100644 --- a/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts +++ b/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts @@ -1,14 +1,17 @@ import type { Group } from '@modules/group'; +import { Page } from '@shared//domain/domainobject'; +import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { BaseDomainObjectRepoInterface } from '@shared/repo/base-domain-object.repo.interface'; import { Course } from '../do'; +import { CourseFilter } from './course-filter'; export interface CourseRepo extends BaseDomainObjectRepoInterface { findCourseById(id: EntityId): Promise; findBySyncedGroup(group: Group): Promise; - findBySchoolId(schoolId: EntityId): Promise; + findCourses(filter: CourseFilter, options?: IFindOptions): Promise>; } export const COURSE_REPO = Symbol('COURSE_REPO'); diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts index 90f3acf54c0..dc255a1153d 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts @@ -1,10 +1,15 @@ -import { EntityData, EntityName } from '@mikro-orm/core'; +import { EntityData, EntityName, FindOptions } from '@mikro-orm/core'; import { Group } from '@modules/group'; +import { Page } from '@shared/domain/domainobject/page'; import { Course as CourseEntity } from '@shared/domain/entity'; +import { IFindOptions, SortOrder } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { BaseDomainObjectRepo } from '@shared/repo/base-domain-object.repo'; +import { CourseScope } from '@shared/repo/course/course.repo'; import { Course, CourseRepo } from '../../domain'; import { CourseEntityMapper } from './mapper/course.entity.mapper'; +import { CourseFilter } from '../../domain/interface/course-filter'; +import { CourseStatusQueryType } from '../../domain/interface/course-status-query-type.enum'; export class CourseMikroOrmRepo extends BaseDomainObjectRepo implements CourseRepo { protected get entityName(): EntityName { @@ -45,8 +50,18 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo { - const entities: CourseEntity[] = await this.em.find(CourseEntity, { school: id }); + public async findCourses(filter: CourseFilter, options?: IFindOptions): Promise> { + const scope: CourseScope = new CourseScope(); + scope.bySchoolId(filter.schoolId); + if (filter.courseStatusQueryType === CourseStatusQueryType.CURRENT) { + scope.forActiveCourses(); + } else { + scope.forArchivedCourses(); + } + + const findOptions = this.mapToMikroOrmOptions(options); + + const [entities, total] = await this.em.findAndCount(CourseEntity, scope.query, findOptions); await Promise.all( entities.map(async (entity: CourseEntity): Promise => { @@ -58,6 +73,25 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo CourseEntityMapper.mapEntityToDo(entity)); - return courses; + const page: Page = new Page(courses, total); + + return page; + } + + private mapToMikroOrmOptions

(options?: IFindOptions): FindOptions { + const findOptions: FindOptions = { + offset: options?.pagination?.skip, + limit: options?.pagination?.limit, + orderBy: options?.order, + }; + + // If no order is specified, a default order is applied here, because pagination can be messed up without order. + if (!findOptions.orderBy) { + findOptions.orderBy = { + _id: SortOrder.asc, + }; + } + + return findOptions; } } diff --git a/apps/server/src/modules/learnroom/service/course-do.service.ts b/apps/server/src/modules/learnroom/service/course-do.service.ts index e33b16cbb62..09844b2b011 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.ts @@ -1,32 +1,36 @@ import { AuthorizationLoaderServiceGeneric } from '@modules/authorization'; import { type Group } from '@modules/group'; import { Inject, Injectable } from '@nestjs/common'; +import { Page } from '@shared/domain/domainobject/page'; +import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { type Course, COURSE_REPO, CourseNotSynchronizedLoggableException, CourseRepo } from '../domain'; +import { type Course as CourseDO, COURSE_REPO, CourseNotSynchronizedLoggableException, CourseRepo } from '../domain'; +import { CourseAlreadySynchronizedLoggableException } from '../domain/error/course-already-synchronized.loggable-exception'; +import { CourseFilter } from '../domain/interface/course-filter'; @Injectable() -export class CourseDoService implements AuthorizationLoaderServiceGeneric { +export class CourseDoService implements AuthorizationLoaderServiceGeneric { constructor(@Inject(COURSE_REPO) private readonly courseRepo: CourseRepo) {} - public async findById(courseId: EntityId): Promise { - const courses: Course = await this.courseRepo.findCourseById(courseId); + public async findById(courseId: EntityId): Promise { + const courses: CourseDO = await this.courseRepo.findCourseById(courseId); return courses; } - public async saveAll(courses: Course[]): Promise { - const savedCourses: Course[] = await this.courseRepo.saveAll(courses); + public async saveAll(courses: CourseDO[]): Promise { + const savedCourses: CourseDO[] = await this.courseRepo.saveAll(courses); return savedCourses; } - public async findBySyncedGroup(group: Group): Promise { - const courses: Course[] = await this.courseRepo.findBySyncedGroup(group); + public async findBySyncedGroup(group: Group): Promise { + const courses: CourseDO[] = await this.courseRepo.findBySyncedGroup(group); return courses; } - public async stopSynchronization(course: Course): Promise { + public async stopSynchronization(course: CourseDO): Promise { if (!course.syncedWithGroup) { throw new CourseNotSynchronizedLoggableException(course.id); } @@ -36,14 +40,18 @@ export class CourseDoService implements AuthorizationLoaderServiceGeneric { + public async startSynchronization(course: CourseDO, group: Group): Promise { + if (course.syncedWithGroup) { + throw new CourseAlreadySynchronizedLoggableException(course.id); + } + course.syncedWithGroup = group.id; await this.courseRepo.save(course); } - public async findCoursesBySchool(schoolId: EntityId): Promise { - const courses: Course[] = await this.courseRepo.findBySchoolId(schoolId); + public async findCourses(filter: CourseFilter, options?: IFindOptions): Promise> { + const courses = await this.courseRepo.findCourses(filter, options); return courses; } diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index 565d2fa0f69..0165323899c 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -5,16 +5,16 @@ import { RoleService } from '@modules/role'; import { School, SchoolService } from '@modules/school'; import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; -import { SortHelper } from '@shared/common'; import { PaginationParams } from '@shared/controller/'; import { Page, UserDO } from '@shared/domain/domainobject'; import { Course as CourseEntity, User } from '@shared/domain/entity'; -import { Pagination, Permission, SortOrder } from '@shared/domain/interface'; +import { IFindOptions, Pagination, Permission, SortOrder } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; -import { CourseSortQueryType } from '../controller/dto/interface/course-sort-query-type.enum'; -import { CourseStatusQueryType } from '../controller/dto/interface/course-status-query-type.enum'; -import { Course } from '../domain'; +import { Course as CourseDO } from '../domain'; +import { CourseFilter } from '../domain/interface/course-filter'; +import { CourseSortQueryType } from '../domain/interface/course-sort-query-type.enum'; +import { CourseStatusQueryType } from '../domain/interface/course-status-query-type.enum'; import { RoleNameMapper } from '../mapper/rolename.mapper'; import { CourseDoService, CourseService } from '../service'; import { CourseInfoDto } from './dto'; @@ -52,69 +52,46 @@ export class CourseUc { sortBy: CourseSortQueryType = CourseSortQueryType.NAME, courseStatusQueryType?: CourseStatusQueryType, pagination?: Pagination, - sortOrder?: SortOrder + sortOrder: SortOrder = SortOrder.asc ): Promise> { const school: School = await this.schoolService.getSchoolById(schoolId); const user: User = await this.authService.getUserWithPermissions(userId); this.authService.checkPermission(user, school, AuthorizationContextBuilder.read([Permission.ADMIN_VIEW])); - const courses: Course[] = await this.courseDoService.findCoursesBySchool(schoolId); + const order = { [sortBy]: sortOrder }; + const filter: CourseFilter = { schoolId, courseStatusQueryType }; + const options: IFindOptions = { pagination, order }; + const courses: Page = await this.courseDoService.findCourses(filter, options); - const courseInfosFromCourses: CourseInfoDto[] = await this.getCourseInfosFromCourses( - courses, - courseStatusQueryType - ); - - courseInfosFromCourses.sort((a: CourseInfoDto, b: CourseInfoDto): number => - SortHelper.genericSortFunction(a[sortBy], b[sortBy], sortOrder) - ); + const resolvedCourses: CourseInfoDto[] = await this.getCourseData(courses.data); - const pageContent: CourseInfoDto[] = this.applyPagination( - courseInfosFromCourses, - pagination?.skip, - pagination?.limit - ); - - const page: Page = new Page(pageContent, courseInfosFromCourses.length); + const page: Page = new Page(resolvedCourses, courses.total); return page; } - private async getCourseInfosFromCourses( - courses: Course[], - courseStatusQueryType: CourseStatusQueryType | undefined - ): Promise { - const now = new Date(); - let untilDate; - let allCourses: Course[]; - if (!courseStatusQueryType || courseStatusQueryType === CourseStatusQueryType.CURRENT) { - allCourses = courses.filter((course: Course) => { - untilDate = course.untilDate; - return now < untilDate || untilDate === undefined; - }); - } else { - allCourses = courses.filter((course) => { - untilDate = course.untilDate; - return now > untilDate && untilDate !== undefined; - }); - } - - const resolvedCourses = await this.getCourseData(allCourses); - - return resolvedCourses; - } + private async getCourseData(courses: CourseDO[]): Promise { + const courseInfos: CourseInfoDto[] = await Promise.all( + courses.map(async (course) => { + const groupName = course.syncedWithGroup ? await this.getSyncedGroupName(course.syncedWithGroup) : undefined; + const teachers: string[] = await this.getCourseTeachers(course.teachers); + const classes: string[] = await this.getCourseClasses(course.classes); + const groups: string[] = await this.getCourseGroups(course.groups); - private applyPagination(courseInfo: CourseInfoDto[], skip = 0, limit?: number): CourseInfoDto[] { - let page: CourseInfoDto[]; + const mapped = new CourseInfoDto({ + id: course.id, + name: course.name, + classes: [...classes, ...groups], + teachers, + syncedWithGroup: groupName, + }); - if (limit === -1) { - page = courseInfo.slice(skip); - } else { - page = courseInfo.slice(skip, limit ? skip + limit : courseInfo.length); - } + return mapped; + }) + ); - return page; + return courseInfos; } private async getSyncedGroupName(groupId: EntityId): Promise { @@ -156,27 +133,4 @@ export class CourseUc { ); return groups; } - - private async getCourseData(courses: Course[]) { - const courseInfos: CourseInfoDto[] = await Promise.all( - courses.map(async (course) => { - const groupName = course.syncedWithGroup ? await this.getSyncedGroupName(course.syncedWithGroup) : undefined; - const teachers: string[] = await this.getCourseTeachers(course.teachers); - const classes: string[] = await this.getCourseClasses(course.classes); - const groups: string[] = await this.getCourseGroups(course.groups); - - const mapped = new CourseInfoDto({ - id: course.id, - name: course.name, - classes: [...classes, ...groups], - teachers, - syncedWithGroup: groupName, - }); - - return mapped; - }) - ); - - return courseInfos; - } } diff --git a/apps/server/src/shared/repo/course/course.repo.ts b/apps/server/src/shared/repo/course/course.repo.ts index fb52b8c115e..0cd3492d9c3 100644 --- a/apps/server/src/shared/repo/course/course.repo.ts +++ b/apps/server/src/shared/repo/course/course.repo.ts @@ -7,7 +7,7 @@ import { Counted, EntityId } from '@shared/domain/types'; import { BaseRepo } from '../base.repo'; import { Scope } from '../scope'; -class CourseScope extends Scope { +export class CourseScope extends Scope { forAllGroupTypes(userId: EntityId): CourseScope { const isStudent = { students: userId }; const isTeacher = { teachers: userId }; @@ -50,6 +50,23 @@ class CourseScope extends Scope { this.addQuery({ id: courseId }); return this; } + + bySchoolId(schoolId: EntityId | undefined): CourseScope { + if (schoolId) { + this.addQuery({ school: schoolId }); + } + return this; + } + + forArchivedCourses(): CourseScope { + const now = new Date(); + const untilDateExists = { untilDate: { $exists: true } } as FilterQuery; + const untilDateInPast = { untilDate: { $lt: now } }; + + this.addQuery({ $and: [untilDateExists, untilDateInPast] }); + + return this; + } } @Injectable() From 09237e202311d69f4f28e40d32f863f6f6caefe4 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Wed, 21 Aug 2024 18:43:41 +0200 Subject: [PATCH 26/64] update seed data roles --- backup/setup/roles.json | 1 + 1 file changed, 1 insertion(+) diff --git a/backup/setup/roles.json b/backup/setup/roles.json index 03922f4ff3b..8de703b913b 100644 --- a/backup/setup/roles.json +++ b/backup/setup/roles.json @@ -90,6 +90,7 @@ "COURSE_CREATE", "COURSE_EDIT", "COURSE_REMOVE", + "COURSE_ADMINISTRATION", "DATASOURCES_CREATE", "DATASOURCES_DELETE", "DATASOURCES_EDIT", From 91099058b3a8761a1a7b6bf56ee6731b069e88ef Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Thu, 22 Aug 2024 11:59:25 +0200 Subject: [PATCH 27/64] update pagination handling for courses --- .../interface/course-sort-query-type.enum.ts | 3 --- .../domain/interface/course.repo.interface.ts | 3 +-- .../learnroom/repo/mikro-orm/course.repo.ts | 8 +++---- .../learnroom/service/course-do.service.ts | 23 +++++++++---------- .../src/modules/learnroom/uc/course.uc.ts | 12 +++++----- 5 files changed, 21 insertions(+), 28 deletions(-) diff --git a/apps/server/src/modules/learnroom/domain/interface/course-sort-query-type.enum.ts b/apps/server/src/modules/learnroom/domain/interface/course-sort-query-type.enum.ts index a70b0cd35b4..2b7b20bc958 100644 --- a/apps/server/src/modules/learnroom/domain/interface/course-sort-query-type.enum.ts +++ b/apps/server/src/modules/learnroom/domain/interface/course-sort-query-type.enum.ts @@ -1,6 +1,3 @@ export enum CourseSortQueryType { NAME = 'name', - // SYNCHRONIZED_GROUP = 'syncedWithGroup', - // TEACHER_NAMES = 'teacherNames', - // CLASS_NAMES = 'classNames', } diff --git a/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts b/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts index 741d917badc..f9fc6a74020 100644 --- a/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts +++ b/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts @@ -1,5 +1,4 @@ import type { Group } from '@modules/group'; -import { Page } from '@shared//domain/domainobject'; import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { BaseDomainObjectRepoInterface } from '@shared/repo/base-domain-object.repo.interface'; @@ -11,7 +10,7 @@ export interface CourseRepo extends BaseDomainObjectRepoInterface { findBySyncedGroup(group: Group): Promise; - findCourses(filter: CourseFilter, options?: IFindOptions): Promise>; + findCourses(filter: CourseFilter, options?: IFindOptions): Promise; } export const COURSE_REPO = Symbol('COURSE_REPO'); diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts index dc255a1153d..03a40d3c15d 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts @@ -50,7 +50,7 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo): Promise> { + public async findCourses(filter: CourseFilter, options?: IFindOptions): Promise { const scope: CourseScope = new CourseScope(); scope.bySchoolId(filter.schoolId); if (filter.courseStatusQueryType === CourseStatusQueryType.CURRENT) { @@ -61,7 +61,7 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo => { @@ -73,9 +73,7 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo CourseEntityMapper.mapEntityToDo(entity)); - const page: Page = new Page(courses, total); - - return page; + return courses; } private mapToMikroOrmOptions

(options?: IFindOptions): FindOptions { diff --git a/apps/server/src/modules/learnroom/service/course-do.service.ts b/apps/server/src/modules/learnroom/service/course-do.service.ts index 09844b2b011..0841bff863c 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.ts @@ -1,36 +1,35 @@ import { AuthorizationLoaderServiceGeneric } from '@modules/authorization'; import { type Group } from '@modules/group'; import { Inject, Injectable } from '@nestjs/common'; -import { Page } from '@shared/domain/domainobject/page'; import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { type Course as CourseDO, COURSE_REPO, CourseNotSynchronizedLoggableException, CourseRepo } from '../domain'; +import { type Course, COURSE_REPO, CourseNotSynchronizedLoggableException, CourseRepo } from '../domain'; import { CourseAlreadySynchronizedLoggableException } from '../domain/error/course-already-synchronized.loggable-exception'; import { CourseFilter } from '../domain/interface/course-filter'; @Injectable() -export class CourseDoService implements AuthorizationLoaderServiceGeneric { +export class CourseDoService implements AuthorizationLoaderServiceGeneric { constructor(@Inject(COURSE_REPO) private readonly courseRepo: CourseRepo) {} - public async findById(courseId: EntityId): Promise { - const courses: CourseDO = await this.courseRepo.findCourseById(courseId); + public async findById(courseId: EntityId): Promise { + const courses: Course = await this.courseRepo.findCourseById(courseId); return courses; } - public async saveAll(courses: CourseDO[]): Promise { - const savedCourses: CourseDO[] = await this.courseRepo.saveAll(courses); + public async saveAll(courses: Course[]): Promise { + const savedCourses: Course[] = await this.courseRepo.saveAll(courses); return savedCourses; } - public async findBySyncedGroup(group: Group): Promise { - const courses: CourseDO[] = await this.courseRepo.findBySyncedGroup(group); + public async findBySyncedGroup(group: Group): Promise { + const courses: Course[] = await this.courseRepo.findBySyncedGroup(group); return courses; } - public async stopSynchronization(course: CourseDO): Promise { + public async stopSynchronization(course: Course): Promise { if (!course.syncedWithGroup) { throw new CourseNotSynchronizedLoggableException(course.id); } @@ -40,7 +39,7 @@ export class CourseDoService implements AuthorizationLoaderServiceGeneric { + public async startSynchronization(course: Course, group: Group): Promise { if (course.syncedWithGroup) { throw new CourseAlreadySynchronizedLoggableException(course.id); } @@ -50,7 +49,7 @@ export class CourseDoService implements AuthorizationLoaderServiceGeneric): Promise> { + public async findCourses(filter: CourseFilter, options?: IFindOptions): Promise { const courses = await this.courseRepo.findCourses(filter, options); return courses; diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index 95ad3f5d226..ed79969908e 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -8,7 +8,7 @@ import { Injectable } from '@nestjs/common'; import { PaginationParams } from '@shared/controller/'; import { Page, UserDO } from '@shared/domain/domainobject'; import { Course as CourseEntity, User } from '@shared/domain/entity'; -import { IFindOptions, Pagination, Permission, SortOrder } from '@shared/domain/interface'; +import { IFindOptions, Pagination, Permission, SortOrder, SortOrderMap } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; import { Course as CourseDO } from '../domain'; @@ -53,7 +53,7 @@ export class CourseUc { public async findAllCourses( userId: EntityId, schoolId: EntityId, - sortBy: CourseSortQueryType = CourseSortQueryType.NAME, + sortByField: CourseSortQueryType = CourseSortQueryType.NAME, courseStatusQueryType?: CourseStatusQueryType, pagination?: Pagination, sortOrder: SortOrder = SortOrder.asc @@ -63,14 +63,14 @@ export class CourseUc { const user: User = await this.authService.getUserWithPermissions(userId); this.authService.checkPermission(user, school, AuthorizationContextBuilder.read([Permission.ADMIN_VIEW])); - const order = { [sortBy]: sortOrder }; + const order: SortOrderMap = { [sortByField]: sortOrder }; const filter: CourseFilter = { schoolId, courseStatusQueryType }; const options: IFindOptions = { pagination, order }; - const courses: Page = await this.courseDoService.findCourses(filter, options); + const courses: CourseDO[] = await this.courseDoService.findCourses(filter, options); - const resolvedCourses: CourseInfoDto[] = await this.getCourseData(courses.data); + const resolvedCourses: CourseInfoDto[] = await this.getCourseData(courses); - const page: Page = new Page(resolvedCourses, courses.total); + const page: Page = new Page(resolvedCourses, courses.length); return page; } From 63ef2051cf203dc9414a7e040cc8d8b2e6ea6ccd Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Thu, 22 Aug 2024 18:43:14 +0200 Subject: [PATCH 28/64] update course do service --- .../src/modules/learnroom/domain/index.ts | 5 +- .../learnroom/domain/interface/index.ts | 5 +- .../mapper/course-info-response.mapper.ts | 1 - .../learnroom/repo/mikro-orm/course.repo.ts | 5 +- .../service/course-do.service.spec.ts | 38 +++++++++++++ .../learnroom/service/course-do.service.ts | 11 ++-- .../modules/learnroom/uc/course.uc.spec.ts | 54 ++++++++++++++++++- .../src/modules/learnroom/uc/course.uc.ts | 4 +- .../learnroom/uc/dto/course-info.dto.ts | 3 -- 9 files changed, 107 insertions(+), 19 deletions(-) diff --git a/apps/server/src/modules/learnroom/domain/index.ts b/apps/server/src/modules/learnroom/domain/index.ts index 65edf954687..20e309382f1 100644 --- a/apps/server/src/modules/learnroom/domain/index.ts +++ b/apps/server/src/modules/learnroom/domain/index.ts @@ -1,5 +1,4 @@ export { Course, CourseProps } from './do'; -export { CourseRepo, COURSE_REPO } from './interface'; -export { CourseNotSynchronizedLoggableException } from './error'; -export { CourseAlreadySynchronizedLoggableException } from './error'; +export { CourseAlreadySynchronizedLoggableException, CourseNotSynchronizedLoggableException } from './error'; +export { COURSE_REPO, CourseFilter, CourseRepo, CourseSortQueryType, CourseStatusQueryType } from './interface'; export { CourseSynchronizationStoppedLoggable } from './loggable'; diff --git a/apps/server/src/modules/learnroom/domain/interface/index.ts b/apps/server/src/modules/learnroom/domain/interface/index.ts index 6c0fd29b1f0..67000ae6c12 100644 --- a/apps/server/src/modules/learnroom/domain/interface/index.ts +++ b/apps/server/src/modules/learnroom/domain/interface/index.ts @@ -1 +1,4 @@ -export { CourseRepo, COURSE_REPO } from './course.repo.interface'; +export { CourseFilter } from './course-filter'; +export { CourseSortQueryType } from './course-sort-query-type.enum'; +export { CourseStatusQueryType } from './course-status-query-type.enum'; +export { COURSE_REPO, CourseRepo } from './course.repo.interface'; diff --git a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts index 56f6636b05b..3edc4ab7a96 100644 --- a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts @@ -28,7 +28,6 @@ export class CourseInfoResponseMapper { name: courseInfo.name, classNames: courseInfo.classes, teacherNames: courseInfo.teachers, - courseStatus: courseInfo.courseStatus, syncedGroup: courseInfo.syncedWithGroup ? courseInfo.syncedWithGroup : undefined, }); diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts index 03a40d3c15d..5c100b3bd7c 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts @@ -1,15 +1,12 @@ import { EntityData, EntityName, FindOptions } from '@mikro-orm/core'; import { Group } from '@modules/group'; -import { Page } from '@shared/domain/domainobject/page'; import { Course as CourseEntity } from '@shared/domain/entity'; import { IFindOptions, SortOrder } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { BaseDomainObjectRepo } from '@shared/repo/base-domain-object.repo'; import { CourseScope } from '@shared/repo/course/course.repo'; -import { Course, CourseRepo } from '../../domain'; +import { Course, CourseFilter, CourseRepo, CourseStatusQueryType } from '../../domain'; import { CourseEntityMapper } from './mapper/course.entity.mapper'; -import { CourseFilter } from '../../domain/interface/course-filter'; -import { CourseStatusQueryType } from '../../domain/interface/course-status-query-type.enum'; export class CourseMikroOrmRepo extends BaseDomainObjectRepo implements CourseRepo { protected get entityName(): EntityName { diff --git a/apps/server/src/modules/learnroom/service/course-do.service.spec.ts b/apps/server/src/modules/learnroom/service/course-do.service.spec.ts index 98c6b2eb47e..3a9a0f97f0a 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.spec.ts @@ -3,11 +3,14 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Group } from '@modules/group'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; +import { IFindOptions, SortOrder } from '@shared/domain/interface'; +import { EntityId } from '@shared/domain/types'; import { groupFactory } from '@shared/testing'; import { Course, COURSE_REPO, CourseAlreadySynchronizedLoggableException, + CourseFilter, CourseNotSynchronizedLoggableException, CourseRepo, } from '../domain'; @@ -221,4 +224,39 @@ describe(CourseDoService.name, () => { }); }); }); + + describe('findCourses', () => { + describe('when course are found', () => { + const setup = () => { + const courses: Course[] = courseFactory.buildList(5); + const schoolId: EntityId = new ObjectId().toHexString(); + const filter: CourseFilter = { schoolId }; + const options: IFindOptions = { + order: { + name: SortOrder.asc, + }, + pagination: { + limit: 2, + skip: 1, + }, + }; + + courseRepo.findCourses.mockResolvedValueOnce(courses); + + return { + courses, + schoolId, + filter, + options, + }; + }; + + it('should return the courses by passing filter and options', async () => { + const { courses, filter, options } = setup(); + const result: Course[] = await service.findCourses(filter, options); + + expect(result).toEqual(courses); + }); + }); + }); }); diff --git a/apps/server/src/modules/learnroom/service/course-do.service.ts b/apps/server/src/modules/learnroom/service/course-do.service.ts index 0841bff863c..46c8ed2e476 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.ts @@ -3,9 +3,14 @@ import { type Group } from '@modules/group'; import { Inject, Injectable } from '@nestjs/common'; import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { type Course, COURSE_REPO, CourseNotSynchronizedLoggableException, CourseRepo } from '../domain'; -import { CourseAlreadySynchronizedLoggableException } from '../domain/error/course-already-synchronized.loggable-exception'; -import { CourseFilter } from '../domain/interface/course-filter'; +import { + type Course, + COURSE_REPO, + CourseAlreadySynchronizedLoggableException, + CourseFilter, + CourseNotSynchronizedLoggableException, + CourseRepo, +} from '../domain'; @Injectable() export class CourseDoService implements AuthorizationLoaderServiceGeneric { diff --git a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts index 17dbd918dc7..aee0b9c6bc7 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts @@ -1,20 +1,37 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AuthorizationService } from '@modules/authorization'; +import { ClassService } from '@modules/class'; +import { GroupService } from '@modules/group'; import { RoleDto, RoleService } from '@modules/role'; +import { SchoolService } from '@modules/school'; +import { SCHOOL_REPO, SchoolRepo } from '@modules/school/domain/interface'; +import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission, RoleName, SortOrder } from '@shared/domain/interface'; import { CourseRepo } from '@shared/repo'; import { courseFactory, setupEntities, UserAndAccountTestFactory } from '@shared/testing'; -import { CourseService } from '../service'; +import { Course as CourseDO, COURSE_REPO, CourseRepo as CourseDORepo } from '../domain'; +import { CourseDoService, CourseService } from '../service'; import { CourseUc } from './course.uc'; +import { CourseEntityMapper } from '../repo/mikro-orm/mapper/course.entity.mapper'; +import { schoolFactory } from '../../school/testing'; +import { courseFactory as courseDoFactory } from '../testing'; describe('CourseUc', () => { let module: TestingModule; let uc: CourseUc; let courseRepo: DeepMocked; + let courseDORepo: DeepMocked; + let schoolRepo: DeepMocked; + let courseService: DeepMocked; let authorizationService: DeepMocked; let roleService: DeepMocked; + let schoolService: DeepMocked; + let courseDoService: DeepMocked; + let groupService: DeepMocked; + let userService: DeepMocked; + let classService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -37,6 +54,34 @@ describe('CourseUc', () => { provide: RoleService, useValue: createMock(), }, + { + provide: SchoolService, + useValue: createMock(), + }, + { + provide: 'SCHOOL_REPO', + useValue: createMock(), + }, + { + provide: CourseDoService, + useValue: createMock(), + }, + { + provide: COURSE_REPO, + useValue: createMock(), + }, + { + provide: GroupService, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + { + provide: ClassService, + useValue: createMock(), + }, ], }).compile(); @@ -45,6 +90,13 @@ describe('CourseUc', () => { courseService = module.get(CourseService); authorizationService = module.get(AuthorizationService); roleService = module.get(RoleService); + schoolService = module.get(SchoolService); + courseDoService = module.get(CourseDoService); + groupService = module.get(GroupService); + userService = module.get(UserService); + classService = module.get(ClassService); + courseDORepo = module.get(COURSE_REPO); + schoolRepo = module.get(SCHOOL_REPO); }); afterAll(async () => { diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index ed79969908e..0c875f23cfa 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -12,9 +12,7 @@ import { IFindOptions, Pagination, Permission, SortOrder, SortOrderMap } from '@ import { Counted, EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; import { Course as CourseDO } from '../domain'; -import { CourseFilter } from '../domain/interface/course-filter'; -import { CourseSortQueryType } from '../domain/interface/course-sort-query-type.enum'; -import { CourseStatusQueryType } from '../domain/interface/course-status-query-type.enum'; +import { CourseFilter, CourseSortQueryType, CourseStatusQueryType } from '../domain/interface'; import { RoleNameMapper } from '../mapper/rolename.mapper'; import { CourseDoService, CourseService } from '../service'; import { CourseInfoDto } from './dto'; diff --git a/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts b/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts index 20cc9189494..42482e2dd39 100644 --- a/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts +++ b/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts @@ -9,8 +9,6 @@ export class CourseInfoDto { classes: string[]; - courseStatus?: string; - syncedWithGroup?: string; constructor(props: CourseInfoDto) { @@ -18,7 +16,6 @@ export class CourseInfoDto { this.name = props.name; this.classes = props.classes; this.teachers = props.teachers; - this.courseStatus = props.courseStatus; this.syncedWithGroup = props.syncedWithGroup; } } From 6f12f968000045de114d8ef6aed4403034cc517e Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Fri, 23 Aug 2024 09:18:20 +0200 Subject: [PATCH 29/64] update classes repo spec --- .../modules/class/repo/classes.repo.spec.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/server/src/modules/class/repo/classes.repo.spec.ts b/apps/server/src/modules/class/repo/classes.repo.spec.ts index 9904a627e6c..79b675da3d3 100644 --- a/apps/server/src/modules/class/repo/classes.repo.spec.ts +++ b/apps/server/src/modules/class/repo/classes.repo.spec.ts @@ -177,4 +177,35 @@ describe(ClassesRepo.name, () => { }); }); }); + + describe('findClassById', () => { + describe('when class is not found in classes', () => { + it('should return null', async () => { + const result = await repo.findClassById(new ObjectId().toHexString()); + + expect(result).toEqual(null); + }); + }); + + describe('when class is in classes', () => { + const setup = async () => { + const class1: ClassEntity = classEntityFactory.buildWithId(); + console.log(class1.id); + await em.persistAndFlush([class1]); + em.clear(); + + return { + class1, + }; + }; + + it('should find class with particular classId', async () => { + const { class1 } = await setup(); + + const result = await repo.findClassById(class1.id); + + expect(result?.id).toEqual(class1.id); + }); + }); + }); }); From 15ae3a55cba64fa3d300deafde71fad460d889bc Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Fri, 23 Aug 2024 11:56:47 +0200 Subject: [PATCH 30/64] N21-2075 Calendar flag for Nuxt --- apps/server/src/modules/server/api/dto/config.response.ts | 4 ++++ apps/server/src/modules/server/api/test/server.api.spec.ts | 1 + apps/server/src/modules/server/server.config.ts | 2 ++ config/default.schema.json | 5 +++++ 4 files changed, 12 insertions(+) diff --git a/apps/server/src/modules/server/api/dto/config.response.ts b/apps/server/src/modules/server/api/dto/config.response.ts index faf17ffa582..4c3d05bbf4e 100644 --- a/apps/server/src/modules/server/api/dto/config.response.ts +++ b/apps/server/src/modules/server/api/dto/config.response.ts @@ -137,6 +137,9 @@ export class ConfigResponse { @ApiProperty() FEATURE_USER_MIGRATION_ENABLED: boolean; + @ApiProperty() + CALENDAR_SERVICE_ENABLED: boolean; + @ApiProperty() FEATURE_COPY_SERVICE_ENABLED: boolean; @@ -225,6 +228,7 @@ export class ConfigResponse { this.ACCESSIBILITY_REPORT_EMAIL = config.ACCESSIBILITY_REPORT_EMAIL; this.ADMIN_TABLES_DISPLAY_CONSENT_COLUMN = config.ADMIN_TABLES_DISPLAY_CONSENT_COLUMN; this.ALERT_STATUS_URL = config.ALERT_STATUS_URL; + this.CALENDAR_SERVICE_ENABLED = config.CALENDAR_SERVICE_ENABLED; this.FEATURE_ES_COLLECTIONS_ENABLED = config.FEATURE_ES_COLLECTIONS_ENABLED; this.FEATURE_EXTENSIONS_ENABLED = config.FEATURE_EXTENSIONS_ENABLED; this.FEATURE_TEAMS_ENABLED = config.FEATURE_TEAMS_ENABLED; diff --git a/apps/server/src/modules/server/api/test/server.api.spec.ts b/apps/server/src/modules/server/api/test/server.api.spec.ts index bd83c1b5594..d81bc735207 100644 --- a/apps/server/src/modules/server/api/test/server.api.spec.ts +++ b/apps/server/src/modules/server/api/test/server.api.spec.ts @@ -38,6 +38,7 @@ describe('Server Controller (API)', () => { 'ADMIN_TABLES_DISPLAY_CONSENT_COLUMN', 'ALERT_STATUS_URL', 'CTL_TOOLS_RELOAD_TIME_MS', + 'CALENDAR_SERVICE_ENABLED', 'DOCUMENT_BASE_DIR', 'FEATURE_ADMIN_TOGGLE_STUDENT_LERNSTORE_VIEW_ENABLED', 'FEATURE_ALLOW_INSECURE_LDAP_URL_ENABLED', diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index 68619562fc3..b0301ad6cfd 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -71,6 +71,7 @@ export interface ServerConfig ACCESSIBILITY_REPORT_EMAIL: string; ADMIN_TABLES_DISPLAY_CONSENT_COLUMN: boolean; ALERT_STATUS_URL: string | null; + CALENDAR_SERVICE_ENABLED: boolean; FEATURE_ES_COLLECTIONS_ENABLED: boolean; FEATURE_EXTENSIONS_ENABLED: boolean; FEATURE_TEAMS_ENABLED: boolean; @@ -131,6 +132,7 @@ const config: ServerConfig = { Configuration.get('ALERT_STATUS_URL') === null ? (Configuration.get('ALERT_STATUS_URL') as null) : (Configuration.get('ALERT_STATUS_URL') as string), + CALENDAR_SERVICE_ENABLED: Configuration.get('CALENDAR_SERVICE_ENABLED') as boolean, FEATURE_ES_COLLECTIONS_ENABLED: Configuration.get('FEATURE_ES_COLLECTIONS_ENABLED') as boolean, FEATURE_EXTENSIONS_ENABLED: Configuration.get('FEATURE_EXTENSIONS_ENABLED') as boolean, FEATURE_TEAMS_ENABLED: Configuration.get('FEATURE_TEAMS_ENABLED') as boolean, diff --git a/config/default.schema.json b/config/default.schema.json index 23ac00589db..bd8e93cd7f6 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1665,6 +1665,11 @@ "type": "boolean", "default": false, "description": "Enables the AI Tutor" + }, + "CALENDAR_SERVICE_ENABLED": { + "type": "boolean", + "default": false, + "description": "Enables calender service" } }, "required": [] From 345e19ac9a0cc7ed37906e18164f8c28c5c4401c Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Fri, 23 Aug 2024 12:14:35 +0200 Subject: [PATCH 31/64] update courses info response and api tests --- .../controller/api-test/course.api.spec.ts | 71 +++++++++++++++++++ .../dto/response/course-info.response.ts | 4 -- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts index b2a4454207b..71f2f8b7d14 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts @@ -9,12 +9,15 @@ import { cleanupCollections, courseFactory, groupEntityFactory, + schoolEntityFactory, TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; import { readFile } from 'node:fs/promises'; import { CourseMetadataListResponse } from '../dto'; import { CourseCommonCartridgeMetadataResponse } from '../dto/course-cc-metadata.response'; +import { CourseInfoListResponse } from '../dto/response'; +import { CourseSortQueryType, CourseStatusQueryType } from '../../domain'; const createStudent = () => { const { studentUser, studentAccount } = UserAndAccountTestFactory.buildStudent({}, [Permission.COURSE_VIEW]); @@ -28,6 +31,11 @@ const createTeacher = () => { return { account: teacherAccount, user: teacherUser }; }; +const createAdmin = () => { + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({}, [Permission.COURSE_ADMINISTRATION]); + return { account: adminAccount, user: adminUser }; +}; + describe('Course Controller (API)', () => { let app: INestApplication; let em: EntityManager; @@ -352,6 +360,7 @@ describe('Course Controller (API)', () => { }); }); }); + describe('[GET] /courses/:courseId/cc-metadata', () => { const setup = async () => { const teacher = createTeacher(); @@ -377,4 +386,66 @@ describe('Course Controller (API)', () => { expect(data.id).toBe(course.id); }); }); + + describe('[GET] /courses/all', () => { + describe('when classes are found', () => { + const setup = async () => { + const student = createStudent(); + const teacher = createTeacher(); + const admin = createAdmin(); + const school = schoolEntityFactory.buildWithId({}); + + const courses: CourseEntity[] = courseFactory.buildList(5, { + school, + }); + const query = { skip: 0, limit: 5, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; + + admin.user.school = school; + await em.persistAndFlush(school); + await em.persistAndFlush(courses); + await em.persistAndFlush([admin.account, admin.user]); + em.clear(); + + return { + student, + courses, + teacher, + admin, + query, + school, + }; + }; + + it('should return the correct response structure', async () => { + const { admin, query } = await setup(); + + const loggedInClient = await testApiClient.login(admin.account); + const response = await loggedInClient.get('/all').query(query); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty('data'); + expect(response.body).toHaveProperty('skip'); + expect(response.body).toHaveProperty('limit'); + expect(response.body).toHaveProperty('total'); + }); + + it('should find all courses as an admin by passing all query parameters', async () => { + const { courses, admin, school, query } = await setup(); + admin.user.school = school; + await em.persistAndFlush(courses); + await em.persistAndFlush([admin.account, admin.user, school]); + em.clear(); + + const loggedInClient = await testApiClient.login(admin.account); + const response = await loggedInClient.get('/all').query(query); + + const { total, skip, limit, data } = response.body as CourseInfoListResponse; + expect(response.statusCode).toBe(200); + expect(skip).toBe(0); + expect(limit).toBe(5); + expect(total).toBe(5); + expect(data[0].id).toBe(courses[0].id); + }); + }); + }); }); diff --git a/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts b/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts index 5eb6ec4945e..fdeea47c7c6 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts @@ -14,9 +14,6 @@ export class CourseInfoResponse { @ApiProperty() classNames: string[]; - @ApiPropertyOptional() - courseStatus?: string; - @ApiPropertyOptional() syncedGroup?: string; @@ -25,7 +22,6 @@ export class CourseInfoResponse { this.name = props.name; this.classNames = props.classNames; this.teacherNames = props.teacherNames; - this.courseStatus = props.courseStatus; this.syncedGroup = props.syncedGroup; } } From f1f7ff717a89e80d5339bc4a5ed80983b8ec183b Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Fri, 23 Aug 2024 15:30:00 +0200 Subject: [PATCH 32/64] update course api test --- .../controller/api-test/course.api.spec.ts | 92 +++++++++++++++---- 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts index 71f2f8b7d14..1be341633e2 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts @@ -388,36 +388,40 @@ describe('Course Controller (API)', () => { }); describe('[GET] /courses/all', () => { - describe('when classes are found', () => { + describe('when logged in as admin', () => { const setup = async () => { const student = createStudent(); const teacher = createTeacher(); const admin = createAdmin(); const school = schoolEntityFactory.buildWithId({}); - const courses: CourseEntity[] = courseFactory.buildList(5, { + const currentCourses: CourseEntity[] = courseFactory.buildList(5, { school, }); - const query = { skip: 0, limit: 5, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; + const archivedCourses: CourseEntity[] = courseFactory.buildList(10, { + school, + untilDate: new Date('2024-07-31T23:59:59'), + }); admin.user.school = school; await em.persistAndFlush(school); - await em.persistAndFlush(courses); + await em.persistAndFlush([...currentCourses, ...archivedCourses]); await em.persistAndFlush([admin.account, admin.user]); em.clear(); return { student, - courses, + currentCourses, + archivedCourses, teacher, admin, - query, school, }; }; it('should return the correct response structure', async () => { - const { admin, query } = await setup(); + const { admin } = await setup(); + const query = {}; const loggedInClient = await testApiClient.login(admin.account); const response = await loggedInClient.get('/all').query(query); @@ -429,12 +433,9 @@ describe('Course Controller (API)', () => { expect(response.body).toHaveProperty('total'); }); - it('should find all courses as an admin by passing all query parameters', async () => { - const { courses, admin, school, query } = await setup(); - admin.user.school = school; - await em.persistAndFlush(courses); - await em.persistAndFlush([admin.account, admin.user, school]); - em.clear(); + it('should return archived courses in pages', async () => { + const { admin } = await setup(); + const query = { skip: 0, limit: 10, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.ARCHIVE }; const loggedInClient = await testApiClient.login(admin.account); const response = await loggedInClient.get('/all').query(query); @@ -442,9 +443,68 @@ describe('Course Controller (API)', () => { const { total, skip, limit, data } = response.body as CourseInfoListResponse; expect(response.statusCode).toBe(200); expect(skip).toBe(0); - expect(limit).toBe(5); - expect(total).toBe(5); - expect(data[0].id).toBe(courses[0].id); + expect(limit).toBe(10); + expect(total).toBe(10); + expect(data.length).toBe(10); + }); + + it('should return current courses in pages', async () => { + const { admin, currentCourses } = await setup(); + const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; + + const loggedInClient = await testApiClient.login(admin.account); + const response = await loggedInClient.get('/all').query(query); + + const { total, skip, limit, data } = response.body as CourseInfoListResponse; + expect(response.statusCode).toBe(200); + expect(skip).toBe(4); + expect(limit).toBe(2); + expect(total).toBe(1); + expect(data.length).toBe(1); + expect(data[0].id).toBe(currentCourses[4].id); + }); + }); + + describe('when logged in not authenticated/authorized', () => { + const setup = async () => { + const teacher = createTeacher(); + + await em.persistAndFlush([teacher.account, teacher.user]); + em.clear(); + + return { + teacher, + }; + }; + + it('should return unauthorized', async () => { + const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; + + const response = await testApiClient.get('/all').query(query); + + expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); + expect(response.body).toEqual({ + code: HttpStatus.UNAUTHORIZED, + message: 'Unauthorized', + title: 'Unauthorized', + type: 'UNAUTHORIZED', + }); + }); + + it('should return forbidden', async () => { + const { teacher } = await setup(); + const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; + + const loggedInClient = await testApiClient.login(teacher.account); + const response = await loggedInClient.get('/all').query(query); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body).toEqual({ + code: HttpStatus.FORBIDDEN, + message: 'Forbidden', + title: 'Forbidden', + type: 'FORBIDDEN', + }); }); }); }); From 70a8a08925a663e236946927f9baf38fa9235907 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Fri, 23 Aug 2024 15:37:03 +0200 Subject: [PATCH 33/64] update server api test --- apps/server/src/modules/server/api/test/server.api.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/modules/server/api/test/server.api.spec.ts b/apps/server/src/modules/server/api/test/server.api.spec.ts index 5627d34e17e..222562455c5 100644 --- a/apps/server/src/modules/server/api/test/server.api.spec.ts +++ b/apps/server/src/modules/server/api/test/server.api.spec.ts @@ -103,6 +103,7 @@ describe('Server Controller (API)', () => { 'FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED', 'FEATURE_AI_TUTOR_ENABLED', 'FEATURE_ROOMS_ENABLED', + 'FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED', ]; expect(response.status).toEqual(HttpStatus.OK); From 3fc8d4d9756f415cddc6da41232d3bbb6a8f3ccb Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Fri, 23 Aug 2024 17:43:13 +0200 Subject: [PATCH 34/64] migration: Migration20240823151836 --- ...ation20240819123013.ts => Migration20240823151836.ts} | 6 +++--- backup/setup/migrations.json | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) rename apps/server/src/migrations/mikro-orm/{Migration20240819123013.ts => Migration20240823151836.ts} (72%) diff --git a/apps/server/src/migrations/mikro-orm/Migration20240819123013.ts b/apps/server/src/migrations/mikro-orm/Migration20240823151836.ts similarity index 72% rename from apps/server/src/migrations/mikro-orm/Migration20240819123013.ts rename to apps/server/src/migrations/mikro-orm/Migration20240823151836.ts index 1bdd05ec471..9a692086200 100644 --- a/apps/server/src/migrations/mikro-orm/Migration20240819123013.ts +++ b/apps/server/src/migrations/mikro-orm/Migration20240823151836.ts @@ -1,6 +1,6 @@ import { Migration } from '@mikro-orm/migrations-mongodb'; -export class Migration20240819123013 extends Migration { +export class Migration20240823151836 extends Migration { async up(): Promise { const adminRoleUpdate = await this.getCollection('roles').updateOne( { name: 'administrator' }, @@ -14,7 +14,7 @@ export class Migration20240819123013 extends Migration { ); if (adminRoleUpdate.modifiedCount > 0) { - console.info('Permission COURSE_ADMINISTRATION was added to role administrator.'); + console.info('Permission COURSE_ADMINISTRATION added to role administrator.'); } } @@ -31,7 +31,7 @@ export class Migration20240819123013 extends Migration { ); if (adminRoleUpdate.modifiedCount > 0) { - console.info('Rollback: Removed permission COURSE_ADMINISTRATION from role administrator.'); + console.info('Rollback: Permission COURSE_ADMINISTRATION added to role administrator.'); } } } diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index 65915f515f2..2a7feac0983 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -205,5 +205,14 @@ "created_at": { "$date": "2024-07-25T14:57:30.752Z" } + }, + { + "_id": { + "$oid": "66c8a9d1d2ae9ba6c4b43c5d" + }, + "name": "Migration20240823151836", + "created_at": { + "$date": "2024-08-23T15:25:05.360Z" + } } ] From 538e736f92b41eb49c4b8e77c6e49914ec549c18 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Mon, 26 Aug 2024 01:03:45 +0200 Subject: [PATCH 35/64] update course us test --- .../modules/learnroom/uc/course.uc.spec.ts | 125 +++++++++++++++++- 1 file changed, 120 insertions(+), 5 deletions(-) diff --git a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts index aee0b9c6bc7..f1c8fd6d497 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts @@ -1,21 +1,33 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AuthorizationService } from '@modules/authorization'; import { ClassService } from '@modules/class'; +import { classFactory } from '@modules/class/domain/testing'; import { GroupService } from '@modules/group'; +import { GroupRepo } from '@modules/group/repo/'; import { RoleDto, RoleService } from '@modules/role'; import { SchoolService } from '@modules/school'; import { SCHOOL_REPO, SchoolRepo } from '@modules/school/domain/interface'; +import { schoolFactory } from '@modules/school/testing'; import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission, RoleName, SortOrder } from '@shared/domain/interface'; import { CourseRepo } from '@shared/repo'; -import { courseFactory, setupEntities, UserAndAccountTestFactory } from '@shared/testing'; -import { Course as CourseDO, COURSE_REPO, CourseRepo as CourseDORepo } from '../domain'; +import { + courseFactory, + groupFactory, + setupEntities, + UserAndAccountTestFactory, + userDoFactory, + userFactory, +} from '@shared/testing'; + +import { ClassesRepo } from '@modules/class/repo'; +import { UserDO } from '@shared/domain/domainobject'; +import { User } from '@shared/domain/entity'; +import { COURSE_REPO, CourseRepo as CourseDORepo, CourseSortQueryType, CourseStatusQueryType } from '../domain'; import { CourseDoService, CourseService } from '../service'; -import { CourseUc } from './course.uc'; -import { CourseEntityMapper } from '../repo/mikro-orm/mapper/course.entity.mapper'; -import { schoolFactory } from '../../school/testing'; import { courseFactory as courseDoFactory } from '../testing'; +import { CourseUc } from './course.uc'; describe('CourseUc', () => { let module: TestingModule; @@ -23,6 +35,8 @@ describe('CourseUc', () => { let courseRepo: DeepMocked; let courseDORepo: DeepMocked; let schoolRepo: DeepMocked; + let groupRepo: DeepMocked; + let classesRepo: DeepMocked; let courseService: DeepMocked; let authorizationService: DeepMocked; @@ -74,6 +88,10 @@ describe('CourseUc', () => { provide: GroupService, useValue: createMock(), }, + { + provide: GroupRepo, + useValue: createMock(), + }, { provide: UserService, useValue: createMock(), @@ -82,6 +100,10 @@ describe('CourseUc', () => { provide: ClassService, useValue: createMock(), }, + { + provide: ClassesRepo, + useValue: createMock(), + }, ], }).compile(); @@ -97,6 +119,8 @@ describe('CourseUc', () => { classService = module.get(ClassService); courseDORepo = module.get(COURSE_REPO); schoolRepo = module.get(SCHOOL_REPO); + groupRepo = module.get(GroupRepo); + classesRepo = module.get(ClassesRepo); }); afterAll(async () => { @@ -171,4 +195,95 @@ describe('CourseUc', () => { expect(courseService.findById).toHaveBeenCalledWith(course.id); }); }); + + describe('findAllCourses', () => { + const setup = () => { + const user: User = userFactory.withRoleByName(RoleName.TEACHER).buildWithId(); + const teacher: UserDO = userDoFactory.build({ id: user.id, firstName: 'firstName', lastName: 'lastName' }); + const { adminUser } = UserAndAccountTestFactory.buildAdmin({}, [ + Permission.COURSE_ADMINISTRATION, + Permission.ADMIN_VIEW, + ]); + const group = groupFactory.build({ name: 'groupName' }); + const clazz = classFactory.build({ name: 'A', gradeLevel: 1 }); + + const courses = courseDoFactory.buildList(5, { + syncedWithGroup: group.id, + teacherIds: [user.id], + groupIds: [group.id], + classIds: [clazz.id], + }); + const pagination = { skip: 1, limit: 2 }; + const courseStatusQueryType: CourseStatusQueryType = CourseStatusQueryType.CURRENT; + const sortByField: CourseSortQueryType = CourseSortQueryType.NAME; + const sortOrder: SortOrder = SortOrder.asc; + + const school = schoolFactory.build(); + schoolService.getSchoolById.mockResolvedValueOnce(school); + authorizationService.getUserWithPermissions.mockResolvedValue(adminUser); + authorizationService.checkPermission.mockReturnValueOnce(undefined); + courseDORepo.findCourses.mockResolvedValueOnce(courses); + courseDoService.findCourses.mockResolvedValueOnce(courses); + groupRepo.findGroupById.mockResolvedValue(group); + groupService.findById.mockResolvedValue(group); + userService.findById.mockResolvedValue(teacher); + groupService.findById.mockResolvedValue(group); + classService.findById.mockResolvedValue(clazz); + classesRepo.findClassById.mockResolvedValue(clazz); + + return { + user, + courses, + pagination, + school, + adminUser, + group, + courseStatusQueryType, + sortByField, + sortOrder, + clazz, + }; + }; + it('should return courses of user', async () => { + const { + clazz, + group, + school, + adminUser, + sortByField, + courseStatusQueryType: statusTypeQuery, + pagination, + sortOrder, + user, + } = setup(); + + const result = await uc.findAllCourses( + adminUser.id, + school.id, + sortByField, + statusTypeQuery, + pagination, + sortOrder + ); + + expect(schoolService.getSchoolById).toHaveBeenCalledWith(school.id); + expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(adminUser.id); + expect(authorizationService.checkPermission).toHaveBeenCalled(); + const filter = { schoolId: school.id, courseStatusQueryType: statusTypeQuery }; + const options = { + pagination, + order: { + [sortByField]: sortOrder, + }, + }; + expect(courseDoService.findCourses).toHaveBeenCalledWith(filter, options); + expect(userService.findById).toHaveBeenCalledWith(user.id); + expect(classService.findById).toHaveBeenCalledWith(clazz.id); + expect(groupService.findById).toHaveBeenCalledWith(group.id); + expect(result.total).toBe(5); + expect(result.data.length).toBe(5); + expect(result.data[0].classes).toStrictEqual(['1A', 'groupName']); + expect(result.data[0].teachers).toStrictEqual(['firstName lastName']); + }); + }); }); From 2d84c978a89913edbcbe3396fea1a8a4b60a12bb Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Mon, 26 Aug 2024 01:43:21 +0200 Subject: [PATCH 36/64] update course uc tests --- .../src/modules/learnroom/uc/course-sync.uc.spec.ts | 6 +++++- apps/server/src/modules/learnroom/uc/course.uc.spec.ts | 9 +-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts index 76dff1cf9f0..516166a06a1 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts @@ -109,7 +109,9 @@ describe(CourseSyncUc.name, () => { const { user, course, group } = setup(); await uc.startSynchronization(user.id, course.id, group.id); - + + expect(courseService.findById).toHaveBeenCalledWith(course.id); + expect(groupService.findById).toHaveBeenCalledWith(group.id); expect(authorizationService.checkPermission).toHaveBeenCalledWith( user, course, @@ -121,6 +123,8 @@ describe(CourseSyncUc.name, () => { const { user, course, group } = setup(); await uc.startSynchronization(user.id, course.id, group.id); + expect(courseService.findById).toHaveBeenCalledWith(course.id); + expect(groupService.findById).toHaveBeenCalledWith(group.id); expect(courseService.startSynchronization).toHaveBeenCalledWith(course, group); }); diff --git a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts index f1c8fd6d497..9e951ed9b4f 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts @@ -6,7 +6,6 @@ import { GroupService } from '@modules/group'; import { GroupRepo } from '@modules/group/repo/'; import { RoleDto, RoleService } from '@modules/role'; import { SchoolService } from '@modules/school'; -import { SCHOOL_REPO, SchoolRepo } from '@modules/school/domain/interface'; import { schoolFactory } from '@modules/school/testing'; import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; @@ -20,7 +19,6 @@ import { userDoFactory, userFactory, } from '@shared/testing'; - import { ClassesRepo } from '@modules/class/repo'; import { UserDO } from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; @@ -34,7 +32,6 @@ describe('CourseUc', () => { let uc: CourseUc; let courseRepo: DeepMocked; let courseDORepo: DeepMocked; - let schoolRepo: DeepMocked; let groupRepo: DeepMocked; let classesRepo: DeepMocked; @@ -72,10 +69,7 @@ describe('CourseUc', () => { provide: SchoolService, useValue: createMock(), }, - { - provide: 'SCHOOL_REPO', - useValue: createMock(), - }, + { provide: CourseDoService, useValue: createMock(), @@ -118,7 +112,6 @@ describe('CourseUc', () => { userService = module.get(UserService); classService = module.get(ClassService); courseDORepo = module.get(COURSE_REPO); - schoolRepo = module.get(SCHOOL_REPO); groupRepo = module.get(GroupRepo); classesRepo = module.get(ClassesRepo); }); From ccbaff830d669cc00d770edc6bdd1dd614f45bf3 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Mon, 26 Aug 2024 01:58:19 +0200 Subject: [PATCH 37/64] update course uc tests --- .../src/modules/learnroom/uc/course-sync.uc.spec.ts | 2 +- apps/server/src/modules/learnroom/uc/course.uc.spec.ts | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts index 516166a06a1..60366ab3065 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts @@ -109,7 +109,7 @@ describe(CourseSyncUc.name, () => { const { user, course, group } = setup(); await uc.startSynchronization(user.id, course.id, group.id); - + expect(courseService.findById).toHaveBeenCalledWith(course.id); expect(groupService.findById).toHaveBeenCalledWith(group.id); expect(authorizationService.checkPermission).toHaveBeenCalledWith( diff --git a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts index 9e951ed9b4f..e1a4df31008 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AuthorizationService } from '@modules/authorization'; import { ClassService } from '@modules/class'; import { classFactory } from '@modules/class/domain/testing'; +import { ClassesRepo } from '@modules/class/repo'; import { GroupService } from '@modules/group'; import { GroupRepo } from '@modules/group/repo/'; import { RoleDto, RoleService } from '@modules/role'; @@ -9,6 +10,8 @@ import { SchoolService } from '@modules/school'; import { schoolFactory } from '@modules/school/testing'; import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; +import { Page, UserDO } from '@shared/domain/domainobject'; +import { User } from '@shared/domain/entity'; import { Permission, RoleName, SortOrder } from '@shared/domain/interface'; import { CourseRepo } from '@shared/repo'; import { @@ -19,13 +22,11 @@ import { userDoFactory, userFactory, } from '@shared/testing'; -import { ClassesRepo } from '@modules/class/repo'; -import { UserDO } from '@shared/domain/domainobject'; -import { User } from '@shared/domain/entity'; import { COURSE_REPO, CourseRepo as CourseDORepo, CourseSortQueryType, CourseStatusQueryType } from '../domain'; import { CourseDoService, CourseService } from '../service'; import { courseFactory as courseDoFactory } from '../testing'; import { CourseUc } from './course.uc'; +import { CourseInfoDto } from './dto'; describe('CourseUc', () => { let module: TestingModule; @@ -250,7 +251,7 @@ describe('CourseUc', () => { user, } = setup(); - const result = await uc.findAllCourses( + const result: Page = await uc.findAllCourses( adminUser.id, school.id, sortByField, From 4285ecef7a181240d7a4bc8a4ca13d9305461b3c Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Mon, 26 Aug 2024 01:58:19 +0200 Subject: [PATCH 38/64] update course uc tests --- .../src/modules/learnroom/uc/course-sync.uc.spec.ts | 2 +- apps/server/src/modules/learnroom/uc/course.uc.spec.ts | 9 +++++---- .../src/modules/server/api/test/server.api.spec.ts | 1 - 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts index 516166a06a1..60366ab3065 100644 --- a/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-sync.uc.spec.ts @@ -109,7 +109,7 @@ describe(CourseSyncUc.name, () => { const { user, course, group } = setup(); await uc.startSynchronization(user.id, course.id, group.id); - + expect(courseService.findById).toHaveBeenCalledWith(course.id); expect(groupService.findById).toHaveBeenCalledWith(group.id); expect(authorizationService.checkPermission).toHaveBeenCalledWith( diff --git a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts index 9e951ed9b4f..e1a4df31008 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AuthorizationService } from '@modules/authorization'; import { ClassService } from '@modules/class'; import { classFactory } from '@modules/class/domain/testing'; +import { ClassesRepo } from '@modules/class/repo'; import { GroupService } from '@modules/group'; import { GroupRepo } from '@modules/group/repo/'; import { RoleDto, RoleService } from '@modules/role'; @@ -9,6 +10,8 @@ import { SchoolService } from '@modules/school'; import { schoolFactory } from '@modules/school/testing'; import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; +import { Page, UserDO } from '@shared/domain/domainobject'; +import { User } from '@shared/domain/entity'; import { Permission, RoleName, SortOrder } from '@shared/domain/interface'; import { CourseRepo } from '@shared/repo'; import { @@ -19,13 +22,11 @@ import { userDoFactory, userFactory, } from '@shared/testing'; -import { ClassesRepo } from '@modules/class/repo'; -import { UserDO } from '@shared/domain/domainobject'; -import { User } from '@shared/domain/entity'; import { COURSE_REPO, CourseRepo as CourseDORepo, CourseSortQueryType, CourseStatusQueryType } from '../domain'; import { CourseDoService, CourseService } from '../service'; import { courseFactory as courseDoFactory } from '../testing'; import { CourseUc } from './course.uc'; +import { CourseInfoDto } from './dto'; describe('CourseUc', () => { let module: TestingModule; @@ -250,7 +251,7 @@ describe('CourseUc', () => { user, } = setup(); - const result = await uc.findAllCourses( + const result: Page = await uc.findAllCourses( adminUser.id, school.id, sortByField, diff --git a/apps/server/src/modules/server/api/test/server.api.spec.ts b/apps/server/src/modules/server/api/test/server.api.spec.ts index 222562455c5..5627d34e17e 100644 --- a/apps/server/src/modules/server/api/test/server.api.spec.ts +++ b/apps/server/src/modules/server/api/test/server.api.spec.ts @@ -103,7 +103,6 @@ describe('Server Controller (API)', () => { 'FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED', 'FEATURE_AI_TUTOR_ENABLED', 'FEATURE_ROOMS_ENABLED', - 'FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED', ]; expect(response.status).toEqual(HttpStatus.OK); From a35f453c224df31adc72d6706b32c397ed66bb4c Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Mon, 26 Aug 2024 12:48:33 +0200 Subject: [PATCH 39/64] N21-2075 refactor response --- .../controller/api-test/course.api.spec.ts | 6 +++--- .../learnroom/controller/course.controller.ts | 8 ++++---- .../dto/response/course-info-list.response.ts | 13 ------------- .../dto/response/course-list.response.ts | 13 +++++++++++++ ...urse-info.response.ts => course.response.ts} | 4 ++-- .../learnroom/controller/dto/response/index.ts | 4 ++-- .../mapper/course-info-response.mapper.ts | 17 ++++++----------- 7 files changed, 30 insertions(+), 35 deletions(-) delete mode 100644 apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts create mode 100644 apps/server/src/modules/learnroom/controller/dto/response/course-list.response.ts rename apps/server/src/modules/learnroom/controller/dto/response/{course-info.response.ts => course.response.ts} (86%) diff --git a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts index 1be341633e2..0c32269c964 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts @@ -16,7 +16,7 @@ import { import { readFile } from 'node:fs/promises'; import { CourseMetadataListResponse } from '../dto'; import { CourseCommonCartridgeMetadataResponse } from '../dto/course-cc-metadata.response'; -import { CourseInfoListResponse } from '../dto/response'; +import { CourseListResponse } from '../dto/response'; import { CourseSortQueryType, CourseStatusQueryType } from '../../domain'; const createStudent = () => { @@ -440,7 +440,7 @@ describe('Course Controller (API)', () => { const loggedInClient = await testApiClient.login(admin.account); const response = await loggedInClient.get('/all').query(query); - const { total, skip, limit, data } = response.body as CourseInfoListResponse; + const { total, skip, limit, data } = response.body as CourseListResponse; expect(response.statusCode).toBe(200); expect(skip).toBe(0); expect(limit).toBe(10); @@ -455,7 +455,7 @@ describe('Course Controller (API)', () => { const loggedInClient = await testApiClient.login(admin.account); const response = await loggedInClient.get('/all').query(query); - const { total, skip, limit, data } = response.body as CourseInfoListResponse; + const { total, skip, limit, data } = response.body as CourseListResponse; expect(response.statusCode).toBe(200); expect(skip).toBe(4); expect(limit).toBe(2); diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 15d0d482352..f18afe712fd 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -46,7 +46,7 @@ import { import { CourseCommonCartridgeMetadataResponse } from './dto/course-cc-metadata.response'; import { CourseFilterParams } from './dto/request/course-filter-params'; import { CourseSortParams } from './dto/request/course-sort-params'; -import { CourseInfoListResponse } from './dto/response'; +import { CourseListResponse } from './dto/response'; @ApiTags('Courses') @JwtAuthentication() @@ -171,7 +171,7 @@ export class CourseController { @Get('/all') @ApiOperation({ summary: 'Get a list of all courses.' }) - @ApiResponse({ status: HttpStatus.OK, type: CourseInfoListResponse }) + @ApiResponse({ status: HttpStatus.OK, type: CourseListResponse }) @ApiResponse({ status: '4XX', type: ErrorResponse }) @ApiResponse({ status: '5XX', type: ErrorResponse }) async getAllCourses( @@ -179,7 +179,7 @@ export class CourseController { @Query() pagination: PaginationParams, @Query() sortingQuery: CourseSortParams, @Query() filterParams: CourseFilterParams - ): Promise { + ): Promise { const courses: Page = await this.courseUc.findAllCourses( currentUser.userId, currentUser.schoolId, @@ -189,7 +189,7 @@ export class CourseController { sortingQuery.sortOrder ); - const response: CourseInfoListResponse = CourseInfoResponseMapper.mapToCourseInfoListResponse( + const response: CourseListResponse = CourseInfoResponseMapper.mapToCourseInfoListResponse( courses, pagination.skip, pagination.limit diff --git a/apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts b/apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts deleted file mode 100644 index 0cdb8b3cc0c..00000000000 --- a/apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { PaginationResponse } from '@shared/controller'; -import { CourseInfoResponse } from './course-info.response'; - -export class CourseInfoListResponse extends PaginationResponse { - constructor(data: CourseInfoResponse[], total: number, skip?: number, limit?: number) { - super(total, skip, limit); - this.data = data; - } - - @ApiProperty({ type: [CourseInfoResponse] }) - data: CourseInfoResponse[]; -} diff --git a/apps/server/src/modules/learnroom/controller/dto/response/course-list.response.ts b/apps/server/src/modules/learnroom/controller/dto/response/course-list.response.ts new file mode 100644 index 00000000000..77e1e9cae02 --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/response/course-list.response.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PaginationResponse } from '@shared/controller'; +import { CourseResponse } from './course.response'; + +export class CourseListResponse extends PaginationResponse { + constructor(data: CourseResponse[], total: number, skip?: number, limit?: number) { + super(total, skip, limit); + this.data = data; + } + + @ApiProperty({ type: [CourseResponse] }) + data: CourseResponse[]; +} diff --git a/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts b/apps/server/src/modules/learnroom/controller/dto/response/course.response.ts similarity index 86% rename from apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts rename to apps/server/src/modules/learnroom/controller/dto/response/course.response.ts index fdeea47c7c6..2bc8b910678 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/course-info.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/course.response.ts @@ -1,7 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { EntityId } from '@shared/domain/types'; -export class CourseInfoResponse { +export class CourseResponse { @ApiProperty() id: EntityId; @@ -17,7 +17,7 @@ export class CourseInfoResponse { @ApiPropertyOptional() syncedGroup?: string; - constructor(props: CourseInfoResponse) { + constructor(props: CourseResponse) { this.id = props.id; this.name = props.name; this.classNames = props.classNames; diff --git a/apps/server/src/modules/learnroom/controller/dto/response/index.ts b/apps/server/src/modules/learnroom/controller/dto/response/index.ts index 7406186c124..843f37b047e 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/index.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/index.ts @@ -1,2 +1,2 @@ -export { CourseInfoListResponse } from './course-info-list.response'; -export { CourseInfoResponse } from './course-info.response'; +export { CourseListResponse } from './course-list.response'; +export { CourseResponse } from './course.response'; diff --git a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts index 3edc4ab7a96..52e3c6ef61b 100644 --- a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts @@ -1,5 +1,5 @@ import { Page } from '@shared/domain/domainobject'; -import { CourseInfoListResponse, CourseInfoResponse } from '../controller/dto/response'; +import { CourseListResponse, CourseResponse } from '../controller/dto/response'; import { CourseInfoDto } from '../uc/dto'; export class CourseInfoResponseMapper { @@ -7,23 +7,18 @@ export class CourseInfoResponseMapper { courseInfos: Page, skip?: number, limit?: number - ): CourseInfoListResponse { - const courseInfoResponses: CourseInfoResponse[] = courseInfos.data.map((courseInfo) => + ): CourseListResponse { + const courseInfoResponses: CourseResponse[] = courseInfos.data.map((courseInfo) => this.mapToCourseInfoResponse(courseInfo) ); - const response: CourseInfoListResponse = new CourseInfoListResponse( - courseInfoResponses, - courseInfos.total, - skip, - limit - ); + const response: CourseListResponse = new CourseListResponse(courseInfoResponses, courseInfos.total, skip, limit); return response; } - private static mapToCourseInfoResponse(courseInfo: CourseInfoDto): CourseInfoResponse { - const courseInfoResponse: CourseInfoResponse = new CourseInfoResponse({ + private static mapToCourseInfoResponse(courseInfo: CourseInfoDto): CourseResponse { + const courseInfoResponse: CourseResponse = new CourseResponse({ id: courseInfo.id, name: courseInfo.name, classNames: courseInfo.classes, From d1cca42bed40db63cbe8e04509febeb9169853f7 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Mon, 26 Aug 2024 13:08:37 +0200 Subject: [PATCH 40/64] N21-2075 refactor endpoint + mapper --- .../src/modules/learnroom/controller/course.controller.ts | 8 ++++---- ...-info-response.mapper.ts => course-response.mapper.ts} | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename apps/server/src/modules/learnroom/mapper/{course-info-response.mapper.ts => course-response.mapper.ts} (96%) diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index f18afe712fd..17ed950250c 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -30,7 +30,7 @@ import { PaginationParams } from '@shared/controller/'; import { Page } from '@shared/domain/domainobject'; import { ErrorResponse } from '@src/core/error/dto'; import { Response } from 'express'; -import { CourseInfoResponseMapper } from '../mapper/course-info-response.mapper'; +import { CourseResponseMapper } from '../mapper/course-response.mapper'; import { CourseMapper } from '../mapper/course.mapper'; import { CourseExportUc, CourseImportUc, CourseSyncUc, CourseUc } from '../uc'; import { CourseInfoDto } from '../uc/dto'; @@ -170,11 +170,11 @@ export class CourseController { } @Get('/all') - @ApiOperation({ summary: 'Get a list of all courses.' }) + @ApiOperation({ summary: 'Get a list of courses for school.' }) @ApiResponse({ status: HttpStatus.OK, type: CourseListResponse }) @ApiResponse({ status: '4XX', type: ErrorResponse }) @ApiResponse({ status: '5XX', type: ErrorResponse }) - async getAllCourses( + async getCoursesForSchool( @CurrentUser() currentUser: ICurrentUser, @Query() pagination: PaginationParams, @Query() sortingQuery: CourseSortParams, @@ -189,7 +189,7 @@ export class CourseController { sortingQuery.sortOrder ); - const response: CourseListResponse = CourseInfoResponseMapper.mapToCourseInfoListResponse( + const response: CourseListResponse = CourseResponseMapper.mapToCourseInfoListResponse( courses, pagination.skip, pagination.limit diff --git a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-response.mapper.ts similarity index 96% rename from apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts rename to apps/server/src/modules/learnroom/mapper/course-response.mapper.ts index 52e3c6ef61b..342eddf3e5d 100644 --- a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/course-response.mapper.ts @@ -2,7 +2,7 @@ import { Page } from '@shared/domain/domainobject'; import { CourseListResponse, CourseResponse } from '../controller/dto/response'; import { CourseInfoDto } from '../uc/dto'; -export class CourseInfoResponseMapper { +export class CourseResponseMapper { public static mapToCourseInfoListResponse( courseInfos: Page, skip?: number, From 742d87d37facb4c6a95dd6909e69e47dff49d8c9 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Mon, 26 Aug 2024 14:57:29 +0200 Subject: [PATCH 41/64] add course info controller + test --- .../api-test/course-info.api.spec.ts | 176 ++++++++++++++++++ .../controller/api-test/course.api.spec.ts | 130 ------------- .../controller/course-info.controller.ts | 48 +++++ .../learnroom/controller/course.controller.ts | 37 ---- .../modules/learnroom/uc/course.uc.spec.ts | 7 +- 5 files changed, 228 insertions(+), 170 deletions(-) create mode 100644 apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts create mode 100644 apps/server/src/modules/learnroom/controller/course-info.controller.ts diff --git a/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts new file mode 100644 index 00000000000..30ffcd090f5 --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts @@ -0,0 +1,176 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { ServerTestModule } from '@modules/server/server.module'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Course as CourseEntity } from '@shared/domain/entity'; +import { Permission } from '@shared/domain/interface'; +import { + cleanupCollections, + courseFactory, + schoolEntityFactory, + TestApiClient, + UserAndAccountTestFactory, +} from '@shared/testing'; +import { CourseSortQueryType, CourseStatusQueryType } from '../../domain'; +import { CourseListResponse } from '../dto/response'; + +const createStudent = () => { + const { studentUser, studentAccount } = UserAndAccountTestFactory.buildStudent({}, [Permission.COURSE_VIEW]); + return { account: studentAccount, user: studentUser }; +}; +const createTeacher = () => { + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({}, [ + Permission.COURSE_VIEW, + Permission.COURSE_EDIT, + ]); + return { account: teacherAccount, user: teacherUser }; +}; + +const createAdmin = () => { + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({}, [Permission.COURSE_ADMINISTRATION]); + return { account: adminAccount, user: adminUser }; +}; + +describe('Course Info Controller (API)', () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testApiClient = new TestApiClient(app, 'courses'); + }); + + afterAll(async () => { + await cleanupCollections(em); + await app.close(); + }); + + describe('[GET] /course-infos', () => { + describe('when logged in as admin', () => { + const setup = async () => { + const student = createStudent(); + const teacher = createTeacher(); + const admin = createAdmin(); + const school = schoolEntityFactory.buildWithId({}); + + const currentCourses: CourseEntity[] = courseFactory.buildList(5, { + school, + }); + const archivedCourses: CourseEntity[] = courseFactory.buildList(10, { + school, + untilDate: new Date('2024-07-31T23:59:59'), + }); + + admin.user.school = school; + await em.persistAndFlush(school); + await em.persistAndFlush([...currentCourses, ...archivedCourses]); + await em.persistAndFlush([admin.account, admin.user]); + em.clear(); + + return { + student, + currentCourses, + archivedCourses, + teacher, + admin, + school, + }; + }; + + it('should return the correct response structure', async () => { + const { admin } = await setup(); + const query = {}; + + const loggedInClient = await testApiClient.login(admin.account); + const response = await loggedInClient.get('/all').query(query); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty('data'); + expect(response.body).toHaveProperty('skip'); + expect(response.body).toHaveProperty('limit'); + expect(response.body).toHaveProperty('total'); + }); + + it('should return archived courses in pages', async () => { + const { admin } = await setup(); + const query = { skip: 0, limit: 10, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.ARCHIVE }; + + const loggedInClient = await testApiClient.login(admin.account); + const response = await loggedInClient.get('/all').query(query); + + const { total, skip, limit, data } = response.body as CourseListResponse; + expect(response.statusCode).toBe(200); + expect(skip).toBe(0); + expect(limit).toBe(10); + expect(total).toBe(10); + expect(data.length).toBe(10); + }); + + it('should return current courses in pages', async () => { + const { admin, currentCourses } = await setup(); + const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; + + const loggedInClient = await testApiClient.login(admin.account); + const response = await loggedInClient.get('/all').query(query); + + const { total, skip, limit, data } = response.body as CourseListResponse; + expect(response.statusCode).toBe(200); + expect(skip).toBe(4); + expect(limit).toBe(2); + expect(total).toBe(1); + expect(data.length).toBe(1); + expect(data[0].id).toBe(currentCourses[4].id); + }); + }); + + describe('when logged in not authenticated/authorized', () => { + const setup = async () => { + const teacher = createTeacher(); + + await em.persistAndFlush([teacher.account, teacher.user]); + em.clear(); + + return { + teacher, + }; + }; + + it('should return unauthorized', async () => { + const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; + + const response = await testApiClient.get('/all').query(query); + + expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); + expect(response.body).toEqual({ + code: HttpStatus.UNAUTHORIZED, + message: 'Unauthorized', + title: 'Unauthorized', + type: 'UNAUTHORIZED', + }); + }); + + it('should return forbidden', async () => { + const { teacher } = await setup(); + const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; + + const loggedInClient = await testApiClient.login(teacher.account); + const response = await loggedInClient.get('/all').query(query); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body).toEqual({ + code: HttpStatus.FORBIDDEN, + message: 'Forbidden', + title: 'Forbidden', + type: 'FORBIDDEN', + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts index 0c32269c964..c9991fd5ebc 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts @@ -9,15 +9,12 @@ import { cleanupCollections, courseFactory, groupEntityFactory, - schoolEntityFactory, TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; import { readFile } from 'node:fs/promises'; import { CourseMetadataListResponse } from '../dto'; import { CourseCommonCartridgeMetadataResponse } from '../dto/course-cc-metadata.response'; -import { CourseListResponse } from '../dto/response'; -import { CourseSortQueryType, CourseStatusQueryType } from '../../domain'; const createStudent = () => { const { studentUser, studentAccount } = UserAndAccountTestFactory.buildStudent({}, [Permission.COURSE_VIEW]); @@ -31,11 +28,6 @@ const createTeacher = () => { return { account: teacherAccount, user: teacherUser }; }; -const createAdmin = () => { - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({}, [Permission.COURSE_ADMINISTRATION]); - return { account: adminAccount, user: adminUser }; -}; - describe('Course Controller (API)', () => { let app: INestApplication; let em: EntityManager; @@ -386,126 +378,4 @@ describe('Course Controller (API)', () => { expect(data.id).toBe(course.id); }); }); - - describe('[GET] /courses/all', () => { - describe('when logged in as admin', () => { - const setup = async () => { - const student = createStudent(); - const teacher = createTeacher(); - const admin = createAdmin(); - const school = schoolEntityFactory.buildWithId({}); - - const currentCourses: CourseEntity[] = courseFactory.buildList(5, { - school, - }); - const archivedCourses: CourseEntity[] = courseFactory.buildList(10, { - school, - untilDate: new Date('2024-07-31T23:59:59'), - }); - - admin.user.school = school; - await em.persistAndFlush(school); - await em.persistAndFlush([...currentCourses, ...archivedCourses]); - await em.persistAndFlush([admin.account, admin.user]); - em.clear(); - - return { - student, - currentCourses, - archivedCourses, - teacher, - admin, - school, - }; - }; - - it('should return the correct response structure', async () => { - const { admin } = await setup(); - const query = {}; - - const loggedInClient = await testApiClient.login(admin.account); - const response = await loggedInClient.get('/all').query(query); - - expect(response.statusCode).toBe(200); - expect(response.body).toHaveProperty('data'); - expect(response.body).toHaveProperty('skip'); - expect(response.body).toHaveProperty('limit'); - expect(response.body).toHaveProperty('total'); - }); - - it('should return archived courses in pages', async () => { - const { admin } = await setup(); - const query = { skip: 0, limit: 10, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.ARCHIVE }; - - const loggedInClient = await testApiClient.login(admin.account); - const response = await loggedInClient.get('/all').query(query); - - const { total, skip, limit, data } = response.body as CourseListResponse; - expect(response.statusCode).toBe(200); - expect(skip).toBe(0); - expect(limit).toBe(10); - expect(total).toBe(10); - expect(data.length).toBe(10); - }); - - it('should return current courses in pages', async () => { - const { admin, currentCourses } = await setup(); - const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; - - const loggedInClient = await testApiClient.login(admin.account); - const response = await loggedInClient.get('/all').query(query); - - const { total, skip, limit, data } = response.body as CourseListResponse; - expect(response.statusCode).toBe(200); - expect(skip).toBe(4); - expect(limit).toBe(2); - expect(total).toBe(1); - expect(data.length).toBe(1); - expect(data[0].id).toBe(currentCourses[4].id); - }); - }); - - describe('when logged in not authenticated/authorized', () => { - const setup = async () => { - const teacher = createTeacher(); - - await em.persistAndFlush([teacher.account, teacher.user]); - em.clear(); - - return { - teacher, - }; - }; - - it('should return unauthorized', async () => { - const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; - - const response = await testApiClient.get('/all').query(query); - - expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); - expect(response.body).toEqual({ - code: HttpStatus.UNAUTHORIZED, - message: 'Unauthorized', - title: 'Unauthorized', - type: 'UNAUTHORIZED', - }); - }); - - it('should return forbidden', async () => { - const { teacher } = await setup(); - const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; - - const loggedInClient = await testApiClient.login(teacher.account); - const response = await loggedInClient.get('/all').query(query); - - expect(response.status).toEqual(HttpStatus.FORBIDDEN); - expect(response.body).toEqual({ - code: HttpStatus.FORBIDDEN, - message: 'Forbidden', - title: 'Forbidden', - type: 'FORBIDDEN', - }); - }); - }); - }); }); diff --git a/apps/server/src/modules/learnroom/controller/course-info.controller.ts b/apps/server/src/modules/learnroom/controller/course-info.controller.ts new file mode 100644 index 00000000000..497bf1aab5c --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/course-info.controller.ts @@ -0,0 +1,48 @@ +import { CurrentUser, ICurrentUser, JwtAuthentication } from '@infra/auth-guard'; +import { Controller, Get, HttpStatus, Query } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { PaginationParams } from '@shared/controller/'; +import { Page } from '@shared/domain/domainobject'; +import { ErrorResponse } from '@src/core/error/dto'; +import { CourseResponseMapper } from '../mapper/course-response.mapper'; +import { CourseUc } from '../uc'; +import { CourseInfoDto } from '../uc/dto'; +import { CourseFilterParams } from './dto/request/course-filter-params'; +import { CourseSortParams } from './dto/request/course-sort-params'; +import { CourseListResponse } from './dto/response'; + +@ApiTags('Course Info') +@JwtAuthentication() +@Controller('course-info') +export class CourseController { + constructor(private readonly courseUc: CourseUc) {} + + @Get() + @ApiOperation({ summary: 'Get a list of courses for school.' }) + @ApiResponse({ status: HttpStatus.OK, type: CourseListResponse }) + @ApiResponse({ status: '4XX', type: ErrorResponse }) + @ApiResponse({ status: '5XX', type: ErrorResponse }) + async getCoursesForSchool( + @CurrentUser() currentUser: ICurrentUser, + @Query() pagination: PaginationParams, + @Query() sortingQuery: CourseSortParams, + @Query() filterParams: CourseFilterParams + ): Promise { + const courses: Page = await this.courseUc.findAllCourses( + currentUser.userId, + currentUser.schoolId, + sortingQuery.sortBy, + filterParams.type, + pagination, + sortingQuery.sortOrder + ); + + const response: CourseListResponse = CourseResponseMapper.mapToCourseInfoListResponse( + courses, + pagination.skip, + pagination.limit + ); + + return response; + } +} diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 17ed950250c..1e8febcb56c 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -22,18 +22,13 @@ import { ApiInternalServerErrorResponse, ApiNoContentResponse, ApiOperation, - ApiResponse, ApiTags, ApiUnprocessableEntityResponse, } from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller/'; -import { Page } from '@shared/domain/domainobject'; -import { ErrorResponse } from '@src/core/error/dto'; import { Response } from 'express'; -import { CourseResponseMapper } from '../mapper/course-response.mapper'; import { CourseMapper } from '../mapper/course.mapper'; import { CourseExportUc, CourseImportUc, CourseSyncUc, CourseUc } from '../uc'; -import { CourseInfoDto } from '../uc/dto'; import { CommonCartridgeFileValidatorPipe } from '../utils'; import { CourseExportBodyParams, @@ -44,9 +39,6 @@ import { CourseUrlParams, } from './dto'; import { CourseCommonCartridgeMetadataResponse } from './dto/course-cc-metadata.response'; -import { CourseFilterParams } from './dto/request/course-filter-params'; -import { CourseSortParams } from './dto/request/course-sort-params'; -import { CourseListResponse } from './dto/response'; @ApiTags('Courses') @JwtAuthentication() @@ -168,33 +160,4 @@ export class CourseController { return CourseMapper.mapToCommonCartridgeMetadataResponse(course); } - - @Get('/all') - @ApiOperation({ summary: 'Get a list of courses for school.' }) - @ApiResponse({ status: HttpStatus.OK, type: CourseListResponse }) - @ApiResponse({ status: '4XX', type: ErrorResponse }) - @ApiResponse({ status: '5XX', type: ErrorResponse }) - async getCoursesForSchool( - @CurrentUser() currentUser: ICurrentUser, - @Query() pagination: PaginationParams, - @Query() sortingQuery: CourseSortParams, - @Query() filterParams: CourseFilterParams - ): Promise { - const courses: Page = await this.courseUc.findAllCourses( - currentUser.userId, - currentUser.schoolId, - sortingQuery.sortBy, - filterParams.type, - pagination, - sortingQuery.sortOrder - ); - - const response: CourseListResponse = CourseResponseMapper.mapToCourseInfoListResponse( - courses, - pagination.skip, - pagination.limit - ); - - return response; - } } diff --git a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts index e1a4df31008..a183b7d95b6 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts @@ -260,9 +260,6 @@ describe('CourseUc', () => { sortOrder ); - expect(schoolService.getSchoolById).toHaveBeenCalledWith(school.id); - expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(adminUser.id); - expect(authorizationService.checkPermission).toHaveBeenCalled(); const filter = { schoolId: school.id, courseStatusQueryType: statusTypeQuery }; const options = { pagination, @@ -270,6 +267,10 @@ describe('CourseUc', () => { [sortByField]: sortOrder, }, }; + + expect(schoolService.getSchoolById).toHaveBeenCalledWith(school.id); + expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(adminUser.id); + expect(authorizationService.checkPermission).toHaveBeenCalled(); expect(courseDoService.findCourses).toHaveBeenCalledWith(filter, options); expect(userService.findById).toHaveBeenCalledWith(user.id); expect(classService.findById).toHaveBeenCalledWith(clazz.id); From 80fbfb4408424be21cc89038d5b86561db27663c Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Mon, 26 Aug 2024 15:09:44 +0200 Subject: [PATCH 42/64] update sourse info + course repo wip --- .../api-test/course-info.api.spec.ts | 12 +- .../mikro-orm/course.repo.integration.spec.ts | 144 +++++++++++++++++- 2 files changed, 149 insertions(+), 7 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts index 30ffcd090f5..4c6278c50ec 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts @@ -52,7 +52,7 @@ describe('Course Info Controller (API)', () => { await app.close(); }); - describe('[GET] /course-infos', () => { + describe('[GET] /course-info', () => { describe('when logged in as admin', () => { const setup = async () => { const student = createStudent(); @@ -89,7 +89,7 @@ describe('Course Info Controller (API)', () => { const query = {}; const loggedInClient = await testApiClient.login(admin.account); - const response = await loggedInClient.get('/all').query(query); + const response = await loggedInClient.get().query(query); expect(response.statusCode).toBe(200); expect(response.body).toHaveProperty('data'); @@ -103,7 +103,7 @@ describe('Course Info Controller (API)', () => { const query = { skip: 0, limit: 10, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.ARCHIVE }; const loggedInClient = await testApiClient.login(admin.account); - const response = await loggedInClient.get('/all').query(query); + const response = await loggedInClient.get().query(query); const { total, skip, limit, data } = response.body as CourseListResponse; expect(response.statusCode).toBe(200); @@ -118,7 +118,7 @@ describe('Course Info Controller (API)', () => { const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; const loggedInClient = await testApiClient.login(admin.account); - const response = await loggedInClient.get('/all').query(query); + const response = await loggedInClient.get().query(query); const { total, skip, limit, data } = response.body as CourseListResponse; expect(response.statusCode).toBe(200); @@ -145,7 +145,7 @@ describe('Course Info Controller (API)', () => { it('should return unauthorized', async () => { const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; - const response = await testApiClient.get('/all').query(query); + const response = await testApiClient.get().query(query); expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); expect(response.body).toEqual({ @@ -161,7 +161,7 @@ describe('Course Info Controller (API)', () => { const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; const loggedInClient = await testApiClient.login(teacher.account); - const response = await loggedInClient.get('/all').query(query); + const response = await loggedInClient.get().query(query); expect(response.status).toEqual(HttpStatus.FORBIDDEN); expect(response.body).toEqual({ diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts index c637c041432..643f9236fc7 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts @@ -7,6 +7,8 @@ import { Group } from '@modules/group'; import { GroupEntity } from '@modules/group/entity'; import { Test, TestingModule } from '@nestjs/testing'; import { Course as CourseEntity, CourseFeatures, CourseGroup, SchoolEntity, User } from '@shared/domain/entity'; +import { IFindOptions, SortOrder } from '@shared/domain/interface'; +import { CourseScope } from '@shared/repo'; import { cleanupCollections, courseFactory as courseEntityFactory, @@ -16,7 +18,7 @@ import { schoolEntityFactory, userFactory, } from '@shared/testing'; -import { Course, COURSE_REPO, CourseProps } from '../../domain'; +import { Course, COURSE_REPO, CourseProps, CourseSortQueryType, CourseStatusQueryType } from '../../domain'; import { courseFactory } from '../../testing'; import { CourseMikroOrmRepo } from './course.repo'; import { CourseEntityMapper } from './mapper/course.entity.mapper'; @@ -174,4 +176,144 @@ describe(CourseMikroOrmRepo.name, () => { }); }); }); + + describe('findCourses', () => { + describe('when entity is not found', () => { + const setup = async () => { + const courseEntity: CourseEntity = courseEntityFactory.buildWithId(); + const userEntity: User = userFactory.buildWithId(); + const schoolEntity: SchoolEntity = schoolEntityFactory.buildWithId(); + const groupEntity: GroupEntity = groupEntityFactory.buildWithId(); + const classEntity: ClassEntity = classEntityFactory.buildWithId(); + const courseScope = new CourseScope(); + + const expectedProps = { + id: courseEntity.id, + name: `course 1`, + features: new Set([CourseFeatures.VIDEOCONFERENCE]), + schoolId: schoolEntity.id, + studentIds: [userEntity.id], + teacherIds: [userEntity.id], + substitutionTeacherIds: [userEntity.id], + groupIds: [groupEntity.id], + classIds: [classEntity.id], + description: 'description', + color: '#ACACAC', + copyingSince: new Date(), + syncedWithGroup: groupEntity.id, + shareToken: 'shareToken', + untilDate: new Date(new Date().getTime() + 3600000), + startDate: new Date(), + }; + const courseDOs: Course[] = courseFactory.buildList(5, expectedProps); + const courseEntities: CourseEntity[] = courseEntityFactory.buildList(5); + + await em.persistAndFlush(courseEntities); + await em.persistAndFlush([courseEntity, userEntity, schoolEntity, groupEntity, classEntity]); + em.clear(); + + const pagination = { skip: 1, limit: 2 }; + const options = { + pagination, + order: { + name: SortOrder.asc, + }, + }; + + return { schoolEntity, courseEntities, courseDOs, options, courseScope }; + }; + + it('should apply archived courses scope when filter is ARCHIVE', async () => { + const { schoolEntity, options, courseScope } = await setup(); + const statusQuery: CourseStatusQueryType = CourseStatusQueryType.ARCHIVE; + const filter = { schoolId: schoolEntity.id, courseStatusQueryType: statusQuery }; + + jest.spyOn(CourseScope.prototype, 'bySchoolId').mockReturnThis(); + jest.spyOn(CourseScope.prototype, 'forArchivedCourses').mockReturnThis(); + + const result = await repo.findCourses(filter, options); + + expect(courseScope.bySchoolId).toHaveBeenCalledWith(filter.schoolId); + expect(courseScope.forArchivedCourses).toHaveBeenCalled(); + }); + + it('should apply archived courses scope when filter is CURRENT', async () => { + const { schoolEntity, options, courseScope } = await setup(); + const statusQuery: CourseStatusQueryType = CourseStatusQueryType.CURRENT; + const filter = { schoolId: schoolEntity.id, courseStatusQueryType: statusQuery }; + + jest.spyOn(CourseScope.prototype, 'bySchoolId').mockReturnThis(); + jest.spyOn(CourseScope.prototype, 'forActiveCourses').mockReturnThis(); + + const result = await repo.findCourses(filter, options); + + expect(courseScope.bySchoolId).toHaveBeenCalledWith(filter.schoolId); + expect(courseScope.forActiveCourses).toHaveBeenCalled(); + }); + + // it('should apply the correct scope and return mapped courses', async () => { + // const { schoolEntity, courseEntities, courseDOs, options, courseScope } = await setup(); + // const statusQuery: CourseStatusQueryType = CourseStatusQueryType.CURRENT; + // const filter = { schoolId: schoolEntity.id, courseStatusQueryType: statusQuery }; + + // const mapEntityToDoSpy = jest + // .spyOn(CourseEntityMapper, 'mapEntityToDo') + // .mockImplementation((entity) => courseDOs[courseEntities.indexOf(entity)]); + + // jest.spyOn(CourseScope.prototype, 'bySchoolId').mockReturnThis(); + // jest.spyOn(CourseScope.prototype, 'forActiveCourses').mockReturnThis(); + + // const result = await repo.findCourses(filter, options); + + // expect(courseScope.bySchoolId).toHaveBeenCalledWith(filter.schoolId); + // expect(courseScope.forActiveCourses).toHaveBeenCalled(); + // expect(mapEntityToDoSpy).toHaveBeenCalledTimes(courseEntities.length); + // expect(result).toEqual(courseDOs); + // }); + + // it('should apply default sorting if no order is provided', async () => { + // const { schoolEntity, courseScope } = await setup(); + // const statusQuery: CourseStatusQueryType = CourseStatusQueryType.CURRENT; + // const filter = { schoolId: schoolEntity.id, courseStatusQueryType: statusQuery }; + + // const defaultOptions: IFindOptions = { + // pagination: { skip: 0, limit: 10 }, + // }; + + // await repo.findCourses(filter, defaultOptions); + + // expect(em.find).toHaveBeenCalledWith( + // CourseEntity, + // courseScope.query, + // expect.objectContaining({ + // orderBy: { _id: 'asc' }, + // }) + // ); + // }); + + // it('should return epmty', async () => { + // const someId = new ObjectId().toHexString(); + // const pagination = { skip: 1, limit: 1 }; + // const statusQuery: CourseStatusQueryType = CourseStatusQueryType.CURRENT; + // const sortByField: CourseSortQueryType = CourseSortQueryType.NAME; + // const sortOrder: SortOrder = SortOrder.asc; + // const filter = { schoolId: someId, courseStatusQueryType: statusQuery }; + // const options = { + // pagination, + // order: { + // [sortByField]: sortOrder, + // }, + // }; + // const result: Course[] = await repo.findCourses(filter, options); + // expect(em.find).toHaveBeenCalledWith([ + // { + // schoolId: someId, + // courseStatusQueryType: CourseStatusQueryType.CURRENT, + // }, + // { order: { name: 'asc' }, pagination: { skip: 1, limit: 2 } }, + // ]); + // expect(result).toEqual([]); + // }); + }); + }); }); From b10bbec41460947d20faec371ed29e4c93613fab Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Mon, 26 Aug 2024 15:40:51 +0200 Subject: [PATCH 43/64] update course info controller + test --- .../learnroom/controller/api-test/course-info.api.spec.ts | 6 ++++-- .../modules/learnroom/controller/course-info.controller.ts | 2 +- apps/server/src/modules/learnroom/learnroom-api.module.ts | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts index 4c6278c50ec..73c0c0e7b2c 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts @@ -44,7 +44,7 @@ describe('Course Info Controller (API)', () => { app = module.createNestApplication(); await app.init(); em = module.get(EntityManager); - testApiClient = new TestApiClient(app, 'courses'); + testApiClient = new TestApiClient(app, 'course-info'); }); afterAll(async () => { @@ -62,6 +62,7 @@ describe('Course Info Controller (API)', () => { const currentCourses: CourseEntity[] = courseFactory.buildList(5, { school, + untilDate: new Date('2045-07-31T23:59:59'), }); const archivedCourses: CourseEntity[] = courseFactory.buildList(10, { school, @@ -70,7 +71,8 @@ describe('Course Info Controller (API)', () => { admin.user.school = school; await em.persistAndFlush(school); - await em.persistAndFlush([...currentCourses, ...archivedCourses]); + await em.persistAndFlush(currentCourses); + await em.persistAndFlush(archivedCourses); await em.persistAndFlush([admin.account, admin.user]); em.clear(); diff --git a/apps/server/src/modules/learnroom/controller/course-info.controller.ts b/apps/server/src/modules/learnroom/controller/course-info.controller.ts index 497bf1aab5c..57870ffef04 100644 --- a/apps/server/src/modules/learnroom/controller/course-info.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course-info.controller.ts @@ -14,7 +14,7 @@ import { CourseListResponse } from './dto/response'; @ApiTags('Course Info') @JwtAuthentication() @Controller('course-info') -export class CourseController { +export class CourseInfoController { constructor(private readonly courseUc: CourseUc) {} @Get() diff --git a/apps/server/src/modules/learnroom/learnroom-api.module.ts b/apps/server/src/modules/learnroom/learnroom-api.module.ts index 01613754d42..694c557d3a7 100644 --- a/apps/server/src/modules/learnroom/learnroom-api.module.ts +++ b/apps/server/src/modules/learnroom/learnroom-api.module.ts @@ -1,8 +1,8 @@ import { AuthorizationModule } from '@modules/authorization'; import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module'; +import { ClassModule } from '@modules/class'; import { CopyHelperModule } from '@modules/copy-helper'; import { GroupModule } from '@modules/group'; -import { ClassModule } from '@modules/class'; import { LessonModule } from '@modules/lesson'; import { RoleModule } from '@modules/role'; @@ -16,6 +16,7 @@ import { RoomsController } from './controller/rooms.controller'; import { LearnroomModule } from './learnroom.module'; import { RoomBoardResponseMapper } from './mapper/room-board-response.mapper'; +import { CourseInfoController } from './controller/course-info.controller'; import { CourseCopyUC, CourseExportUc, @@ -42,7 +43,7 @@ import { UserModule, ClassModule, ], - controllers: [DashboardController, CourseController, RoomsController], + controllers: [DashboardController, CourseController, CourseInfoController, RoomsController], providers: [ DashboardUc, CourseUc, From 5b3558e711af3876796b9d1224b6486f1b1370a7 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Mon, 26 Aug 2024 16:57:06 +0200 Subject: [PATCH 44/64] update classe service tests --- .../class/service/class.service.spec.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/apps/server/src/modules/class/service/class.service.spec.ts b/apps/server/src/modules/class/service/class.service.spec.ts index c7275661cca..7e483ae6544 100644 --- a/apps/server/src/modules/class/service/class.service.spec.ts +++ b/apps/server/src/modules/class/service/class.service.spec.ts @@ -12,6 +12,7 @@ import { deletionRequestFactory } from '@modules/deletion/domain/testing'; import { InternalServerErrorException } from '@nestjs/common'; import { EventBus } from '@nestjs/cqrs'; import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { EntityId } from '@shared/domain/types'; import { setupEntities } from '@shared/testing'; import { Logger } from '@src/core/logger'; @@ -126,6 +127,34 @@ describe(ClassService.name, () => { }); }); + describe('findById', () => { + describe('when the user has classes', () => { + const setup = () => { + const clazz: Class = classFactory.build(); + + classesRepo.findClassById.mockResolvedValueOnce(clazz); + + return { + clazz, + }; + }; + + it('should return the class', async () => { + const { clazz } = setup(); + + const result: Class = await service.findById(clazz.id); + + expect(result).toEqual(clazz); + }); + + it('should throw error', async () => { + classesRepo.findClassById.mockResolvedValueOnce(null); + + await expect(service.findById('someId')).rejects.toThrowError(NotFoundLoggableException); + }); + }); + }); + describe('deleteUserDataFromClasses', () => { describe('when user is missing', () => { const setup = () => { From c81fe1f97df58dc53a2e009d9a322fb41ad76345 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Tue, 27 Aug 2024 08:13:33 +0200 Subject: [PATCH 45/64] N21-2075 repo test --- .../mikro-orm/course.repo.integration.spec.ts | 171 ++++++------------ 1 file changed, 55 insertions(+), 116 deletions(-) diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts index 643f9236fc7..88177e01f3b 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts @@ -7,8 +7,7 @@ import { Group } from '@modules/group'; import { GroupEntity } from '@modules/group/entity'; import { Test, TestingModule } from '@nestjs/testing'; import { Course as CourseEntity, CourseFeatures, CourseGroup, SchoolEntity, User } from '@shared/domain/entity'; -import { IFindOptions, SortOrder } from '@shared/domain/interface'; -import { CourseScope } from '@shared/repo'; +import { SortOrder } from '@shared/domain/interface'; import { cleanupCollections, courseFactory as courseEntityFactory, @@ -18,7 +17,7 @@ import { schoolEntityFactory, userFactory, } from '@shared/testing'; -import { Course, COURSE_REPO, CourseProps, CourseSortQueryType, CourseStatusQueryType } from '../../domain'; +import { Course, COURSE_REPO, CourseProps, CourseStatusQueryType } from '../../domain'; import { courseFactory } from '../../testing'; import { CourseMikroOrmRepo } from './course.repo'; import { CourseEntityMapper } from './mapper/course.entity.mapper'; @@ -178,142 +177,82 @@ describe(CourseMikroOrmRepo.name, () => { }); describe('findCourses', () => { - describe('when entity is not found', () => { + describe('when entitys are not found', () => { const setup = async () => { - const courseEntity: CourseEntity = courseEntityFactory.buildWithId(); - const userEntity: User = userFactory.buildWithId(); const schoolEntity: SchoolEntity = schoolEntityFactory.buildWithId(); - const groupEntity: GroupEntity = groupEntityFactory.buildWithId(); - const classEntity: ClassEntity = classEntityFactory.buildWithId(); - const courseScope = new CourseScope(); + const courseEntities: CourseEntity[] = courseEntityFactory.buildList(2, { + school: schoolEntity, + untilDate: new Date('2050-04-24'), + }); - const expectedProps = { - id: courseEntity.id, - name: `course 1`, - features: new Set([CourseFeatures.VIDEOCONFERENCE]), - schoolId: schoolEntity.id, - studentIds: [userEntity.id], - teacherIds: [userEntity.id], - substitutionTeacherIds: [userEntity.id], - groupIds: [groupEntity.id], - classIds: [classEntity.id], - description: 'description', - color: '#ACACAC', - copyingSince: new Date(), - syncedWithGroup: groupEntity.id, - shareToken: 'shareToken', - untilDate: new Date(new Date().getTime() + 3600000), - startDate: new Date(), - }; - const courseDOs: Course[] = courseFactory.buildList(5, expectedProps); - const courseEntities: CourseEntity[] = courseEntityFactory.buildList(5); - - await em.persistAndFlush(courseEntities); - await em.persistAndFlush([courseEntity, userEntity, schoolEntity, groupEntity, classEntity]); + await em.persistAndFlush([schoolEntity, ...courseEntities]); em.clear(); const pagination = { skip: 1, limit: 2 }; + + const filter = { schoolId: schoolEntity.id, courseStatusQueryType: CourseStatusQueryType.ARCHIVE }; + + const courseDOs = courseEntities.map((courseEntity) => CourseEntityMapper.mapEntityToDo(courseEntity)); + return { courseDOs, filter }; + }; + + it('should return empty array', async () => { + const { filter } = await setup(); + + const result = await repo.findCourses(filter); + + expect(result).toEqual([]); + }); + }); + + describe('when entitys are found for school', () => { + const setup = async () => { + const schoolEntity: SchoolEntity = schoolEntityFactory.buildWithId(); + const courseEntities: CourseEntity[] = courseEntityFactory.buildList(5, { + school: schoolEntity, + untilDate: new Date('1995-04-24'), + }); + + courseEntities.push( + ...courseEntityFactory.buildList(3, { + school: schoolEntity, + untilDate: new Date('2050-04-24'), + }) + ); + + await em.persistAndFlush([schoolEntity, ...courseEntities]); + em.clear(); + + const pagination = { skip: 0, limit: 10 }; const options = { pagination, order: { - name: SortOrder.asc, + name: SortOrder.desc, }, }; + const filter = { schoolId: schoolEntity.id, courseStatusQueryType: CourseStatusQueryType.ARCHIVE }; - return { schoolEntity, courseEntities, courseDOs, options, courseScope }; - }; + const courseDOs = courseEntities.map((courseEntity) => CourseEntityMapper.mapEntityToDo(courseEntity)); - it('should apply archived courses scope when filter is ARCHIVE', async () => { - const { schoolEntity, options, courseScope } = await setup(); - const statusQuery: CourseStatusQueryType = CourseStatusQueryType.ARCHIVE; - const filter = { schoolId: schoolEntity.id, courseStatusQueryType: statusQuery }; + return { courseDOs, options, filter }; + }; - jest.spyOn(CourseScope.prototype, 'bySchoolId').mockReturnThis(); - jest.spyOn(CourseScope.prototype, 'forArchivedCourses').mockReturnThis(); + it('should return archived courses', async () => { + const { options, filter, courseDOs } = await setup(); const result = await repo.findCourses(filter, options); - expect(courseScope.bySchoolId).toHaveBeenCalledWith(filter.schoolId); - expect(courseScope.forArchivedCourses).toHaveBeenCalled(); + expect(result.length).toEqual(5); }); - it('should apply archived courses scope when filter is CURRENT', async () => { - const { schoolEntity, options, courseScope } = await setup(); - const statusQuery: CourseStatusQueryType = CourseStatusQueryType.CURRENT; - const filter = { schoolId: schoolEntity.id, courseStatusQueryType: statusQuery }; - - jest.spyOn(CourseScope.prototype, 'bySchoolId').mockReturnThis(); - jest.spyOn(CourseScope.prototype, 'forActiveCourses').mockReturnThis(); + it('should return current courses', async () => { + const { options, filter } = await setup(); + filter.courseStatusQueryType = CourseStatusQueryType.CURRENT; const result = await repo.findCourses(filter, options); - expect(courseScope.bySchoolId).toHaveBeenCalledWith(filter.schoolId); - expect(courseScope.forActiveCourses).toHaveBeenCalled(); + expect(result.length).toEqual(3); }); - - // it('should apply the correct scope and return mapped courses', async () => { - // const { schoolEntity, courseEntities, courseDOs, options, courseScope } = await setup(); - // const statusQuery: CourseStatusQueryType = CourseStatusQueryType.CURRENT; - // const filter = { schoolId: schoolEntity.id, courseStatusQueryType: statusQuery }; - - // const mapEntityToDoSpy = jest - // .spyOn(CourseEntityMapper, 'mapEntityToDo') - // .mockImplementation((entity) => courseDOs[courseEntities.indexOf(entity)]); - - // jest.spyOn(CourseScope.prototype, 'bySchoolId').mockReturnThis(); - // jest.spyOn(CourseScope.prototype, 'forActiveCourses').mockReturnThis(); - - // const result = await repo.findCourses(filter, options); - - // expect(courseScope.bySchoolId).toHaveBeenCalledWith(filter.schoolId); - // expect(courseScope.forActiveCourses).toHaveBeenCalled(); - // expect(mapEntityToDoSpy).toHaveBeenCalledTimes(courseEntities.length); - // expect(result).toEqual(courseDOs); - // }); - - // it('should apply default sorting if no order is provided', async () => { - // const { schoolEntity, courseScope } = await setup(); - // const statusQuery: CourseStatusQueryType = CourseStatusQueryType.CURRENT; - // const filter = { schoolId: schoolEntity.id, courseStatusQueryType: statusQuery }; - - // const defaultOptions: IFindOptions = { - // pagination: { skip: 0, limit: 10 }, - // }; - - // await repo.findCourses(filter, defaultOptions); - - // expect(em.find).toHaveBeenCalledWith( - // CourseEntity, - // courseScope.query, - // expect.objectContaining({ - // orderBy: { _id: 'asc' }, - // }) - // ); - // }); - - // it('should return epmty', async () => { - // const someId = new ObjectId().toHexString(); - // const pagination = { skip: 1, limit: 1 }; - // const statusQuery: CourseStatusQueryType = CourseStatusQueryType.CURRENT; - // const sortByField: CourseSortQueryType = CourseSortQueryType.NAME; - // const sortOrder: SortOrder = SortOrder.asc; - // const filter = { schoolId: someId, courseStatusQueryType: statusQuery }; - // const options = { - // pagination, - // order: { - // [sortByField]: sortOrder, - // }, - // }; - // const result: Course[] = await repo.findCourses(filter, options); - // expect(em.find).toHaveBeenCalledWith([ - // { - // schoolId: someId, - // courseStatusQueryType: CourseStatusQueryType.CURRENT, - // }, - // { order: { name: 'asc' }, pagination: { skip: 1, limit: 2 } }, - // ]); - // expect(result).toEqual([]); - // }); }); }); }); From 6f48e95b43286e822d335189830b85159b9eb62e Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Tue, 27 Aug 2024 08:14:11 +0200 Subject: [PATCH 46/64] update course repo + test --- .../mikro-orm/course.repo.integration.spec.ts | 89 +------------------ .../learnroom/repo/mikro-orm/course.repo.ts | 9 +- 2 files changed, 4 insertions(+), 94 deletions(-) diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts index 643f9236fc7..7bcc383a1c4 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts @@ -186,22 +186,17 @@ describe(CourseMikroOrmRepo.name, () => { const groupEntity: GroupEntity = groupEntityFactory.buildWithId(); const classEntity: ClassEntity = classEntityFactory.buildWithId(); const courseScope = new CourseScope(); + const emSPy = jest.spyOn(em, 'find'); const expectedProps = { - id: courseEntity.id, name: `course 1`, features: new Set([CourseFeatures.VIDEOCONFERENCE]), schoolId: schoolEntity.id, studentIds: [userEntity.id], - teacherIds: [userEntity.id], - substitutionTeacherIds: [userEntity.id], groupIds: [groupEntity.id], classIds: [classEntity.id], - description: 'description', - color: '#ACACAC', copyingSince: new Date(), syncedWithGroup: groupEntity.id, - shareToken: 'shareToken', untilDate: new Date(new Date().getTime() + 3600000), startDate: new Date(), }; @@ -220,7 +215,7 @@ describe(CourseMikroOrmRepo.name, () => { }, }; - return { schoolEntity, courseEntities, courseDOs, options, courseScope }; + return { emSPy, schoolEntity, courseEntities, courseDOs, options, courseScope }; }; it('should apply archived courses scope when filter is ARCHIVE', async () => { @@ -231,89 +226,11 @@ describe(CourseMikroOrmRepo.name, () => { jest.spyOn(CourseScope.prototype, 'bySchoolId').mockReturnThis(); jest.spyOn(CourseScope.prototype, 'forArchivedCourses').mockReturnThis(); - const result = await repo.findCourses(filter, options); + await repo.findCourses(filter, options); expect(courseScope.bySchoolId).toHaveBeenCalledWith(filter.schoolId); expect(courseScope.forArchivedCourses).toHaveBeenCalled(); }); - - it('should apply archived courses scope when filter is CURRENT', async () => { - const { schoolEntity, options, courseScope } = await setup(); - const statusQuery: CourseStatusQueryType = CourseStatusQueryType.CURRENT; - const filter = { schoolId: schoolEntity.id, courseStatusQueryType: statusQuery }; - - jest.spyOn(CourseScope.prototype, 'bySchoolId').mockReturnThis(); - jest.spyOn(CourseScope.prototype, 'forActiveCourses').mockReturnThis(); - - const result = await repo.findCourses(filter, options); - - expect(courseScope.bySchoolId).toHaveBeenCalledWith(filter.schoolId); - expect(courseScope.forActiveCourses).toHaveBeenCalled(); - }); - - // it('should apply the correct scope and return mapped courses', async () => { - // const { schoolEntity, courseEntities, courseDOs, options, courseScope } = await setup(); - // const statusQuery: CourseStatusQueryType = CourseStatusQueryType.CURRENT; - // const filter = { schoolId: schoolEntity.id, courseStatusQueryType: statusQuery }; - - // const mapEntityToDoSpy = jest - // .spyOn(CourseEntityMapper, 'mapEntityToDo') - // .mockImplementation((entity) => courseDOs[courseEntities.indexOf(entity)]); - - // jest.spyOn(CourseScope.prototype, 'bySchoolId').mockReturnThis(); - // jest.spyOn(CourseScope.prototype, 'forActiveCourses').mockReturnThis(); - - // const result = await repo.findCourses(filter, options); - - // expect(courseScope.bySchoolId).toHaveBeenCalledWith(filter.schoolId); - // expect(courseScope.forActiveCourses).toHaveBeenCalled(); - // expect(mapEntityToDoSpy).toHaveBeenCalledTimes(courseEntities.length); - // expect(result).toEqual(courseDOs); - // }); - - // it('should apply default sorting if no order is provided', async () => { - // const { schoolEntity, courseScope } = await setup(); - // const statusQuery: CourseStatusQueryType = CourseStatusQueryType.CURRENT; - // const filter = { schoolId: schoolEntity.id, courseStatusQueryType: statusQuery }; - - // const defaultOptions: IFindOptions = { - // pagination: { skip: 0, limit: 10 }, - // }; - - // await repo.findCourses(filter, defaultOptions); - - // expect(em.find).toHaveBeenCalledWith( - // CourseEntity, - // courseScope.query, - // expect.objectContaining({ - // orderBy: { _id: 'asc' }, - // }) - // ); - // }); - - // it('should return epmty', async () => { - // const someId = new ObjectId().toHexString(); - // const pagination = { skip: 1, limit: 1 }; - // const statusQuery: CourseStatusQueryType = CourseStatusQueryType.CURRENT; - // const sortByField: CourseSortQueryType = CourseSortQueryType.NAME; - // const sortOrder: SortOrder = SortOrder.asc; - // const filter = { schoolId: someId, courseStatusQueryType: statusQuery }; - // const options = { - // pagination, - // order: { - // [sortByField]: sortOrder, - // }, - // }; - // const result: Course[] = await repo.findCourses(filter, options); - // expect(em.find).toHaveBeenCalledWith([ - // { - // schoolId: someId, - // courseStatusQueryType: CourseStatusQueryType.CURRENT, - // }, - // { order: { name: 'asc' }, pagination: { skip: 1, limit: 2 } }, - // ]); - // expect(result).toEqual([]); - // }); }); }); }); diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts index 5c100b3bd7c..0d66936dbbb 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts @@ -1,7 +1,7 @@ import { EntityData, EntityName, FindOptions } from '@mikro-orm/core'; import { Group } from '@modules/group'; import { Course as CourseEntity } from '@shared/domain/entity'; -import { IFindOptions, SortOrder } from '@shared/domain/interface'; +import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { BaseDomainObjectRepo } from '@shared/repo/base-domain-object.repo'; import { CourseScope } from '@shared/repo/course/course.repo'; @@ -80,13 +80,6 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo Date: Tue, 27 Aug 2024 08:35:33 +0200 Subject: [PATCH 47/64] N21-2075 refactor response --- .../controller/dto/request/course-filter-params.ts | 2 +- .../controller/dto/request/course-sort-params.ts | 2 +- .../controller/dto/response/course-list.response.ts | 10 +++++----- .../{course.response.ts => courseInfoDataResponse.ts} | 4 ++-- .../modules/learnroom/controller/dto/response/index.ts | 2 +- .../modules/learnroom/mapper/course-response.mapper.ts | 8 ++++---- 6 files changed, 14 insertions(+), 14 deletions(-) rename apps/server/src/modules/learnroom/controller/dto/response/{course.response.ts => courseInfoDataResponse.ts} (85%) diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts index d3af51409bf..ed2e8c27e00 100644 --- a/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts +++ b/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts @@ -1,6 +1,6 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; -import { CourseStatusQueryType } from '../../../domain/interface/course-status-query-type.enum'; +import { CourseStatusQueryType } from '../../../domain'; export class CourseFilterParams { @IsOptional() diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts index 964e16b2656..a59a27c4ebd 100644 --- a/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts +++ b/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts @@ -1,7 +1,7 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { SortingParams } from '@shared/controller'; import { IsEnum, IsOptional } from 'class-validator'; -import { CourseSortQueryType } from '../../../domain/interface/course-sort-query-type.enum'; +import { CourseSortQueryType } from '../../../domain'; export class CourseSortParams extends SortingParams { @IsOptional() diff --git a/apps/server/src/modules/learnroom/controller/dto/response/course-list.response.ts b/apps/server/src/modules/learnroom/controller/dto/response/course-list.response.ts index 77e1e9cae02..9c58bea888e 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/course-list.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/course-list.response.ts @@ -1,13 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; import { PaginationResponse } from '@shared/controller'; -import { CourseResponse } from './course.response'; +import { CourseInfoDataResponse } from './courseInfoDataResponse'; -export class CourseListResponse extends PaginationResponse { - constructor(data: CourseResponse[], total: number, skip?: number, limit?: number) { +export class CourseListResponse extends PaginationResponse { + constructor(data: CourseInfoDataResponse[], total: number, skip?: number, limit?: number) { super(total, skip, limit); this.data = data; } - @ApiProperty({ type: [CourseResponse] }) - data: CourseResponse[]; + @ApiProperty({ type: [CourseInfoDataResponse] }) + data: CourseInfoDataResponse[]; } diff --git a/apps/server/src/modules/learnroom/controller/dto/response/course.response.ts b/apps/server/src/modules/learnroom/controller/dto/response/courseInfoDataResponse.ts similarity index 85% rename from apps/server/src/modules/learnroom/controller/dto/response/course.response.ts rename to apps/server/src/modules/learnroom/controller/dto/response/courseInfoDataResponse.ts index 2bc8b910678..e506825a681 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/course.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/courseInfoDataResponse.ts @@ -1,7 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { EntityId } from '@shared/domain/types'; -export class CourseResponse { +export class CourseInfoDataResponse { @ApiProperty() id: EntityId; @@ -17,7 +17,7 @@ export class CourseResponse { @ApiPropertyOptional() syncedGroup?: string; - constructor(props: CourseResponse) { + constructor(props: CourseInfoDataResponse) { this.id = props.id; this.name = props.name; this.classNames = props.classNames; diff --git a/apps/server/src/modules/learnroom/controller/dto/response/index.ts b/apps/server/src/modules/learnroom/controller/dto/response/index.ts index 843f37b047e..97ac8b57f7b 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/index.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/index.ts @@ -1,2 +1,2 @@ export { CourseListResponse } from './course-list.response'; -export { CourseResponse } from './course.response'; +export { CourseInfoDataResponse } from './courseInfoDataResponse'; diff --git a/apps/server/src/modules/learnroom/mapper/course-response.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-response.mapper.ts index 342eddf3e5d..cbda32d2d5b 100644 --- a/apps/server/src/modules/learnroom/mapper/course-response.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/course-response.mapper.ts @@ -1,5 +1,5 @@ import { Page } from '@shared/domain/domainobject'; -import { CourseListResponse, CourseResponse } from '../controller/dto/response'; +import { CourseListResponse, CourseInfoDataResponse } from '../controller/dto/response'; import { CourseInfoDto } from '../uc/dto'; export class CourseResponseMapper { @@ -8,7 +8,7 @@ export class CourseResponseMapper { skip?: number, limit?: number ): CourseListResponse { - const courseInfoResponses: CourseResponse[] = courseInfos.data.map((courseInfo) => + const courseInfoResponses: CourseInfoDataResponse[] = courseInfos.data.map((courseInfo) => this.mapToCourseInfoResponse(courseInfo) ); @@ -17,8 +17,8 @@ export class CourseResponseMapper { return response; } - private static mapToCourseInfoResponse(courseInfo: CourseInfoDto): CourseResponse { - const courseInfoResponse: CourseResponse = new CourseResponse({ + private static mapToCourseInfoResponse(courseInfo: CourseInfoDto): CourseInfoDataResponse { + const courseInfoResponse: CourseInfoDataResponse = new CourseInfoDataResponse({ id: courseInfo.id, name: courseInfo.name, classNames: courseInfo.classes, From 831aee1348a670966672892888f9e164afacacab Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Tue, 27 Aug 2024 08:54:07 +0200 Subject: [PATCH 48/64] N21-2075 refactoring response + mapper --- .../controller/api-test/course-info.api.spec.ts | 6 +++--- .../learnroom/controller/course-info.controller.ts | 10 +++++----- ...ist.response.ts => course-info-list.response.ts} | 2 +- .../learnroom/controller/dto/response/index.ts | 2 +- ...nse.mapper.ts => course-info-response.mapper.ts} | 13 +++++++++---- 5 files changed, 19 insertions(+), 14 deletions(-) rename apps/server/src/modules/learnroom/controller/dto/response/{course-list.response.ts => course-info-list.response.ts} (81%) rename apps/server/src/modules/learnroom/mapper/{course-response.mapper.ts => course-info-response.mapper.ts} (72%) diff --git a/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts index 73c0c0e7b2c..13f52a5a0b3 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts @@ -12,7 +12,7 @@ import { UserAndAccountTestFactory, } from '@shared/testing'; import { CourseSortQueryType, CourseStatusQueryType } from '../../domain'; -import { CourseListResponse } from '../dto/response'; +import { CourseInfoListResponse } from '../dto/response'; const createStudent = () => { const { studentUser, studentAccount } = UserAndAccountTestFactory.buildStudent({}, [Permission.COURSE_VIEW]); @@ -107,7 +107,7 @@ describe('Course Info Controller (API)', () => { const loggedInClient = await testApiClient.login(admin.account); const response = await loggedInClient.get().query(query); - const { total, skip, limit, data } = response.body as CourseListResponse; + const { total, skip, limit, data } = response.body as CourseInfoListResponse; expect(response.statusCode).toBe(200); expect(skip).toBe(0); expect(limit).toBe(10); @@ -122,7 +122,7 @@ describe('Course Info Controller (API)', () => { const loggedInClient = await testApiClient.login(admin.account); const response = await loggedInClient.get().query(query); - const { total, skip, limit, data } = response.body as CourseListResponse; + const { total, skip, limit, data } = response.body as CourseInfoListResponse; expect(response.statusCode).toBe(200); expect(skip).toBe(4); expect(limit).toBe(2); diff --git a/apps/server/src/modules/learnroom/controller/course-info.controller.ts b/apps/server/src/modules/learnroom/controller/course-info.controller.ts index 57870ffef04..5ab05cac756 100644 --- a/apps/server/src/modules/learnroom/controller/course-info.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course-info.controller.ts @@ -4,12 +4,12 @@ import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller/'; import { Page } from '@shared/domain/domainobject'; import { ErrorResponse } from '@src/core/error/dto'; -import { CourseResponseMapper } from '../mapper/course-response.mapper'; +import { CourseInfoResponseMapper } from '../mapper/course-info-response.mapper'; import { CourseUc } from '../uc'; import { CourseInfoDto } from '../uc/dto'; import { CourseFilterParams } from './dto/request/course-filter-params'; import { CourseSortParams } from './dto/request/course-sort-params'; -import { CourseListResponse } from './dto/response'; +import { CourseInfoListResponse } from './dto/response'; @ApiTags('Course Info') @JwtAuthentication() @@ -19,7 +19,7 @@ export class CourseInfoController { @Get() @ApiOperation({ summary: 'Get a list of courses for school.' }) - @ApiResponse({ status: HttpStatus.OK, type: CourseListResponse }) + @ApiResponse({ status: HttpStatus.OK, type: CourseInfoListResponse }) @ApiResponse({ status: '4XX', type: ErrorResponse }) @ApiResponse({ status: '5XX', type: ErrorResponse }) async getCoursesForSchool( @@ -27,7 +27,7 @@ export class CourseInfoController { @Query() pagination: PaginationParams, @Query() sortingQuery: CourseSortParams, @Query() filterParams: CourseFilterParams - ): Promise { + ): Promise { const courses: Page = await this.courseUc.findAllCourses( currentUser.userId, currentUser.schoolId, @@ -37,7 +37,7 @@ export class CourseInfoController { sortingQuery.sortOrder ); - const response: CourseListResponse = CourseResponseMapper.mapToCourseInfoListResponse( + const response: CourseInfoListResponse = CourseInfoResponseMapper.mapToCourseInfoListResponse( courses, pagination.skip, pagination.limit diff --git a/apps/server/src/modules/learnroom/controller/dto/response/course-list.response.ts b/apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts similarity index 81% rename from apps/server/src/modules/learnroom/controller/dto/response/course-list.response.ts rename to apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts index 9c58bea888e..bab655ab3a7 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/course-list.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { PaginationResponse } from '@shared/controller'; import { CourseInfoDataResponse } from './courseInfoDataResponse'; -export class CourseListResponse extends PaginationResponse { +export class CourseInfoListResponse extends PaginationResponse { constructor(data: CourseInfoDataResponse[], total: number, skip?: number, limit?: number) { super(total, skip, limit); this.data = data; diff --git a/apps/server/src/modules/learnroom/controller/dto/response/index.ts b/apps/server/src/modules/learnroom/controller/dto/response/index.ts index 97ac8b57f7b..45256ebbf13 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/index.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/index.ts @@ -1,2 +1,2 @@ -export { CourseListResponse } from './course-list.response'; +export { CourseInfoListResponse } from './course-info-list.response'; export { CourseInfoDataResponse } from './courseInfoDataResponse'; diff --git a/apps/server/src/modules/learnroom/mapper/course-response.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts similarity index 72% rename from apps/server/src/modules/learnroom/mapper/course-response.mapper.ts rename to apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts index cbda32d2d5b..3274a0af251 100644 --- a/apps/server/src/modules/learnroom/mapper/course-response.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts @@ -1,18 +1,23 @@ import { Page } from '@shared/domain/domainobject'; -import { CourseListResponse, CourseInfoDataResponse } from '../controller/dto/response'; +import { CourseInfoListResponse, CourseInfoDataResponse } from '../controller/dto/response'; import { CourseInfoDto } from '../uc/dto'; -export class CourseResponseMapper { +export class CourseInfoResponseMapper { public static mapToCourseInfoListResponse( courseInfos: Page, skip?: number, limit?: number - ): CourseListResponse { + ): CourseInfoListResponse { const courseInfoResponses: CourseInfoDataResponse[] = courseInfos.data.map((courseInfo) => this.mapToCourseInfoResponse(courseInfo) ); - const response: CourseListResponse = new CourseListResponse(courseInfoResponses, courseInfos.total, skip, limit); + const response: CourseInfoListResponse = new CourseInfoListResponse( + courseInfoResponses, + courseInfos.total, + skip, + limit + ); return response; } From 4729c85d9b56179d63b073ee39cb5114ebc8c21f Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Tue, 27 Aug 2024 11:07:31 +0200 Subject: [PATCH 49/64] N21-2075 nested ternary operation fix --- .../src/modules/authorization/domain/rules/course.rule.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/authorization/domain/rules/course.rule.ts b/apps/server/src/modules/authorization/domain/rules/course.rule.ts index 2659d720350..249832b8a42 100644 --- a/apps/server/src/modules/authorization/domain/rules/course.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/course.rule.ts @@ -26,11 +26,15 @@ export class CourseRule implements Rule { : this.authorizationHelper.hasAccessToEntity( user, object, - action === Action.read + this.isReadAction(action) ? ['teachers', 'substitutionTeachers', 'students'] : ['teachers', 'substitutionTeachers'] ); return hasAccessToEntity && hasRequiredPermission; } + + isReadAction(action: Action) { + return action === Action.read; + } } From 85dbe0cf789d94bc09820fc0efebd0c631d4bd0b Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Tue, 27 Aug 2024 11:14:44 +0200 Subject: [PATCH 50/64] N21-2075 fix merge --- apps/server/src/modules/learnroom/learnroom-api.module.ts | 2 +- apps/server/src/modules/learnroom/learnroom.module.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/modules/learnroom/learnroom-api.module.ts b/apps/server/src/modules/learnroom/learnroom-api.module.ts index 6e54277e3b1..55caa8bc3bc 100644 --- a/apps/server/src/modules/learnroom/learnroom-api.module.ts +++ b/apps/server/src/modules/learnroom/learnroom-api.module.ts @@ -47,7 +47,7 @@ import { UserModule, ClassModule, ], - controllers: [DashboardController, CourseController, CourseInfoController, RoomsController, CourseRoomsController], + controllers: [DashboardController, CourseController, CourseInfoController, CourseRoomsController], providers: [ DashboardUc, CourseUc, diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index 957d42d2278..96c76839302 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -116,7 +116,6 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; CourseDoService, CourseRoomsService, GroupService, - RoomsService, CommonCartridgeExportService, CommonCartridgeImportService, CourseGroupService, From fbc4006d53d22c7a74d453bdeae964ffc153bb8f Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Tue, 27 Aug 2024 13:35:13 +0200 Subject: [PATCH 51/64] N21-2075 fix rule --- .../src/modules/authorization/domain/rules/course.rule.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/authorization/domain/rules/course.rule.ts b/apps/server/src/modules/authorization/domain/rules/course.rule.ts index 249832b8a42..c97afb098fa 100644 --- a/apps/server/src/modules/authorization/domain/rules/course.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/course.rule.ts @@ -35,6 +35,9 @@ export class CourseRule implements Rule { } isReadAction(action: Action) { - return action === Action.read; + if (action === Action.read) { + return true; + } + return false; } } From 95ee590dd505762d0afe6e47480bd0124ff34e2e Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Wed, 28 Aug 2024 14:17:30 +0200 Subject: [PATCH 52/64] upda course info controller; add course-info uc; update tests; cleanup --- .../api-test/course-info.api.spec.ts | 2 +- .../controller/course-info.controller.ts | 6 +- .../domain/interface/course.repo.interface.ts | 3 +- .../modules/learnroom/learnroom-api.module.ts | 8 +- .../mikro-orm/course.repo.integration.spec.ts | 14 +- .../learnroom/repo/mikro-orm/course.repo.ts | 9 +- .../service/course-do.service.spec.ts | 7 +- .../learnroom/service/course-do.service.ts | 5 +- .../learnroom/uc/course-info.uc.spec.ts | 215 ++++++++++++++++++ .../modules/learnroom/uc/course-info.uc.ts | 118 ++++++++++ .../modules/learnroom/uc/course.uc.spec.ts | 166 +------------- .../src/modules/learnroom/uc/course.uc.ts | 111 +-------- apps/server/src/modules/learnroom/uc/index.ts | 1 + 13 files changed, 372 insertions(+), 293 deletions(-) create mode 100644 apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts create mode 100644 apps/server/src/modules/learnroom/uc/course-info.uc.ts diff --git a/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts index 13f52a5a0b3..f26689f9a27 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts @@ -126,7 +126,7 @@ describe('Course Info Controller (API)', () => { expect(response.statusCode).toBe(200); expect(skip).toBe(4); expect(limit).toBe(2); - expect(total).toBe(1); + expect(total).toBe(5); expect(data.length).toBe(1); expect(data[0].id).toBe(currentCourses[4].id); }); diff --git a/apps/server/src/modules/learnroom/controller/course-info.controller.ts b/apps/server/src/modules/learnroom/controller/course-info.controller.ts index 5ab05cac756..1f1c5808bd5 100644 --- a/apps/server/src/modules/learnroom/controller/course-info.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course-info.controller.ts @@ -5,17 +5,17 @@ import { PaginationParams } from '@shared/controller/'; import { Page } from '@shared/domain/domainobject'; import { ErrorResponse } from '@src/core/error/dto'; import { CourseInfoResponseMapper } from '../mapper/course-info-response.mapper'; -import { CourseUc } from '../uc'; import { CourseInfoDto } from '../uc/dto'; import { CourseFilterParams } from './dto/request/course-filter-params'; import { CourseSortParams } from './dto/request/course-sort-params'; import { CourseInfoListResponse } from './dto/response'; +import { CourseInfoUc } from '../uc/course-info.uc'; @ApiTags('Course Info') @JwtAuthentication() @Controller('course-info') export class CourseInfoController { - constructor(private readonly courseUc: CourseUc) {} + constructor(private readonly courseInfoUc: CourseInfoUc) {} @Get() @ApiOperation({ summary: 'Get a list of courses for school.' }) @@ -28,7 +28,7 @@ export class CourseInfoController { @Query() sortingQuery: CourseSortParams, @Query() filterParams: CourseFilterParams ): Promise { - const courses: Page = await this.courseUc.findAllCourses( + const courses: Page = await this.courseInfoUc.getCourseInfo( currentUser.userId, currentUser.schoolId, sortingQuery.sortBy, diff --git a/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts b/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts index f9fc6a74020..e1c63920c92 100644 --- a/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts +++ b/apps/server/src/modules/learnroom/domain/interface/course.repo.interface.ts @@ -1,4 +1,5 @@ import type { Group } from '@modules/group'; +import { Page } from '@shared/domain/domainobject'; import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { BaseDomainObjectRepoInterface } from '@shared/repo/base-domain-object.repo.interface'; @@ -10,7 +11,7 @@ export interface CourseRepo extends BaseDomainObjectRepoInterface { findBySyncedGroup(group: Group): Promise; - findCourses(filter: CourseFilter, options?: IFindOptions): Promise; + getCourseInfo(filter: CourseFilter, options?: IFindOptions): Promise>; } export const COURSE_REPO = Symbol('COURSE_REPO'); diff --git a/apps/server/src/modules/learnroom/learnroom-api.module.ts b/apps/server/src/modules/learnroom/learnroom-api.module.ts index 55caa8bc3bc..3c565127dab 100644 --- a/apps/server/src/modules/learnroom/learnroom-api.module.ts +++ b/apps/server/src/modules/learnroom/learnroom-api.module.ts @@ -10,9 +10,9 @@ import { SchoolModule } from '@modules/school'; import { UserModule } from '@modules/user'; import { Module } from '@nestjs/common'; import { CourseRepo, DashboardModelMapper, DashboardRepo, LegacyBoardRepo, UserRepo } from '@shared/repo'; +import { CourseRoomsController } from './controller/course-rooms.controller'; import { CourseController } from './controller/course.controller'; import { DashboardController } from './controller/dashboard.controller'; -import { CourseRoomsController } from './controller/course-rooms.controller'; import { LearnroomModule } from './learnroom.module'; import { RoomBoardResponseMapper } from './mapper/room-board-response.mapper'; @@ -21,13 +21,14 @@ import { CourseCopyUC, CourseExportUc, CourseImportUc, + CourseInfoUc, + CourseRoomsAuthorisationService, + CourseRoomsUc, CourseSyncUc, CourseUc, DashboardUc, LessonCopyUC, RoomBoardDTOFactory, - CourseRoomsAuthorisationService, - CourseRoomsUc, } from './uc'; /** @@ -51,6 +52,7 @@ import { providers: [ DashboardUc, CourseUc, + CourseInfoUc, CourseRoomsUc, RoomBoardResponseMapper, RoomBoardDTOFactory, diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts index 599d263777a..80f41bc382c 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts @@ -197,9 +197,9 @@ describe(CourseMikroOrmRepo.name, () => { it('should return empty array', async () => { const { filter } = await setup(); - const result = await repo.findCourses(filter); + const result = await repo.getCourseInfo(filter); - expect(result).toEqual([]); + expect(result.data).toEqual([]); }); }); @@ -238,18 +238,20 @@ describe(CourseMikroOrmRepo.name, () => { it('should return archived courses', async () => { const { options, filter } = await setup(); - const result = await repo.findCourses(filter, options); + const result = await repo.getCourseInfo(filter, options); - expect(result.length).toEqual(5); + expect(result.data.length).toEqual(5); + expect(result.total).toEqual(5); }); it('should return current courses', async () => { const { options, filter } = await setup(); filter.courseStatusQueryType = CourseStatusQueryType.CURRENT; - const result = await repo.findCourses(filter, options); + const result = await repo.getCourseInfo(filter, options); - expect(result.length).toEqual(3); + expect(result.data.length).toEqual(3); + expect(result.total).toEqual(3); }); }); }); diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts index 0d66936dbbb..bccda6653ec 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts @@ -1,5 +1,6 @@ import { EntityData, EntityName, FindOptions } from '@mikro-orm/core'; import { Group } from '@modules/group'; +import { Page } from '@shared/domain/domainobject'; import { Course as CourseEntity } from '@shared/domain/entity'; import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; @@ -47,7 +48,7 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo): Promise { + public async getCourseInfo(filter: CourseFilter, options?: IFindOptions): Promise> { const scope: CourseScope = new CourseScope(); scope.bySchoolId(filter.schoolId); if (filter.courseStatusQueryType === CourseStatusQueryType.CURRENT) { @@ -58,8 +59,7 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo => { if (!entity.courseGroups.isInitialized()) { @@ -69,8 +69,9 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo CourseEntityMapper.mapEntityToDo(entity)); + const page: Page = new Page(courses, total); - return courses; + return page; } private mapToMikroOrmOptions

(options?: IFindOptions): FindOptions { diff --git a/apps/server/src/modules/learnroom/service/course-do.service.spec.ts b/apps/server/src/modules/learnroom/service/course-do.service.spec.ts index 3a9a0f97f0a..7bf631befa9 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.spec.ts @@ -3,6 +3,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Group } from '@modules/group'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; +import { Page } from '@shared/domain/domainobject'; import { IFindOptions, SortOrder } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { groupFactory } from '@shared/testing'; @@ -241,7 +242,7 @@ describe(CourseDoService.name, () => { }, }; - courseRepo.findCourses.mockResolvedValueOnce(courses); + courseRepo.getCourseInfo.mockResolvedValueOnce(new Page(courses, 5)); return { courses, @@ -253,9 +254,9 @@ describe(CourseDoService.name, () => { it('should return the courses by passing filter and options', async () => { const { courses, filter, options } = setup(); - const result: Course[] = await service.findCourses(filter, options); + const result: Page = await service.getCourseInfo(filter, options); - expect(result).toEqual(courses); + expect(result.data).toEqual(courses); }); }); }); diff --git a/apps/server/src/modules/learnroom/service/course-do.service.ts b/apps/server/src/modules/learnroom/service/course-do.service.ts index 46c8ed2e476..951693dee25 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.ts @@ -1,6 +1,7 @@ import { AuthorizationLoaderServiceGeneric } from '@modules/authorization'; import { type Group } from '@modules/group'; import { Inject, Injectable } from '@nestjs/common'; +import { Page } from '@shared/domain/domainobject'; import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { @@ -54,8 +55,8 @@ export class CourseDoService implements AuthorizationLoaderServiceGeneric): Promise { - const courses = await this.courseRepo.findCourses(filter, options); + public async getCourseInfo(filter: CourseFilter, options?: IFindOptions): Promise> { + const courses = await this.courseRepo.getCourseInfo(filter, options); return courses; } diff --git a/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts new file mode 100644 index 00000000000..c6984d2236d --- /dev/null +++ b/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts @@ -0,0 +1,215 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AuthorizationService } from '@modules/authorization'; +import { ClassService } from '@modules/class'; +import { classFactory } from '@modules/class/domain/testing'; +import { ClassesRepo } from '@modules/class/repo'; +import { GroupService } from '@modules/group'; +import { GroupRepo } from '@modules/group/repo/'; +import { SchoolService } from '@modules/school'; +import { schoolFactory } from '@modules/school/testing'; +import { UserService } from '@modules/user'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Page } from '@shared/domain/domainobject'; +import { Permission, RoleName, SortOrder } from '@shared/domain/interface'; +import { groupFactory, setupEntities, UserAndAccountTestFactory, userDoFactory, userFactory } from '@shared/testing'; +import { Course, COURSE_REPO, CourseRepo as CourseDORepo, CourseSortQueryType, CourseStatusQueryType } from '../domain'; +import { CourseDoService } from '../service'; +import { courseFactory as courseDoFactory } from '../testing'; +import { CourseInfoUc } from './course-info.uc'; +import { CourseInfoDto } from './dto'; + +describe('CourseInfoUc', () => { + let module: TestingModule; + let uc: CourseInfoUc; + let groupRepo: DeepMocked; + let classesRepo: DeepMocked; + + let authorizationService: DeepMocked; + let schoolService: DeepMocked; + let courseDoService: DeepMocked; + let groupService: DeepMocked; + let userService: DeepMocked; + let classService: DeepMocked; + + beforeAll(async () => { + await setupEntities(); + module = await Test.createTestingModule({ + providers: [ + CourseInfoUc, + { + provide: AuthorizationService, + useValue: createMock(), + }, + { + provide: SchoolService, + useValue: createMock(), + }, + + { + provide: CourseDoService, + useValue: createMock(), + }, + { + provide: COURSE_REPO, + useValue: createMock(), + }, + { + provide: GroupService, + useValue: createMock(), + }, + { + provide: GroupRepo, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + { + provide: ClassService, + useValue: createMock(), + }, + { + provide: ClassesRepo, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(CourseInfoUc); + authorizationService = module.get(AuthorizationService); + schoolService = module.get(SchoolService); + courseDoService = module.get(CourseDoService); + groupService = module.get(GroupService); + userService = module.get(UserService); + classService = module.get(ClassService); + groupRepo = module.get(GroupRepo); + classesRepo = module.get(ClassesRepo); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('getCourseInfo', () => { + const setup = () => { + const user = userFactory.withRoleByName(RoleName.TEACHER).buildWithId(); + const teacher = userDoFactory.build({ id: user.id, firstName: 'firstName', lastName: 'lastName' }); + const { adminUser } = UserAndAccountTestFactory.buildAdmin({}, [ + Permission.COURSE_ADMINISTRATION, + Permission.ADMIN_VIEW, + ]); + const group = groupFactory.build({ name: 'groupName' }); + const clazz = classFactory.build({ name: 'A', gradeLevel: 1 }); + + const courses = courseDoFactory.buildList(5, { + syncedWithGroup: group.id, + teacherIds: [user.id], + groupIds: [group.id], + classIds: [clazz.id], + }); + const pagination = { skip: 1, limit: 2 }; + const courseStatusQueryType: CourseStatusQueryType = CourseStatusQueryType.CURRENT; + const sortByField: CourseSortQueryType = CourseSortQueryType.NAME; + const sortOrder: SortOrder = SortOrder.asc; + + const school = schoolFactory.build(); + schoolService.getSchoolById.mockResolvedValueOnce(school); + authorizationService.getUserWithPermissions.mockResolvedValue(adminUser); + authorizationService.checkPermission.mockReturnValueOnce(undefined); + groupRepo.findGroupById.mockResolvedValue(group); + groupService.findById.mockResolvedValue(group); + userService.findById.mockResolvedValue(teacher); + classService.findById.mockResolvedValue(clazz); + classesRepo.findClassById.mockResolvedValue(clazz); + + return { + user, + courses, + pagination, + school, + adminUser, + group, + courseStatusQueryType, + sortByField, + sortOrder, + clazz, + }; + }; + + it('should return courses with sorted and filtered results', async () => { + const { + courses, + clazz, + group, + school, + adminUser, + sortByField, + courseStatusQueryType: statusTypeQuery, + pagination, + sortOrder, + user, + } = setup(); + courseDoService.getCourseInfo.mockResolvedValueOnce(new Page(courses, 5)); + + const result: Page = await uc.getCourseInfo( + adminUser.id, + school.id, + sortByField, + statusTypeQuery, + pagination, + sortOrder + ); + + const filter = { schoolId: school.id, courseStatusQueryType: statusTypeQuery }; + const options = { + pagination, + order: { + [sortByField]: sortOrder, + }, + }; + + expect(schoolService.getSchoolById).toHaveBeenCalledWith(school.id); + expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(adminUser.id); + expect(authorizationService.checkPermission).toHaveBeenCalled(); + expect(courseDoService.getCourseInfo).toHaveBeenCalledWith(filter, options); + expect(userService.findById).toHaveBeenCalledWith(user.id); + expect(classService.findById).toHaveBeenCalledWith(clazz.id); + expect(groupService.findById).toHaveBeenCalledWith(group.id); + expect(result.total).toBe(5); + expect(result.data.length).toBe(5); + expect(result.data[0].classes).toStrictEqual(['1A', 'groupName']); + expect(result.data[0].teachers).toStrictEqual(['firstName lastName']); + }); + + it('should return an empty page if no courses are found', async () => { + const { adminUser, school, courseStatusQueryType: statusTypeQuery, sortByField, pagination, sortOrder } = setup(); + + courseDoService.getCourseInfo.mockResolvedValueOnce(new Page([], 0)); + + const result = await uc.getCourseInfo( + adminUser.id, + school.id, + sortByField, + statusTypeQuery, + pagination, + sortOrder + ); + + expect(result.total).toBe(0); + expect(result.data.length).toBe(0); + }); + + it('should handle empty data inputs', async () => { + const { adminUser, school } = setup(); + + courseDoService.getCourseInfo.mockResolvedValueOnce(new Page([], 0)); + + const result = await uc.getCourseInfo(adminUser.id, school.id, undefined, undefined, undefined, undefined); + + expect(schoolService.getSchoolById).toHaveBeenCalledWith(school.id); + expect(result.total).toBe(0); + expect(result.data.length).toBe(0); + }); + }); +}); diff --git a/apps/server/src/modules/learnroom/uc/course-info.uc.ts b/apps/server/src/modules/learnroom/uc/course-info.uc.ts new file mode 100644 index 00000000000..95223e539cb --- /dev/null +++ b/apps/server/src/modules/learnroom/uc/course-info.uc.ts @@ -0,0 +1,118 @@ +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { ClassService } from '@modules/class'; +import { Group, GroupService } from '@modules/group'; +import { School, SchoolService } from '@modules/school'; +import { UserService } from '@modules/user'; +import { Injectable } from '@nestjs/common'; +import { Page, UserDO } from '@shared/domain/domainobject'; +import { User } from '@shared/domain/entity'; +import { IFindOptions, Pagination, Permission, SortOrder, SortOrderMap } from '@shared/domain/interface'; +import { EntityId } from '@shared/domain/types'; +import { Course as CourseDO } from '../domain'; +import { CourseFilter, CourseSortQueryType, CourseStatusQueryType } from '../domain/interface'; +import { CourseDoService } from '../service'; +import { CourseInfoDto } from './dto'; + +@Injectable() +export class CourseInfoUc { + public constructor( + private readonly authService: AuthorizationService, + private readonly schoolService: SchoolService, + private readonly courseDoService: CourseDoService, + private readonly groupService: GroupService, + private readonly userService: UserService, + private readonly classService: ClassService + ) {} + + public async getCourseInfo( + userId: EntityId, + schoolId: EntityId, + sortByField: CourseSortQueryType = CourseSortQueryType.NAME, + courseStatusQueryType?: CourseStatusQueryType, + pagination?: Pagination, + sortOrder: SortOrder = SortOrder.asc + ): Promise> { + const school: School = await this.schoolService.getSchoolById(schoolId); + + const user: User = await this.authService.getUserWithPermissions(userId); + this.authService.checkPermission(user, school, AuthorizationContextBuilder.read([Permission.ADMIN_VIEW])); + + const order: SortOrderMap = { [sortByField]: sortOrder }; + const filter: CourseFilter = { schoolId, courseStatusQueryType }; + const options: IFindOptions = { pagination, order }; + const courses: Page = await this.courseDoService.getCourseInfo(filter, options); + let page: Page = new Page([], courses.total); + + if (courses.total === 0) { + return page; + } + const resolvedCourses: CourseInfoDto[] = await this.getCourseData(courses.data); + + page = new Page(resolvedCourses, courses.total); + + return page; + } + + private async getCourseData(courses: CourseDO[]): Promise { + const courseInfos: CourseInfoDto[] = await Promise.all( + courses.map(async (course) => { + const groupName = course.syncedWithGroup ? await this.getSyncedGroupName(course.syncedWithGroup) : undefined; + const teachers: string[] = await this.getCourseTeachers(course.teachers); + const classes: string[] = await this.getCourseClasses(course.classes); + const groups: string[] = await this.getCourseGroups(course.groups); + + const mapped = new CourseInfoDto({ + id: course.id, + name: course.name, + classes: [...classes, ...groups], + teachers, + syncedWithGroup: groupName, + }); + + return mapped; + }) + ); + + return courseInfos; + } + + private async getSyncedGroupName(groupId: EntityId): Promise { + const group: Group = await this.groupService.findById(groupId); + + return group.name; + } + + private async getCourseTeachers(teacherIds: EntityId[]): Promise { + const teacherNames: string[] = await Promise.all( + teacherIds.map(async (teacherId): Promise => { + const teacher: UserDO = await this.userService.findById(teacherId); + const fullName = teacher.firstName.concat(' ').concat(teacher.lastName); + + return fullName; + }) + ); + return teacherNames; + } + + private async getCourseClasses(classIds: EntityId[]): Promise { + const classes: string[] = await Promise.all[]>( + classIds.map(async (classId): Promise => { + const clazz = await this.classService.findById(classId); + + return clazz.gradeLevel ? clazz.gradeLevel?.toString().concat(clazz.name) : clazz.name; + }) + ); + return classes; + } + + private async getCourseGroups(groupIds: EntityId[]): Promise { + const groups: string[] = await Promise.all[]>( + groupIds.map(async (groupId): Promise => { + const group = await this.groupService.findById(groupId); + + return group.name; + }) + ); + return groups; + } +} diff --git a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts index a183b7d95b6..17dbd918dc7 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts @@ -1,49 +1,20 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AuthorizationService } from '@modules/authorization'; -import { ClassService } from '@modules/class'; -import { classFactory } from '@modules/class/domain/testing'; -import { ClassesRepo } from '@modules/class/repo'; -import { GroupService } from '@modules/group'; -import { GroupRepo } from '@modules/group/repo/'; import { RoleDto, RoleService } from '@modules/role'; -import { SchoolService } from '@modules/school'; -import { schoolFactory } from '@modules/school/testing'; -import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; -import { Page, UserDO } from '@shared/domain/domainobject'; -import { User } from '@shared/domain/entity'; import { Permission, RoleName, SortOrder } from '@shared/domain/interface'; import { CourseRepo } from '@shared/repo'; -import { - courseFactory, - groupFactory, - setupEntities, - UserAndAccountTestFactory, - userDoFactory, - userFactory, -} from '@shared/testing'; -import { COURSE_REPO, CourseRepo as CourseDORepo, CourseSortQueryType, CourseStatusQueryType } from '../domain'; -import { CourseDoService, CourseService } from '../service'; -import { courseFactory as courseDoFactory } from '../testing'; +import { courseFactory, setupEntities, UserAndAccountTestFactory } from '@shared/testing'; +import { CourseService } from '../service'; import { CourseUc } from './course.uc'; -import { CourseInfoDto } from './dto'; describe('CourseUc', () => { let module: TestingModule; let uc: CourseUc; let courseRepo: DeepMocked; - let courseDORepo: DeepMocked; - let groupRepo: DeepMocked; - let classesRepo: DeepMocked; - let courseService: DeepMocked; let authorizationService: DeepMocked; let roleService: DeepMocked; - let schoolService: DeepMocked; - let courseDoService: DeepMocked; - let groupService: DeepMocked; - let userService: DeepMocked; - let classService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -66,39 +37,6 @@ describe('CourseUc', () => { provide: RoleService, useValue: createMock(), }, - { - provide: SchoolService, - useValue: createMock(), - }, - - { - provide: CourseDoService, - useValue: createMock(), - }, - { - provide: COURSE_REPO, - useValue: createMock(), - }, - { - provide: GroupService, - useValue: createMock(), - }, - { - provide: GroupRepo, - useValue: createMock(), - }, - { - provide: UserService, - useValue: createMock(), - }, - { - provide: ClassService, - useValue: createMock(), - }, - { - provide: ClassesRepo, - useValue: createMock(), - }, ], }).compile(); @@ -107,14 +45,6 @@ describe('CourseUc', () => { courseService = module.get(CourseService); authorizationService = module.get(AuthorizationService); roleService = module.get(RoleService); - schoolService = module.get(SchoolService); - courseDoService = module.get(CourseDoService); - groupService = module.get(GroupService); - userService = module.get(UserService); - classService = module.get(ClassService); - courseDORepo = module.get(COURSE_REPO); - groupRepo = module.get(GroupRepo); - classesRepo = module.get(ClassesRepo); }); afterAll(async () => { @@ -189,96 +119,4 @@ describe('CourseUc', () => { expect(courseService.findById).toHaveBeenCalledWith(course.id); }); }); - - describe('findAllCourses', () => { - const setup = () => { - const user: User = userFactory.withRoleByName(RoleName.TEACHER).buildWithId(); - const teacher: UserDO = userDoFactory.build({ id: user.id, firstName: 'firstName', lastName: 'lastName' }); - const { adminUser } = UserAndAccountTestFactory.buildAdmin({}, [ - Permission.COURSE_ADMINISTRATION, - Permission.ADMIN_VIEW, - ]); - const group = groupFactory.build({ name: 'groupName' }); - const clazz = classFactory.build({ name: 'A', gradeLevel: 1 }); - - const courses = courseDoFactory.buildList(5, { - syncedWithGroup: group.id, - teacherIds: [user.id], - groupIds: [group.id], - classIds: [clazz.id], - }); - const pagination = { skip: 1, limit: 2 }; - const courseStatusQueryType: CourseStatusQueryType = CourseStatusQueryType.CURRENT; - const sortByField: CourseSortQueryType = CourseSortQueryType.NAME; - const sortOrder: SortOrder = SortOrder.asc; - - const school = schoolFactory.build(); - schoolService.getSchoolById.mockResolvedValueOnce(school); - authorizationService.getUserWithPermissions.mockResolvedValue(adminUser); - authorizationService.checkPermission.mockReturnValueOnce(undefined); - courseDORepo.findCourses.mockResolvedValueOnce(courses); - courseDoService.findCourses.mockResolvedValueOnce(courses); - groupRepo.findGroupById.mockResolvedValue(group); - groupService.findById.mockResolvedValue(group); - userService.findById.mockResolvedValue(teacher); - groupService.findById.mockResolvedValue(group); - classService.findById.mockResolvedValue(clazz); - classesRepo.findClassById.mockResolvedValue(clazz); - - return { - user, - courses, - pagination, - school, - adminUser, - group, - courseStatusQueryType, - sortByField, - sortOrder, - clazz, - }; - }; - it('should return courses of user', async () => { - const { - clazz, - group, - school, - adminUser, - sortByField, - courseStatusQueryType: statusTypeQuery, - pagination, - sortOrder, - user, - } = setup(); - - const result: Page = await uc.findAllCourses( - adminUser.id, - school.id, - sortByField, - statusTypeQuery, - pagination, - sortOrder - ); - - const filter = { schoolId: school.id, courseStatusQueryType: statusTypeQuery }; - const options = { - pagination, - order: { - [sortByField]: sortOrder, - }, - }; - - expect(schoolService.getSchoolById).toHaveBeenCalledWith(school.id); - expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(adminUser.id); - expect(authorizationService.checkPermission).toHaveBeenCalled(); - expect(courseDoService.findCourses).toHaveBeenCalledWith(filter, options); - expect(userService.findById).toHaveBeenCalledWith(user.id); - expect(classService.findById).toHaveBeenCalledWith(clazz.id); - expect(groupService.findById).toHaveBeenCalledWith(group.id); - expect(result.total).toBe(5); - expect(result.data.length).toBe(5); - expect(result.data[0].classes).toStrictEqual(['1A', 'groupName']); - expect(result.data[0].teachers).toStrictEqual(['firstName lastName']); - }); - }); }); diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index 0c875f23cfa..7d0dcd24a1a 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -1,21 +1,13 @@ -import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; -import { ClassService } from '@modules/class'; -import { Group, GroupService } from '@modules/group'; +import { AuthorizationService } from '@modules/authorization'; import { RoleService } from '@modules/role'; -import { School, SchoolService } from '@modules/school'; -import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; import { PaginationParams } from '@shared/controller/'; -import { Page, UserDO } from '@shared/domain/domainobject'; -import { Course as CourseEntity, User } from '@shared/domain/entity'; -import { IFindOptions, Pagination, Permission, SortOrder, SortOrderMap } from '@shared/domain/interface'; +import { Course as CourseEntity } from '@shared/domain/entity'; +import { SortOrder } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; -import { Course as CourseDO } from '../domain'; -import { CourseFilter, CourseSortQueryType, CourseStatusQueryType } from '../domain/interface'; import { RoleNameMapper } from '../mapper/rolename.mapper'; -import { CourseDoService, CourseService } from '../service'; -import { CourseInfoDto } from './dto'; +import { CourseService } from '../service'; @Injectable() export class CourseUc { @@ -23,12 +15,7 @@ export class CourseUc { private readonly courseRepo: CourseRepo, private readonly courseService: CourseService, private readonly authService: AuthorizationService, - private readonly roleService: RoleService, - private readonly schoolService: SchoolService, - private readonly courseDoService: CourseDoService, - private readonly groupService: GroupService, - private readonly userService: UserService, - private readonly classService: ClassService + private readonly roleService: RoleService ) {} public findAllByUser(userId: EntityId, options?: PaginationParams): Promise> { @@ -47,92 +34,4 @@ export class CourseUc { public async findCourseById(courseId: EntityId): Promise { return this.courseService.findById(courseId); } - - public async findAllCourses( - userId: EntityId, - schoolId: EntityId, - sortByField: CourseSortQueryType = CourseSortQueryType.NAME, - courseStatusQueryType?: CourseStatusQueryType, - pagination?: Pagination, - sortOrder: SortOrder = SortOrder.asc - ): Promise> { - const school: School = await this.schoolService.getSchoolById(schoolId); - - const user: User = await this.authService.getUserWithPermissions(userId); - this.authService.checkPermission(user, school, AuthorizationContextBuilder.read([Permission.ADMIN_VIEW])); - - const order: SortOrderMap = { [sortByField]: sortOrder }; - const filter: CourseFilter = { schoolId, courseStatusQueryType }; - const options: IFindOptions = { pagination, order }; - const courses: CourseDO[] = await this.courseDoService.findCourses(filter, options); - - const resolvedCourses: CourseInfoDto[] = await this.getCourseData(courses); - - const page: Page = new Page(resolvedCourses, courses.length); - - return page; - } - - private async getCourseData(courses: CourseDO[]): Promise { - const courseInfos: CourseInfoDto[] = await Promise.all( - courses.map(async (course) => { - const groupName = course.syncedWithGroup ? await this.getSyncedGroupName(course.syncedWithGroup) : undefined; - const teachers: string[] = await this.getCourseTeachers(course.teachers); - const classes: string[] = await this.getCourseClasses(course.classes); - const groups: string[] = await this.getCourseGroups(course.groups); - - const mapped = new CourseInfoDto({ - id: course.id, - name: course.name, - classes: [...classes, ...groups], - teachers, - syncedWithGroup: groupName, - }); - - return mapped; - }) - ); - - return courseInfos; - } - - private async getSyncedGroupName(groupId: EntityId): Promise { - const group: Group = await this.groupService.findById(groupId); - - return group.name; - } - - private async getCourseTeachers(teacherIds: EntityId[]): Promise { - const teacherNames: string[] = await Promise.all( - teacherIds.map(async (teacherId): Promise => { - const teacher: UserDO = await this.userService.findById(teacherId); - const fullName = teacher.firstName.concat(' ').concat(teacher.lastName); - - return fullName; - }) - ); - return teacherNames; - } - - private async getCourseClasses(classIds: EntityId[]): Promise { - const classes: string[] = await Promise.all[]>( - classIds.map(async (classId): Promise => { - const clazz = await this.classService.findById(classId); - - return clazz.gradeLevel ? clazz.gradeLevel?.toString().concat(clazz.name) : clazz.name; - }) - ); - return classes; - } - - private async getCourseGroups(groupIds: EntityId[]): Promise { - const groups: string[] = await Promise.all[]>( - groupIds.map(async (groupId): Promise => { - const group = await this.groupService.findById(groupId); - - return group.name; - }) - ); - return groups; - } } diff --git a/apps/server/src/modules/learnroom/uc/index.ts b/apps/server/src/modules/learnroom/uc/index.ts index c50d28e74d7..6c5de6d226a 100644 --- a/apps/server/src/modules/learnroom/uc/index.ts +++ b/apps/server/src/modules/learnroom/uc/index.ts @@ -2,6 +2,7 @@ export * from './course-copy.uc'; export * from './course-export.uc'; export * from './course-import.uc'; export * from './course-sync.uc'; +export * from './course-info.uc'; export * from './course.uc'; export * from './dashboard.uc'; export * from './lesson-copy.uc'; From bf6daedb4c33f258af1d89b93c45b317d59011e2 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Wed, 28 Aug 2024 15:21:59 +0200 Subject: [PATCH 53/64] clean up --- apps/server/src/modules/learnroom/domain/do/course.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/server/src/modules/learnroom/domain/do/course.ts b/apps/server/src/modules/learnroom/domain/do/course.ts index 49903e18a8c..a6e8eb84bce 100644 --- a/apps/server/src/modules/learnroom/domain/do/course.ts +++ b/apps/server/src/modules/learnroom/domain/do/course.ts @@ -81,10 +81,6 @@ export class Course extends DomainObject { this.props.untilDate = value; } - get untilDate(): Date | undefined { - return this.props.untilDate; - } - set syncedWithGroup(value: EntityId | undefined) { this.props.syncedWithGroup = value; } From 04d628b91cf78b56f4dfdd8ce0216ae7df0d6375 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Wed, 28 Aug 2024 15:40:21 +0200 Subject: [PATCH 54/64] clean up --- .../src/modules/learnroom/controller/dto/index.ts | 1 - .../learnroom/controller/dto/school.params.ts | 12 ------------ .../server/src/modules/learnroom/learnroom.module.ts | 1 - apps/server/src/modules/learnroom/uc/course.uc.ts | 6 +++--- 4 files changed, 3 insertions(+), 17 deletions(-) delete mode 100644 apps/server/src/modules/learnroom/controller/dto/school.params.ts diff --git a/apps/server/src/modules/learnroom/controller/dto/index.ts b/apps/server/src/modules/learnroom/controller/dto/index.ts index d06dba8fdf8..756a0f26429 100644 --- a/apps/server/src/modules/learnroom/controller/dto/index.ts +++ b/apps/server/src/modules/learnroom/controller/dto/index.ts @@ -14,4 +14,3 @@ export * from './course-room.url.params'; export * from './single-column-board'; export * from './course-sync.body.params'; export * from './course-export.body.params'; -export * from './school.params'; diff --git a/apps/server/src/modules/learnroom/controller/dto/school.params.ts b/apps/server/src/modules/learnroom/controller/dto/school.params.ts deleted file mode 100644 index dfd863ee1b5..00000000000 --- a/apps/server/src/modules/learnroom/controller/dto/school.params.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsMongoId } from 'class-validator'; - -export class SchoolParams { - @IsMongoId() - @ApiProperty({ - description: 'The id of the school', - required: true, - nullable: false, - }) - schoolId!: string; -} diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index 96c76839302..921d3df5e0a 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -115,7 +115,6 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; CourseService, CourseDoService, CourseRoomsService, - GroupService, CommonCartridgeExportService, CommonCartridgeImportService, CourseGroupService, diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index 7d0dcd24a1a..5f562c229e5 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -2,7 +2,7 @@ import { AuthorizationService } from '@modules/authorization'; import { RoleService } from '@modules/role'; import { Injectable } from '@nestjs/common'; import { PaginationParams } from '@shared/controller/'; -import { Course as CourseEntity } from '@shared/domain/entity'; +import { Course } from '@shared/domain/entity'; import { SortOrder } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; @@ -18,7 +18,7 @@ export class CourseUc { private readonly roleService: RoleService ) {} - public findAllByUser(userId: EntityId, options?: PaginationParams): Promise> { + public findAllByUser(userId: EntityId, options?: PaginationParams): Promise> { return this.courseRepo.findAllByUserId(userId, {}, { pagination: options, order: { updatedAt: SortOrder.desc } }); } @@ -31,7 +31,7 @@ export class CourseUc { return role.permissions ?? []; } - public async findCourseById(courseId: EntityId): Promise { + public async findCourseById(courseId: EntityId): Promise { return this.courseService.findById(courseId); } } From 11ab0846c9d67b5deb300372ec3d4bc93a03976d Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Wed, 28 Aug 2024 17:36:21 +0200 Subject: [PATCH 55/64] rename course info controller method --- .../src/modules/learnroom/controller/course-info.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/learnroom/controller/course-info.controller.ts b/apps/server/src/modules/learnroom/controller/course-info.controller.ts index 1f1c5808bd5..bbc1ddda87a 100644 --- a/apps/server/src/modules/learnroom/controller/course-info.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course-info.controller.ts @@ -22,7 +22,7 @@ export class CourseInfoController { @ApiResponse({ status: HttpStatus.OK, type: CourseInfoListResponse }) @ApiResponse({ status: '4XX', type: ErrorResponse }) @ApiResponse({ status: '5XX', type: ErrorResponse }) - async getCoursesForSchool( + async getCourseInfo( @CurrentUser() currentUser: ICurrentUser, @Query() pagination: PaginationParams, @Query() sortingQuery: CourseSortParams, From c1df6f4c8214c5c36a1834b8ffbac3b0949130a3 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Wed, 28 Aug 2024 18:00:57 +0200 Subject: [PATCH 56/64] update course info controller response summary --- .../src/modules/learnroom/controller/course-info.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/learnroom/controller/course-info.controller.ts b/apps/server/src/modules/learnroom/controller/course-info.controller.ts index bbc1ddda87a..ad4290ad64b 100644 --- a/apps/server/src/modules/learnroom/controller/course-info.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course-info.controller.ts @@ -18,7 +18,7 @@ export class CourseInfoController { constructor(private readonly courseInfoUc: CourseInfoUc) {} @Get() - @ApiOperation({ summary: 'Get a list of courses for school.' }) + @ApiOperation({ summary: 'Get course information.' }) @ApiResponse({ status: HttpStatus.OK, type: CourseInfoListResponse }) @ApiResponse({ status: '4XX', type: ErrorResponse }) @ApiResponse({ status: '5XX', type: ErrorResponse }) From 17a3a07b37756b3abd8e6dda9955390d4e3f1229 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Fri, 30 Aug 2024 14:52:06 +0200 Subject: [PATCH 57/64] update review changes --- .../domain/rules/course.rule.spec.ts | 42 +++- .../src/modules/class/domain/class.do.ts | 8 + .../class/domain/testing/class.do.spec.ts | 24 ++ .../modules/class/repo/classes.repo.spec.ts | 1 - .../api-test/course-info.api.spec.ts | 37 +-- .../controller/course-info.controller.ts | 2 +- .../learnroom/controller/course.controller.ts | 3 +- .../dto/request/course-filter-params.ts | 8 +- .../dto/request/course-sort-params.ts | 10 +- ...sponse.ts => course-info-data-response.ts} | 2 +- .../dto/response/course-info-list.response.ts | 2 +- .../controller/dto/response/index.ts | 2 +- .../src/modules/learnroom/domain/index.ts | 2 +- .../domain/interface/course-filter.ts | 4 +- .../interface/course-sort-props.enum.ts | 3 + .../interface/course-sort-query-type.enum.ts | 3 - ...ery-type.enum.ts => course-status.enum.ts} | 2 +- .../learnroom/domain/interface/index.ts | 4 +- .../src/modules/learnroom/learnroom.module.ts | 34 +-- .../mapper/course-info-response.mapper.ts | 2 +- .../mikro-orm/course.repo.integration.spec.ts | 8 +- .../learnroom/repo/mikro-orm/course.repo.ts | 6 +- .../learnroom/uc/course-info.uc.spec.ts | 229 +++++++++--------- .../modules/learnroom/uc/course-info.uc.ts | 36 ++- .../learnroom/uc/dto/course-info.dto.ts | 4 +- .../src/shared/repo/course/course.repo.ts | 66 +---- .../shared/repo/course/course.scope.spec.ts | 202 +++++++++++++++ .../src/shared/repo/course/course.scope.ts | 67 +++++ apps/server/src/shared/repo/course/index.ts | 2 + 29 files changed, 519 insertions(+), 296 deletions(-) rename apps/server/src/modules/learnroom/controller/dto/response/{courseInfoDataResponse.ts => course-info-data-response.ts} (94%) create mode 100644 apps/server/src/modules/learnroom/domain/interface/course-sort-props.enum.ts delete mode 100644 apps/server/src/modules/learnroom/domain/interface/course-sort-query-type.enum.ts rename apps/server/src/modules/learnroom/domain/interface/{course-status-query-type.enum.ts => course-status.enum.ts} (56%) create mode 100644 apps/server/src/shared/repo/course/course.scope.spec.ts create mode 100644 apps/server/src/shared/repo/course/course.scope.ts diff --git a/apps/server/src/modules/authorization/domain/rules/course.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/course.rule.spec.ts index d66b7856ca9..46d616cf45b 100644 --- a/apps/server/src/modules/authorization/domain/rules/course.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/course.rule.spec.ts @@ -8,6 +8,7 @@ import { Action } from '../type'; import { CourseRule } from './course.rule'; describe('CourseRule', () => { + let module: TestingModule; let service: CourseRule; let authorizationHelper: AuthorizationHelper; let user: User; @@ -19,7 +20,7 @@ describe('CourseRule', () => { beforeAll(async () => { await setupEntities(); - const module: TestingModule = await Test.createTestingModule({ + module = await Test.createTestingModule({ providers: [AuthorizationHelper, CourseRule], }).compile(); @@ -32,12 +33,20 @@ describe('CourseRule', () => { user = userFactory.build({ roles: [role] }); }); + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + describe('when validating an entity', () => { it('should call hasAllPermissions on AuthorizationHelper', () => { entity = courseEntityFactory.build({ teachers: [user] }); const spy = jest.spyOn(authorizationHelper, 'hasAllPermissions'); service.hasPermission(user, entity, { action: Action.read, requiredPermissions: [] }); - expect(spy).toBeCalledWith(user, []); + expect(spy).toHaveBeenCalledWith(user, []); }); it('should call hasAccessToEntity on AuthorizationHelper if action = "read"', () => { @@ -73,6 +82,35 @@ describe('CourseRule', () => { }); }); + describe('when validating an entity and the user has COURSE_ADMINISTRATION permission', () => { + const setup = () => { + const permissionD = Permission.COURSE_ADMINISTRATION; + const adminRole = roleFactory.build({ permissions: [permissionD] }); + const adminUser = userFactory.build({ roles: [adminRole] }); + + return { + adminUser, + permissionD, + }; + }; + + it('should call hasAllPermissions with admin permissions on AuthorizationHelper', () => { + const { permissionD, adminUser } = setup(); + entity = courseEntityFactory.build(); + const spy = jest.spyOn(authorizationHelper, 'hasAllPermissions'); + service.hasPermission(adminUser, entity, { action: Action.read, requiredPermissions: [] }); + expect(spy).toHaveBeenNthCalledWith(2, adminUser, [permissionD]); + }); + + it('should not call hasAccessToEntity on AuthorizationHelper', () => { + const { adminUser } = setup(); + entity = courseEntityFactory.build(); + const spy = jest.spyOn(authorizationHelper, 'hasAccessToEntity'); + service.hasPermission(adminUser, entity, { action: Action.read, requiredPermissions: [] }); + expect(spy).toHaveBeenCalledTimes(0); + }); + }); + describe('when validating a domain object', () => { describe('when the user is authorized', () => { const setup = () => { diff --git a/apps/server/src/modules/class/domain/class.do.ts b/apps/server/src/modules/class/domain/class.do.ts index fd6449a9d46..68a7e621947 100644 --- a/apps/server/src/modules/class/domain/class.do.ts +++ b/apps/server/src/modules/class/domain/class.do.ts @@ -74,4 +74,12 @@ export class Class extends DomainObject { public removeUser(userId: string) { this.props.userIds = this.props.userIds?.filter((userId1) => userId1 !== userId); } + + public getClassFullName(): string { + const classFullName = this.props.gradeLevel + ? this.props.gradeLevel.toString().concat(this.props.name) + : this.props.name; + + return classFullName; + } } diff --git a/apps/server/src/modules/class/domain/testing/class.do.spec.ts b/apps/server/src/modules/class/domain/testing/class.do.spec.ts index 510af786665..7ff616ab4ba 100644 --- a/apps/server/src/modules/class/domain/testing/class.do.spec.ts +++ b/apps/server/src/modules/class/domain/testing/class.do.spec.ts @@ -86,4 +86,28 @@ describe(Class.name, () => { }); }); }); + + describe('getClassFullName', () => { + describe('When function is called', () => { + it('should return full class name consisting of grade level and class name', () => { + const gradeLevel = 1; + const name = 'A'; + const domainObject = classFactory.build({ name, gradeLevel }); + + const result = domainObject.getClassFullName(); + + expect(result).toEqual('1A'); + }); + + it('should return full class name consisting of class name only', () => { + const gradeLevel = undefined; + const name = 'A'; + const domainObject = classFactory.build({ name, gradeLevel }); + + const result = domainObject.getClassFullName(); + + expect(result).toEqual('A'); + }); + }); + }); }); diff --git a/apps/server/src/modules/class/repo/classes.repo.spec.ts b/apps/server/src/modules/class/repo/classes.repo.spec.ts index 79b675da3d3..dbb58fb5b44 100644 --- a/apps/server/src/modules/class/repo/classes.repo.spec.ts +++ b/apps/server/src/modules/class/repo/classes.repo.spec.ts @@ -190,7 +190,6 @@ describe(ClassesRepo.name, () => { describe('when class is in classes', () => { const setup = async () => { const class1: ClassEntity = classEntityFactory.buildWithId(); - console.log(class1.id); await em.persistAndFlush([class1]); em.clear(); diff --git a/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts index f26689f9a27..602dd6c8461 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course-info.api.spec.ts @@ -11,7 +11,7 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { CourseSortQueryType, CourseStatusQueryType } from '../../domain'; +import { CourseSortProps, CourseStatus } from '../../domain'; import { CourseInfoListResponse } from '../dto/response'; const createStudent = () => { @@ -102,7 +102,7 @@ describe('Course Info Controller (API)', () => { it('should return archived courses in pages', async () => { const { admin } = await setup(); - const query = { skip: 0, limit: 10, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.ARCHIVE }; + const query = { skip: 0, limit: 10, sortBy: CourseSortProps.NAME, status: CourseStatus.ARCHIVE }; const loggedInClient = await testApiClient.login(admin.account); const response = await loggedInClient.get().query(query); @@ -117,7 +117,7 @@ describe('Course Info Controller (API)', () => { it('should return current courses in pages', async () => { const { admin, currentCourses } = await setup(); - const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; + const query = { skip: 4, limit: 2, sortBy: CourseSortProps.NAME, status: CourseStatus.CURRENT }; const loggedInClient = await testApiClient.login(admin.account); const response = await loggedInClient.get().query(query); @@ -132,20 +132,9 @@ describe('Course Info Controller (API)', () => { }); }); - describe('when logged in not authenticated/authorized', () => { - const setup = async () => { - const teacher = createTeacher(); - - await em.persistAndFlush([teacher.account, teacher.user]); - em.clear(); - - return { - teacher, - }; - }; - + describe('when not authorized', () => { it('should return unauthorized', async () => { - const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; + const query = {}; const response = await testApiClient.get().query(query); @@ -157,22 +146,6 @@ describe('Course Info Controller (API)', () => { type: 'UNAUTHORIZED', }); }); - - it('should return forbidden', async () => { - const { teacher } = await setup(); - const query = { skip: 4, limit: 2, sortBy: CourseSortQueryType.NAME, type: CourseStatusQueryType.CURRENT }; - - const loggedInClient = await testApiClient.login(teacher.account); - const response = await loggedInClient.get().query(query); - - expect(response.status).toEqual(HttpStatus.FORBIDDEN); - expect(response.body).toEqual({ - code: HttpStatus.FORBIDDEN, - message: 'Forbidden', - title: 'Forbidden', - type: 'FORBIDDEN', - }); - }); }); }); }); diff --git a/apps/server/src/modules/learnroom/controller/course-info.controller.ts b/apps/server/src/modules/learnroom/controller/course-info.controller.ts index ad4290ad64b..38a4355f7d8 100644 --- a/apps/server/src/modules/learnroom/controller/course-info.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course-info.controller.ts @@ -32,7 +32,7 @@ export class CourseInfoController { currentUser.userId, currentUser.schoolId, sortingQuery.sortBy, - filterParams.type, + filterParams.status, pagination, sortingQuery.sortOrder ); diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 1e8febcb56c..7d20c2f5292 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -120,7 +120,8 @@ export class CourseController { @Post(':courseId/start-sync/') @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ summary: 'Start the synchronization of a course with a group.' }) - @ApiNoContentResponse({ description: 'The course was successfully connected to a group.' }) + @ApiNoContentResponse({ description: 'The course was successfully synchronized to a group.' }) + @ApiUnprocessableEntityResponse({ description: 'The course is already synchronized with a group.' }) public async startSynchronization( @CurrentUser() currentUser: ICurrentUser, @Param() params: CourseUrlParams, diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts index ed2e8c27e00..d1e8ede12cf 100644 --- a/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts +++ b/apps/server/src/modules/learnroom/controller/dto/request/course-filter-params.ts @@ -1,10 +1,10 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; -import { CourseStatusQueryType } from '../../../domain'; +import { CourseStatus } from '../../../domain'; export class CourseFilterParams { @IsOptional() - @IsEnum(CourseStatusQueryType) - @ApiPropertyOptional({ enum: CourseStatusQueryType, enumName: 'CourseStatusQueryType' }) - type?: CourseStatusQueryType; + @IsEnum(CourseStatus) + @ApiPropertyOptional({ enum: CourseStatus, enumName: 'CourseStatus' }) + status?: CourseStatus; } diff --git a/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts b/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts index a59a27c4ebd..eef900bb569 100644 --- a/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts +++ b/apps/server/src/modules/learnroom/controller/dto/request/course-sort-params.ts @@ -1,11 +1,11 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { SortingParams } from '@shared/controller'; import { IsEnum, IsOptional } from 'class-validator'; -import { CourseSortQueryType } from '../../../domain'; +import { CourseSortProps } from '../../../domain/interface/course-sort-props.enum'; -export class CourseSortParams extends SortingParams { +export class CourseSortParams extends SortingParams { @IsOptional() - @IsEnum(CourseSortQueryType) - @ApiPropertyOptional({ enum: CourseSortQueryType, enumName: 'CourseSortQueryType' }) - sortBy?: CourseSortQueryType; + @IsEnum(CourseSortProps) + @ApiPropertyOptional({ enum: CourseSortProps, enumName: 'CourseSortProps' }) + sortBy?: CourseSortProps; } diff --git a/apps/server/src/modules/learnroom/controller/dto/response/courseInfoDataResponse.ts b/apps/server/src/modules/learnroom/controller/dto/response/course-info-data-response.ts similarity index 94% rename from apps/server/src/modules/learnroom/controller/dto/response/courseInfoDataResponse.ts rename to apps/server/src/modules/learnroom/controller/dto/response/course-info-data-response.ts index e506825a681..789c50e60d1 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/courseInfoDataResponse.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/course-info-data-response.ts @@ -11,7 +11,7 @@ export class CourseInfoDataResponse { @ApiProperty({ type: [String] }) teacherNames: string[]; - @ApiProperty() + @ApiProperty({ type: [String] }) classNames: string[]; @ApiPropertyOptional() diff --git a/apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts b/apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts index bab655ab3a7..d988fe45dbc 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/course-info-list.response.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { PaginationResponse } from '@shared/controller'; -import { CourseInfoDataResponse } from './courseInfoDataResponse'; +import { CourseInfoDataResponse } from './course-info-data-response'; export class CourseInfoListResponse extends PaginationResponse { constructor(data: CourseInfoDataResponse[], total: number, skip?: number, limit?: number) { diff --git a/apps/server/src/modules/learnroom/controller/dto/response/index.ts b/apps/server/src/modules/learnroom/controller/dto/response/index.ts index 45256ebbf13..dd0dbbc3a7f 100644 --- a/apps/server/src/modules/learnroom/controller/dto/response/index.ts +++ b/apps/server/src/modules/learnroom/controller/dto/response/index.ts @@ -1,2 +1,2 @@ export { CourseInfoListResponse } from './course-info-list.response'; -export { CourseInfoDataResponse } from './courseInfoDataResponse'; +export { CourseInfoDataResponse } from './course-info-data-response'; diff --git a/apps/server/src/modules/learnroom/domain/index.ts b/apps/server/src/modules/learnroom/domain/index.ts index 20e309382f1..e38999d9811 100644 --- a/apps/server/src/modules/learnroom/domain/index.ts +++ b/apps/server/src/modules/learnroom/domain/index.ts @@ -1,4 +1,4 @@ export { Course, CourseProps } from './do'; export { CourseAlreadySynchronizedLoggableException, CourseNotSynchronizedLoggableException } from './error'; -export { COURSE_REPO, CourseFilter, CourseRepo, CourseSortQueryType, CourseStatusQueryType } from './interface'; +export { COURSE_REPO, CourseFilter, CourseRepo, CourseSortProps, CourseStatus } from './interface'; export { CourseSynchronizationStoppedLoggable } from './loggable'; diff --git a/apps/server/src/modules/learnroom/domain/interface/course-filter.ts b/apps/server/src/modules/learnroom/domain/interface/course-filter.ts index 13adc757b96..6a1f93ac5dc 100644 --- a/apps/server/src/modules/learnroom/domain/interface/course-filter.ts +++ b/apps/server/src/modules/learnroom/domain/interface/course-filter.ts @@ -1,7 +1,7 @@ import { EntityId } from '@shared/domain/types'; -import { CourseStatusQueryType } from './course-status-query-type.enum'; +import { CourseStatus } from './course-status.enum'; export interface CourseFilter { schoolId?: EntityId; - courseStatusQueryType?: CourseStatusQueryType; + status?: CourseStatus; } diff --git a/apps/server/src/modules/learnroom/domain/interface/course-sort-props.enum.ts b/apps/server/src/modules/learnroom/domain/interface/course-sort-props.enum.ts new file mode 100644 index 00000000000..c2eb5eb5323 --- /dev/null +++ b/apps/server/src/modules/learnroom/domain/interface/course-sort-props.enum.ts @@ -0,0 +1,3 @@ +export enum CourseSortProps { + NAME = 'name', +} diff --git a/apps/server/src/modules/learnroom/domain/interface/course-sort-query-type.enum.ts b/apps/server/src/modules/learnroom/domain/interface/course-sort-query-type.enum.ts deleted file mode 100644 index 2b7b20bc958..00000000000 --- a/apps/server/src/modules/learnroom/domain/interface/course-sort-query-type.enum.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum CourseSortQueryType { - NAME = 'name', -} diff --git a/apps/server/src/modules/learnroom/domain/interface/course-status-query-type.enum.ts b/apps/server/src/modules/learnroom/domain/interface/course-status.enum.ts similarity index 56% rename from apps/server/src/modules/learnroom/domain/interface/course-status-query-type.enum.ts rename to apps/server/src/modules/learnroom/domain/interface/course-status.enum.ts index 8d419726c17..2866db339dc 100644 --- a/apps/server/src/modules/learnroom/domain/interface/course-status-query-type.enum.ts +++ b/apps/server/src/modules/learnroom/domain/interface/course-status.enum.ts @@ -1,4 +1,4 @@ -export enum CourseStatusQueryType { +export enum CourseStatus { ARCHIVE = 'archive', CURRENT = 'current', } diff --git a/apps/server/src/modules/learnroom/domain/interface/index.ts b/apps/server/src/modules/learnroom/domain/interface/index.ts index 67000ae6c12..a890705533d 100644 --- a/apps/server/src/modules/learnroom/domain/interface/index.ts +++ b/apps/server/src/modules/learnroom/domain/interface/index.ts @@ -1,4 +1,4 @@ export { CourseFilter } from './course-filter'; -export { CourseSortQueryType } from './course-sort-query-type.enum'; -export { CourseStatusQueryType } from './course-status-query-type.enum'; +export { CourseSortProps } from './course-sort-props.enum'; +export { CourseStatus } from './course-status.enum'; export { COURSE_REPO, CourseRepo } from './course.repo.interface'; diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index 921d3df5e0a..0adf76cacd0 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -1,18 +1,12 @@ -import { AccountModule } from '@modules/account'; import { BoardModule } from '@modules/board'; import { ClassModule } from '@modules/class'; import { CopyHelperModule } from '@modules/copy-helper'; -import { GroupService } from '@modules/group'; -import { GroupRepo } from '@modules/group/repo'; +import { GroupModule } from '@modules/group'; import { LessonModule } from '@modules/lesson'; -import { RegistrationPinModule } from '@modules/registration-pin'; -import { RoleModule } from '@modules/role'; -import { SchoolService } from '@modules/school'; -import { SCHOOL_REPO } from '@modules/school/domain/interface'; -import { SystemModule } from '@modules/system'; +import { SchoolModule } from '@modules/school'; import { TaskModule } from '@modules/task'; import { ContextExternalToolModule } from '@modules/tool/context-external-tool'; -import { UserModule, UserService } from '@modules/user'; +import { UserModule } from '@modules/user'; import { forwardRef, Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { @@ -24,11 +18,8 @@ import { LegacyBoardRepo, UserRepo, } from '@shared/repo'; -import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { LoggerModule } from '@src/core/logger'; -import { CalendarModule } from '../../infra/calendar'; import { BoardNodeRepo } from '../board/repo'; -import { SchoolMikroOrmRepo } from '../school/repo/mikro-orm/school.repo'; import { COURSE_REPO } from './domain'; import { CommonCartridgeExportMapper } from './mapper/common-cartridge-export.mapper'; import { CommonCartridgeImportMapper } from './mapper/common-cartridge-import.mapper'; @@ -41,10 +32,10 @@ import { CourseCopyService, CourseDoService, CourseGroupService, + CourseRoomsService, CourseService, DashboardService, GroupDeletedHandlerService, - CourseRoomsService, } from './service'; import { CommonCartridgeFileValidatorPipe } from './utils'; @@ -61,13 +52,10 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; LoggerModule, TaskModule, CqrsModule, - SystemModule, UserModule, - RoleModule, ClassModule, - AccountModule, - RegistrationPinModule, - CalendarModule, + SchoolModule, + GroupModule, ], providers: [ { @@ -91,8 +79,6 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; }, CourseService, CourseDoService, - GroupRepo, - GroupService, DashboardElementRepo, DashboardModelMapper, DashboardService, @@ -101,14 +87,6 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; UserRepo, GroupDeletedHandlerService, ColumnBoardNodeRepo, - SchoolService, - { - provide: SCHOOL_REPO, - useClass: SchoolMikroOrmRepo, - }, - UserService, - UserRepo, - UserDORepo, ], exports: [ CourseCopyService, diff --git a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts index 3274a0af251..34d2e468ebf 100644 --- a/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts +++ b/apps/server/src/modules/learnroom/mapper/course-info-response.mapper.ts @@ -28,7 +28,7 @@ export class CourseInfoResponseMapper { name: courseInfo.name, classNames: courseInfo.classes, teacherNames: courseInfo.teachers, - syncedGroup: courseInfo.syncedWithGroup ? courseInfo.syncedWithGroup : undefined, + syncedGroup: courseInfo.syncedGroupName, }); return courseInfoResponse; diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts index 80f41bc382c..194917950f1 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.integration.spec.ts @@ -17,7 +17,7 @@ import { schoolEntityFactory, userFactory, } from '@shared/testing'; -import { Course, COURSE_REPO, CourseProps, CourseStatusQueryType } from '../../domain'; +import { Course, COURSE_REPO, CourseProps, CourseStatus } from '../../domain'; import { courseFactory } from '../../testing'; import { CourseMikroOrmRepo } from './course.repo'; import { CourseEntityMapper } from './mapper/course.entity.mapper'; @@ -188,7 +188,7 @@ describe(CourseMikroOrmRepo.name, () => { await em.persistAndFlush([schoolEntity, ...courseEntities]); em.clear(); - const filter = { schoolId: schoolEntity.id, courseStatusQueryType: CourseStatusQueryType.ARCHIVE }; + const filter = { schoolId: schoolEntity.id, status: CourseStatus.ARCHIVE }; const courseDOs = courseEntities.map((courseEntity) => CourseEntityMapper.mapEntityToDo(courseEntity)); return { courseDOs, filter }; @@ -228,7 +228,7 @@ describe(CourseMikroOrmRepo.name, () => { name: SortOrder.desc, }, }; - const filter = { schoolId: schoolEntity.id, courseStatusQueryType: CourseStatusQueryType.ARCHIVE }; + const filter = { schoolId: schoolEntity.id, status: CourseStatus.ARCHIVE }; const courseDOs = courseEntities.map((courseEntity) => CourseEntityMapper.mapEntityToDo(courseEntity)); @@ -247,7 +247,7 @@ describe(CourseMikroOrmRepo.name, () => { it('should return current courses', async () => { const { options, filter } = await setup(); - filter.courseStatusQueryType = CourseStatusQueryType.CURRENT; + filter.status = CourseStatus.CURRENT; const result = await repo.getCourseInfo(filter, options); expect(result.data.length).toEqual(3); diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts index bccda6653ec..647d43aed26 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts @@ -4,9 +4,9 @@ import { Page } from '@shared/domain/domainobject'; import { Course as CourseEntity } from '@shared/domain/entity'; import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; +import { CourseScope } from '@shared/repo'; import { BaseDomainObjectRepo } from '@shared/repo/base-domain-object.repo'; -import { CourseScope } from '@shared/repo/course/course.repo'; -import { Course, CourseFilter, CourseRepo, CourseStatusQueryType } from '../../domain'; +import { Course, CourseFilter, CourseRepo, CourseStatus } from '../../domain'; import { CourseEntityMapper } from './mapper/course.entity.mapper'; export class CourseMikroOrmRepo extends BaseDomainObjectRepo implements CourseRepo { @@ -51,7 +51,7 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo): Promise> { const scope: CourseScope = new CourseScope(); scope.bySchoolId(filter.schoolId); - if (filter.courseStatusQueryType === CourseStatusQueryType.CURRENT) { + if (filter.status === CourseStatus.CURRENT) { scope.forActiveCourses(); } else { scope.forArchivedCourses(); diff --git a/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts index c6984d2236d..a9d5065b607 100644 --- a/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts @@ -1,4 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; import { AuthorizationService } from '@modules/authorization'; import { ClassService } from '@modules/class'; import { classFactory } from '@modules/class/domain/testing'; @@ -10,9 +11,9 @@ import { schoolFactory } from '@modules/school/testing'; import { UserService } from '@modules/user'; import { Test, TestingModule } from '@nestjs/testing'; import { Page } from '@shared/domain/domainobject'; -import { Permission, RoleName, SortOrder } from '@shared/domain/interface'; +import { IFindOptions, Permission, RoleName, SortOrder } from '@shared/domain/interface'; import { groupFactory, setupEntities, UserAndAccountTestFactory, userDoFactory, userFactory } from '@shared/testing'; -import { Course, COURSE_REPO, CourseRepo as CourseDORepo, CourseSortQueryType, CourseStatusQueryType } from '../domain'; +import { Course, CourseFilter, CourseSortProps, CourseStatus } from '../domain'; import { CourseDoService } from '../service'; import { courseFactory as courseDoFactory } from '../testing'; import { CourseInfoUc } from './course-info.uc'; @@ -44,15 +45,10 @@ describe('CourseInfoUc', () => { provide: SchoolService, useValue: createMock(), }, - { provide: CourseDoService, useValue: createMock(), }, - { - provide: COURSE_REPO, - useValue: createMock(), - }, { provide: GroupService, useValue: createMock(), @@ -92,124 +88,123 @@ describe('CourseInfoUc', () => { }); describe('getCourseInfo', () => { - const setup = () => { - const user = userFactory.withRoleByName(RoleName.TEACHER).buildWithId(); - const teacher = userDoFactory.build({ id: user.id, firstName: 'firstName', lastName: 'lastName' }); - const { adminUser } = UserAndAccountTestFactory.buildAdmin({}, [ - Permission.COURSE_ADMINISTRATION, - Permission.ADMIN_VIEW, - ]); - const group = groupFactory.build({ name: 'groupName' }); - const clazz = classFactory.build({ name: 'A', gradeLevel: 1 }); - - const courses = courseDoFactory.buildList(5, { - syncedWithGroup: group.id, - teacherIds: [user.id], - groupIds: [group.id], - classIds: [clazz.id], - }); - const pagination = { skip: 1, limit: 2 }; - const courseStatusQueryType: CourseStatusQueryType = CourseStatusQueryType.CURRENT; - const sortByField: CourseSortQueryType = CourseSortQueryType.NAME; - const sortOrder: SortOrder = SortOrder.asc; - - const school = schoolFactory.build(); - schoolService.getSchoolById.mockResolvedValueOnce(school); - authorizationService.getUserWithPermissions.mockResolvedValue(adminUser); - authorizationService.checkPermission.mockReturnValueOnce(undefined); - groupRepo.findGroupById.mockResolvedValue(group); - groupService.findById.mockResolvedValue(group); - userService.findById.mockResolvedValue(teacher); - classService.findById.mockResolvedValue(clazz); - classesRepo.findClassById.mockResolvedValue(clazz); - - return { - user, - courses, - pagination, - school, - adminUser, - group, - courseStatusQueryType, - sortByField, - sortOrder, - clazz, - }; - }; - - it('should return courses with sorted and filtered results', async () => { - const { - courses, - clazz, - group, - school, - adminUser, - sortByField, - courseStatusQueryType: statusTypeQuery, - pagination, - sortOrder, - user, - } = setup(); - courseDoService.getCourseInfo.mockResolvedValueOnce(new Page(courses, 5)); - - const result: Page = await uc.getCourseInfo( - adminUser.id, - school.id, - sortByField, - statusTypeQuery, - pagination, - sortOrder - ); - - const filter = { schoolId: school.id, courseStatusQueryType: statusTypeQuery }; - const options = { - pagination, - order: { - [sortByField]: sortOrder, - }, + describe('when courses are found', () => { + const setup = () => { + const user = userFactory.withRoleByName(RoleName.TEACHER).buildWithId(); + const teacher = userDoFactory.build({ id: user.id, firstName: 'firstName', lastName: 'lastName' }); + const { adminUser } = UserAndAccountTestFactory.buildAdmin({}, [ + Permission.COURSE_ADMINISTRATION, + Permission.ADMIN_VIEW, + ]); + const group = groupFactory.build({ name: 'groupName' }); + const clazz = classFactory.build({ name: 'A', gradeLevel: 1 }); + const courses = courseDoFactory.buildList(5, { + syncedWithGroup: group.id, + teacherIds: [user.id], + groupIds: [group.id], + classIds: [clazz.id], + }); + const pagination = { skip: 1, limit: 2 }; + const courseStatus: CourseStatus = CourseStatus.CURRENT; + const sortByField: CourseSortProps = CourseSortProps.NAME; + const sortOrder: SortOrder = SortOrder.asc; + const school = schoolFactory.build(); + + schoolService.getSchoolById.mockResolvedValueOnce(school); + authorizationService.getUserWithPermissions.mockResolvedValue(adminUser); + authorizationService.checkPermission.mockReturnValueOnce(undefined); + groupRepo.findGroupById.mockResolvedValue(group); + groupService.findById.mockResolvedValue(group); + userService.findById.mockResolvedValue(teacher); + classService.findById.mockResolvedValue(clazz); + classesRepo.findClassById.mockResolvedValue(clazz); + courseDoService.getCourseInfo.mockResolvedValueOnce(new Page(courses, 5)); + + return { + user, + courses, + pagination, + school, + adminUser, + group, + courseStatus, + sortByField, + sortOrder, + clazz, + }; }; + it('should return courses with sorted and filtered results', async () => { + const { clazz, group, school, adminUser, sortByField, courseStatus, pagination, sortOrder, user } = setup(); + + const result: Page = await uc.getCourseInfo( + adminUser.id, + school.id, + sortByField, + courseStatus, + pagination, + sortOrder + ); + + const filter: CourseFilter = { schoolId: school.id, status: courseStatus }; + const options = { + pagination, + order: { + [sortByField]: sortOrder, + }, + }; + + expect(schoolService.getSchoolById).toHaveBeenCalledWith(school.id); + expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(adminUser.id); + expect(authorizationService.checkPermission).toHaveBeenCalled(); + expect(courseDoService.getCourseInfo).toHaveBeenCalledWith(filter, options); + expect(userService.findById).toHaveBeenCalledWith(user.id); + expect(classService.findById).toHaveBeenCalledWith(clazz.id); + expect(groupService.findById).toHaveBeenCalledWith(group.id); + expect(result.total).toBe(5); + expect(result.data.length).toBe(5); + expect(result.data[0].classes).toStrictEqual(['1A', 'groupName']); + expect(result.data[0].teachers).toStrictEqual(['firstName lastName']); + }); - expect(schoolService.getSchoolById).toHaveBeenCalledWith(school.id); - expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(adminUser.id); - expect(authorizationService.checkPermission).toHaveBeenCalled(); - expect(courseDoService.getCourseInfo).toHaveBeenCalledWith(filter, options); - expect(userService.findById).toHaveBeenCalledWith(user.id); - expect(classService.findById).toHaveBeenCalledWith(clazz.id); - expect(groupService.findById).toHaveBeenCalledWith(group.id); - expect(result.total).toBe(5); - expect(result.data.length).toBe(5); - expect(result.data[0].classes).toStrictEqual(['1A', 'groupName']); - expect(result.data[0].teachers).toStrictEqual(['firstName lastName']); + it('should handle empty options gracefully', async () => { + const { adminUser, school } = setup(); + const filter: CourseFilter = { schoolId: school.id, status: undefined }; + const options: IFindOptions = { + order: { + name: SortOrder.asc, + }, + pagination: undefined, + }; + + await uc.getCourseInfo(adminUser.id, school.id, undefined, undefined, undefined, undefined); + + expect(schoolService.getSchoolById).toHaveBeenCalledWith(school.id); + expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(adminUser.id); + expect(authorizationService.checkPermission).toHaveBeenCalled(); + expect(courseDoService.getCourseInfo).toHaveBeenCalledWith(filter, options); + }); }); - it('should return an empty page if no courses are found', async () => { - const { adminUser, school, courseStatusQueryType: statusTypeQuery, sortByField, pagination, sortOrder } = setup(); - - courseDoService.getCourseInfo.mockResolvedValueOnce(new Page([], 0)); + describe('when courses are not found', () => { + const setup = () => { + const adminUserId = new ObjectId().toHexString(); + const schoolId = new ObjectId().toHexString(); + courseDoService.getCourseInfo.mockResolvedValueOnce(new Page([], 0)); - const result = await uc.getCourseInfo( - adminUser.id, - school.id, - sortByField, - statusTypeQuery, - pagination, - sortOrder - ); - - expect(result.total).toBe(0); - expect(result.data.length).toBe(0); - }); - - it('should handle empty data inputs', async () => { - const { adminUser, school } = setup(); + return { + adminUserId, + schoolId, + }; + }; - courseDoService.getCourseInfo.mockResolvedValueOnce(new Page([], 0)); + it('should return an empty page if no courses are found', async () => { + const { adminUserId, schoolId } = setup(); - const result = await uc.getCourseInfo(adminUser.id, school.id, undefined, undefined, undefined, undefined); + const result = await uc.getCourseInfo(adminUserId, schoolId); - expect(schoolService.getSchoolById).toHaveBeenCalledWith(school.id); - expect(result.total).toBe(0); - expect(result.data.length).toBe(0); + expect(result.total).toBe(0); + expect(result.data.length).toBe(0); + }); }); }); }); diff --git a/apps/server/src/modules/learnroom/uc/course-info.uc.ts b/apps/server/src/modules/learnroom/uc/course-info.uc.ts index 95223e539cb..564b5a014b0 100644 --- a/apps/server/src/modules/learnroom/uc/course-info.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-info.uc.ts @@ -9,7 +9,8 @@ import { User } from '@shared/domain/entity'; import { IFindOptions, Pagination, Permission, SortOrder, SortOrderMap } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { Course as CourseDO } from '../domain'; -import { CourseFilter, CourseSortQueryType, CourseStatusQueryType } from '../domain/interface'; +import { CourseFilter, CourseStatus } from '../domain/interface'; +import { CourseSortProps } from '../domain/interface/course-sort-props.enum'; import { CourseDoService } from '../service'; import { CourseInfoDto } from './dto'; @@ -27,25 +28,22 @@ export class CourseInfoUc { public async getCourseInfo( userId: EntityId, schoolId: EntityId, - sortByField: CourseSortQueryType = CourseSortQueryType.NAME, - courseStatusQueryType?: CourseStatusQueryType, + sortByField: CourseSortProps = CourseSortProps.NAME, + courseStatusQueryType?: CourseStatus, pagination?: Pagination, sortOrder: SortOrder = SortOrder.asc ): Promise> { const school: School = await this.schoolService.getSchoolById(schoolId); const user: User = await this.authService.getUserWithPermissions(userId); - this.authService.checkPermission(user, school, AuthorizationContextBuilder.read([Permission.ADMIN_VIEW])); + this.authService.checkPermission(user, school, AuthorizationContextBuilder.read([Permission.COURSE_VIEW])); const order: SortOrderMap = { [sortByField]: sortOrder }; - const filter: CourseFilter = { schoolId, courseStatusQueryType }; + const filter: CourseFilter = { schoolId, status: courseStatusQueryType }; const options: IFindOptions = { pagination, order }; const courses: Page = await this.courseDoService.getCourseInfo(filter, options); let page: Page = new Page([], courses.total); - if (courses.total === 0) { - return page; - } const resolvedCourses: CourseInfoDto[] = await this.getCourseData(courses.data); page = new Page(resolvedCourses, courses.total); @@ -57,16 +55,16 @@ export class CourseInfoUc { const courseInfos: CourseInfoDto[] = await Promise.all( courses.map(async (course) => { const groupName = course.syncedWithGroup ? await this.getSyncedGroupName(course.syncedWithGroup) : undefined; - const teachers: string[] = await this.getCourseTeachers(course.teachers); - const classes: string[] = await this.getCourseClasses(course.classes); - const groups: string[] = await this.getCourseGroups(course.groups); + const teacherNames: string[] = await this.getCourseTeacherFullNames(course.teachers); + const classNames: string[] = await this.getCourseClassNamaes(course.classes); + const groupNames: string[] = await this.getCourseGroupNames(course.groups); const mapped = new CourseInfoDto({ id: course.id, name: course.name, - classes: [...classes, ...groups], - teachers, - syncedWithGroup: groupName, + classes: [...classNames, ...groupNames], + teachers: teacherNames, + syncedGroupName: groupName, }); return mapped; @@ -82,11 +80,11 @@ export class CourseInfoUc { return group.name; } - private async getCourseTeachers(teacherIds: EntityId[]): Promise { + private async getCourseTeacherFullNames(teacherIds: EntityId[]): Promise { const teacherNames: string[] = await Promise.all( teacherIds.map(async (teacherId): Promise => { const teacher: UserDO = await this.userService.findById(teacherId); - const fullName = teacher.firstName.concat(' ').concat(teacher.lastName); + const fullName = teacher.firstName.concat(' ', teacher.lastName); return fullName; }) @@ -94,18 +92,18 @@ export class CourseInfoUc { return teacherNames; } - private async getCourseClasses(classIds: EntityId[]): Promise { + private async getCourseClassNamaes(classIds: EntityId[]): Promise { const classes: string[] = await Promise.all[]>( classIds.map(async (classId): Promise => { const clazz = await this.classService.findById(classId); - return clazz.gradeLevel ? clazz.gradeLevel?.toString().concat(clazz.name) : clazz.name; + return clazz.getClassFullName(); }) ); return classes; } - private async getCourseGroups(groupIds: EntityId[]): Promise { + private async getCourseGroupNames(groupIds: EntityId[]): Promise { const groups: string[] = await Promise.all[]>( groupIds.map(async (groupId): Promise => { const group = await this.groupService.findById(groupId); diff --git a/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts b/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts index 42482e2dd39..a26fe97612a 100644 --- a/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts +++ b/apps/server/src/modules/learnroom/uc/dto/course-info.dto.ts @@ -9,13 +9,13 @@ export class CourseInfoDto { classes: string[]; - syncedWithGroup?: string; + syncedGroupName?: string; constructor(props: CourseInfoDto) { this.id = props.id; this.name = props.name; this.classes = props.classes; this.teachers = props.teachers; - this.syncedWithGroup = props.syncedWithGroup; + this.syncedGroupName = props.syncedGroupName; } } diff --git a/apps/server/src/shared/repo/course/course.repo.ts b/apps/server/src/shared/repo/course/course.repo.ts index 0cd3492d9c3..ebe1074db02 100644 --- a/apps/server/src/shared/repo/course/course.repo.ts +++ b/apps/server/src/shared/repo/course/course.repo.ts @@ -1,73 +1,11 @@ -import { FilterQuery, QueryOrderMap } from '@mikro-orm/core'; +import { QueryOrderMap } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; import { Course } from '@shared/domain/entity'; import { IFindOptions } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { BaseRepo } from '../base.repo'; -import { Scope } from '../scope'; - -export class CourseScope extends Scope { - forAllGroupTypes(userId: EntityId): CourseScope { - const isStudent = { students: userId }; - const isTeacher = { teachers: userId }; - const isSubstitutionTeacher = { substitutionTeachers: userId }; - - if (userId) { - this.addQuery({ $or: [isStudent, isTeacher, isSubstitutionTeacher] }); - } - - return this; - } - - forTeacherOrSubstituteTeacher(userId: EntityId): CourseScope { - const isTeacher = { teachers: userId }; - const isSubstitutionTeacher = { substitutionTeachers: userId }; - - if (userId) { - this.addQuery({ $or: [isTeacher, isSubstitutionTeacher] }); - } - - return this; - } - - forTeacher(userId: EntityId): CourseScope { - this.addQuery({ teachers: userId }); - return this; - } - - forActiveCourses(): CourseScope { - const now = new Date(); - const noUntilDate = { untilDate: { $exists: false } } as FilterQuery; - const untilDateInFuture = { untilDate: { $gte: now } }; - - this.addQuery({ $or: [noUntilDate, untilDateInFuture] }); - - return this; - } - - forCourseId(courseId: EntityId): CourseScope { - this.addQuery({ id: courseId }); - return this; - } - - bySchoolId(schoolId: EntityId | undefined): CourseScope { - if (schoolId) { - this.addQuery({ school: schoolId }); - } - return this; - } - - forArchivedCourses(): CourseScope { - const now = new Date(); - const untilDateExists = { untilDate: { $exists: true } } as FilterQuery; - const untilDateInPast = { untilDate: { $lt: now } }; - - this.addQuery({ $and: [untilDateExists, untilDateInPast] }); - - return this; - } -} +import { CourseScope } from './course.scope'; @Injectable() export class CourseRepo extends BaseRepo { diff --git a/apps/server/src/shared/repo/course/course.scope.spec.ts b/apps/server/src/shared/repo/course/course.scope.spec.ts new file mode 100644 index 00000000000..84d0a22767d --- /dev/null +++ b/apps/server/src/shared/repo/course/course.scope.spec.ts @@ -0,0 +1,202 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { CourseScope } from './course.scope'; + +describe(CourseScope.name, () => { + let scope: CourseScope; + + beforeEach(() => { + scope = new CourseScope(); + scope.allowEmptyQuery(true); + jest.useFakeTimers(); + jest.setSystemTime(new Date()); + }); + + describe('forAllGroupTypes', () => { + describe('when id is defined', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const isStudent = { students: userId }; + const isTeacher = { teachers: userId }; + const isSubstitutionTeacher = { substitutionTeachers: userId }; + + return { + userId, + isStudent, + isTeacher, + isSubstitutionTeacher, + }; + }; + + it('should add query', () => { + const { userId, isStudent, isTeacher, isSubstitutionTeacher } = setup(); + + scope.forAllGroupTypes(userId); + + expect(scope.query).toEqual({ $or: [isStudent, isTeacher, isSubstitutionTeacher] }); + }); + }); + }); + + describe('forTeacherOrSubstituteTeacher', () => { + describe('when id is defined', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const isTeacher = { teachers: userId }; + const isSubstitutionTeacher = { substitutionTeachers: userId }; + + return { + userId, + isTeacher, + isSubstitutionTeacher, + }; + }; + + it('should add query', () => { + const { userId, isTeacher, isSubstitutionTeacher } = setup(); + + scope.forTeacherOrSubstituteTeacher(userId); + + expect(scope.query).toEqual({ $or: [isTeacher, isSubstitutionTeacher] }); + }); + }); + }); + + describe('forTeacher', () => { + describe('when id is defined', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const isTeacher = { teachers: userId }; + + return { + userId, + isTeacher, + }; + }; + + it('should add query', () => { + const { userId } = setup(); + + scope.forTeacher(userId); + + expect(scope.query).toEqual({ teachers: userId }); + }); + }); + }); + + describe('forActiveCourses', () => { + describe('when called', () => { + const setup = () => { + const now = new Date(); + + const noUntilDate = { untilDate: { $exists: false } }; + const untilDateInFuture = { untilDate: { $gte: now } }; + + return { + noUntilDate, + untilDateInFuture, + }; + }; + + it('should add query', () => { + const { noUntilDate, untilDateInFuture } = setup(); + + scope.forActiveCourses(); + + expect(scope.query).toEqual({ $or: [noUntilDate, untilDateInFuture] }); + }); + }); + }); + + describe('forCourseId', () => { + describe('when id is defined', () => { + const setup = () => { + const courseId = new ObjectId().toHexString(); + + return { courseId }; + }; + + it('should add query', () => { + const { courseId } = setup(); + + scope.forCourseId(courseId); + + expect(scope.query).toEqual({ id: courseId }); + }); + }); + }); + + describe('bySchoolId', () => { + describe('when id is defined', () => { + const setup = () => { + const schoolId = new ObjectId().toHexString(); + + return { schoolId }; + }; + + it('should add query', () => { + const { schoolId } = setup(); + + scope.bySchoolId(schoolId); + + expect(scope.query).toEqual({ school: schoolId }); + }); + }); + + describe('when id is not defined', () => { + it('should add query', () => { + scope.bySchoolId(undefined); + + expect(scope.query).toEqual({}); + }); + }); + }); + + describe('bySchoolId', () => { + describe('when id is defined', () => { + const setup = () => { + const schoolId = new ObjectId().toHexString(); + + return { schoolId }; + }; + + it('should add query', () => { + const { schoolId } = setup(); + + scope.bySchoolId(schoolId); + + expect(scope.query).toEqual({ school: schoolId }); + }); + }); + + describe('when id is not defined', () => { + it('should add query', () => { + scope.bySchoolId(undefined); + + expect(scope.query).toEqual({}); + }); + }); + }); + + describe('forArchivedCourses', () => { + describe('when called', () => { + const setup = () => { + const now = new Date(); + const untilDateExists = { untilDate: { $exists: true } }; + const untilDateInPast = { untilDate: { $lt: now } }; + + return { + untilDateExists, + untilDateInPast, + }; + }; + + it('should add query', () => { + const { untilDateExists, untilDateInPast } = setup(); + + scope.forArchivedCourses(); + + expect(scope.query).toEqual({ $and: [untilDateExists, untilDateInPast] }); + }); + }); + }); +}); diff --git a/apps/server/src/shared/repo/course/course.scope.ts b/apps/server/src/shared/repo/course/course.scope.ts new file mode 100644 index 00000000000..35bcb8cdfe2 --- /dev/null +++ b/apps/server/src/shared/repo/course/course.scope.ts @@ -0,0 +1,67 @@ +import { FilterQuery } from '@mikro-orm/core'; + +import { Course } from '@shared/domain/entity'; +import { EntityId } from '@shared/domain/types'; +import { Scope } from '../scope'; + +export class CourseScope extends Scope { + forAllGroupTypes(userId: EntityId): this { + const isStudent = { students: userId }; + const isTeacher = { teachers: userId }; + const isSubstitutionTeacher = { substitutionTeachers: userId }; + + if (userId) { + this.addQuery({ $or: [isStudent, isTeacher, isSubstitutionTeacher] }); + } + + return this; + } + + forTeacherOrSubstituteTeacher(userId: EntityId): this { + const isTeacher = { teachers: userId }; + const isSubstitutionTeacher = { substitutionTeachers: userId }; + + if (userId) { + this.addQuery({ $or: [isTeacher, isSubstitutionTeacher] }); + } + + return this; + } + + forTeacher(userId: EntityId): this { + this.addQuery({ teachers: userId }); + return this; + } + + forActiveCourses(): this { + const now = new Date(); + const noUntilDate = { untilDate: { $exists: false } } as FilterQuery; + const untilDateInFuture = { untilDate: { $gte: now } }; + + this.addQuery({ $or: [noUntilDate, untilDateInFuture] }); + + return this; + } + + forCourseId(courseId: EntityId): this { + this.addQuery({ id: courseId }); + return this; + } + + bySchoolId(schoolId: EntityId | undefined): this { + if (schoolId) { + this.addQuery({ school: schoolId }); + } + return this; + } + + forArchivedCourses(): this { + const now = new Date(); + const untilDateExists = { untilDate: { $exists: true } } as FilterQuery; + const untilDateInPast = { untilDate: { $lt: now } }; + + this.addQuery({ $and: [untilDateExists, untilDateInPast] }); + + return this; + } +} diff --git a/apps/server/src/shared/repo/course/index.ts b/apps/server/src/shared/repo/course/index.ts index f2a743f00e6..7ef7bcf99eb 100644 --- a/apps/server/src/shared/repo/course/index.ts +++ b/apps/server/src/shared/repo/course/index.ts @@ -1 +1,3 @@ export * from './course.repo'; +export * from './course.scope'; + From 41da6f3c296557f72d174f02518c06f59d2eed4c Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Fri, 30 Aug 2024 15:10:20 +0200 Subject: [PATCH 58/64] correct typo --- .../src/modules/learnroom/service/course-do.service.spec.ts | 2 +- apps/server/src/shared/repo/course/index.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/modules/learnroom/service/course-do.service.spec.ts b/apps/server/src/modules/learnroom/service/course-do.service.spec.ts index 7bf631befa9..a6fae948299 100644 --- a/apps/server/src/modules/learnroom/service/course-do.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/course-do.service.spec.ts @@ -181,7 +181,7 @@ describe(CourseDoService.name, () => { }); describe('startSynchronization', () => { - describe('when a course is нот synchronized with a group', () => { + describe('when a course is not synchronized with a group', () => { const setup = () => { const course: Course = courseFactory.build(); const group: Group = groupFactory.build(); diff --git a/apps/server/src/shared/repo/course/index.ts b/apps/server/src/shared/repo/course/index.ts index 7ef7bcf99eb..1e8ed2bf9f5 100644 --- a/apps/server/src/shared/repo/course/index.ts +++ b/apps/server/src/shared/repo/course/index.ts @@ -1,3 +1,2 @@ export * from './course.repo'; export * from './course.scope'; - From bc7f721159d59507ef14e79ef8fab09b247ebf13 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Fri, 30 Aug 2024 15:16:08 +0200 Subject: [PATCH 59/64] update course info uc --- apps/server/src/modules/learnroom/uc/course-info.uc.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/modules/learnroom/uc/course-info.uc.ts b/apps/server/src/modules/learnroom/uc/course-info.uc.ts index 564b5a014b0..16548ef1a3a 100644 --- a/apps/server/src/modules/learnroom/uc/course-info.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-info.uc.ts @@ -42,11 +42,10 @@ export class CourseInfoUc { const filter: CourseFilter = { schoolId, status: courseStatusQueryType }; const options: IFindOptions = { pagination, order }; const courses: Page = await this.courseDoService.getCourseInfo(filter, options); - let page: Page = new Page([], courses.total); const resolvedCourses: CourseInfoDto[] = await this.getCourseData(courses.data); - page = new Page(resolvedCourses, courses.total); + const page = new Page(resolvedCourses, courses.total); return page; } From ef03b5e442475704ecab18be9a1643ce8e274848 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Tue, 3 Sep 2024 12:34:02 +0200 Subject: [PATCH 60/64] N21-2075 extend delete course --- src/services/user-group/services/courses.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/user-group/services/courses.js b/src/services/user-group/services/courses.js index 7f773999f60..8bf833b1268 100644 --- a/src/services/user-group/services/courses.js +++ b/src/services/user-group/services/courses.js @@ -1,5 +1,6 @@ const { authenticate } = require('@feathersjs/authentication'); const { iff, isProvider } = require('feathers-hooks-common'); +const { Configuration } = require('@hpi-schul-cloud/commons/lib'); const { ifNotLocal, restrictToCurrentSchool, @@ -34,6 +35,8 @@ const { const { checkScopePermissions } = require('../../helpers/scopePermissions/hooks'); +const newRoomViewEnabled = Configuration.get('FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED'); + class Courses { constructor(options) { this.options = options || {}; @@ -60,6 +63,9 @@ class Courses { } remove(id, params) { + if (newRoomViewEnabled) { + this.app.service('courseCalendar').remove(id, prepareInternalParams(params)); + } return this.app.service('courseModel').remove(id, prepareInternalParams(params)); } From 37a957599cabf2ff2230715a3fa41e56b6d3bfa9 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Tue, 3 Sep 2024 13:10:02 +0200 Subject: [PATCH 61/64] update course-info --- .../learnroom/uc/course-info.uc.spec.ts | 46 +++++++++++++++++-- .../modules/learnroom/uc/course-info.uc.ts | 6 ++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts index a9d5065b607..3fc061e874d 100644 --- a/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts @@ -9,6 +9,7 @@ import { GroupRepo } from '@modules/group/repo/'; import { SchoolService } from '@modules/school'; import { schoolFactory } from '@modules/school/testing'; import { UserService } from '@modules/user'; +import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Page } from '@shared/domain/domainobject'; import { IFindOptions, Permission, RoleName, SortOrder } from '@shared/domain/interface'; @@ -92,10 +93,7 @@ describe('CourseInfoUc', () => { const setup = () => { const user = userFactory.withRoleByName(RoleName.TEACHER).buildWithId(); const teacher = userDoFactory.build({ id: user.id, firstName: 'firstName', lastName: 'lastName' }); - const { adminUser } = UserAndAccountTestFactory.buildAdmin({}, [ - Permission.COURSE_ADMINISTRATION, - Permission.ADMIN_VIEW, - ]); + const { adminUser } = UserAndAccountTestFactory.buildAdmin({}, [Permission.COURSE_ADMINISTRATION]); const group = groupFactory.build({ name: 'groupName' }); const clazz = classFactory.build({ name: 'A', gradeLevel: 1 }); const courses = courseDoFactory.buildList(5, { @@ -185,6 +183,46 @@ describe('CourseInfoUc', () => { }); }); + describe('when user does not have permission', () => { + const setup = () => { + const user = userFactory.withRoleByName(RoleName.TEACHER).buildWithId(); + const { adminUser } = UserAndAccountTestFactory.buildAdmin({}, []); + + const pagination = { skip: 1, limit: 2 }; + const courseStatus: CourseStatus = CourseStatus.CURRENT; + const sortByField: CourseSortProps = CourseSortProps.NAME; + const sortOrder: SortOrder = SortOrder.asc; + const school = schoolFactory.build(); + + schoolService.getSchoolById.mockResolvedValueOnce(school); + authorizationService.getUserWithPermissions.mockResolvedValue(adminUser); + authorizationService.checkPermission.mockImplementationOnce(() => { + throw new ForbiddenException(); + }); + + return { + user, + pagination, + school, + adminUser, + courseStatus, + sortByField, + sortOrder, + }; + }; + it('should throw an forbidden exception', async () => { + const { school, adminUser, sortByField, courseStatus, pagination, sortOrder } = setup(); + + const getCourseInfo = async () => + uc.getCourseInfo(adminUser.id, school.id, sortByField, courseStatus, pagination, sortOrder); + + expect(userService.findById).toHaveBeenCalledTimes(0); + expect(classService.findById).toHaveBeenCalledTimes(0); + expect(groupService.findById).toHaveBeenCalledTimes(0); + await expect(getCourseInfo()).rejects.toThrow(ForbiddenException); + }); + }); + describe('when courses are not found', () => { const setup = () => { const adminUserId = new ObjectId().toHexString(); diff --git a/apps/server/src/modules/learnroom/uc/course-info.uc.ts b/apps/server/src/modules/learnroom/uc/course-info.uc.ts index 16548ef1a3a..e133a86616d 100644 --- a/apps/server/src/modules/learnroom/uc/course-info.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-info.uc.ts @@ -36,7 +36,11 @@ export class CourseInfoUc { const school: School = await this.schoolService.getSchoolById(schoolId); const user: User = await this.authService.getUserWithPermissions(userId); - this.authService.checkPermission(user, school, AuthorizationContextBuilder.read([Permission.COURSE_VIEW])); + this.authService.checkPermission( + user, + school, + AuthorizationContextBuilder.read([Permission.COURSE_ADMINISTRATION]) + ); const order: SortOrderMap = { [sortByField]: sortOrder }; const filter: CourseFilter = { schoolId, status: courseStatusQueryType }; From d37104756f992e570e980924ca8d5c6b463b07c6 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Tue, 3 Sep 2024 13:19:20 +0200 Subject: [PATCH 62/64] N21-2075 extend delete course --- src/services/user-group/services/courses.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/user-group/services/courses.js b/src/services/user-group/services/courses.js index 8bf833b1268..2d358210e6e 100644 --- a/src/services/user-group/services/courses.js +++ b/src/services/user-group/services/courses.js @@ -64,7 +64,7 @@ class Courses { remove(id, params) { if (newRoomViewEnabled) { - this.app.service('courseCalendar').remove(id, prepareInternalParams(params)); + this.app.service('/calendar/courses').remove(id, prepareInternalParams(params)); } return this.app.service('courseModel').remove(id, prepareInternalParams(params)); } From 174f0ef4ea5ff433957f6d2a6fb2bbcae8e546af Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Tue, 3 Sep 2024 13:24:00 +0200 Subject: [PATCH 63/64] update course info tests --- .../learnroom/uc/course-info.uc.spec.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts index 3fc061e874d..1be745c067e 100644 --- a/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts @@ -185,13 +185,7 @@ describe('CourseInfoUc', () => { describe('when user does not have permission', () => { const setup = () => { - const user = userFactory.withRoleByName(RoleName.TEACHER).buildWithId(); const { adminUser } = UserAndAccountTestFactory.buildAdmin({}, []); - - const pagination = { skip: 1, limit: 2 }; - const courseStatus: CourseStatus = CourseStatus.CURRENT; - const sortByField: CourseSortProps = CourseSortProps.NAME; - const sortOrder: SortOrder = SortOrder.asc; const school = schoolFactory.build(); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -201,24 +195,15 @@ describe('CourseInfoUc', () => { }); return { - user, - pagination, school, adminUser, - courseStatus, - sortByField, - sortOrder, }; }; it('should throw an forbidden exception', async () => { - const { school, adminUser, sortByField, courseStatus, pagination, sortOrder } = setup(); + const { school, adminUser } = setup(); - const getCourseInfo = async () => - uc.getCourseInfo(adminUser.id, school.id, sortByField, courseStatus, pagination, sortOrder); + const getCourseInfo = async () => uc.getCourseInfo(adminUser.id, school.id); - expect(userService.findById).toHaveBeenCalledTimes(0); - expect(classService.findById).toHaveBeenCalledTimes(0); - expect(groupService.findById).toHaveBeenCalledTimes(0); await expect(getCourseInfo()).rejects.toThrow(ForbiddenException); }); }); From 2ffeedf91789153431f7f974383fff1ddf0e2d47 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov Date: Tue, 3 Sep 2024 17:21:43 +0200 Subject: [PATCH 64/64] update course info tests --- .../learnroom/uc/course-info.uc.spec.ts | 184 +++++++++++++----- 1 file changed, 131 insertions(+), 53 deletions(-) diff --git a/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts index 1be745c067e..2369fc6404c 100644 --- a/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-info.uc.spec.ts @@ -3,9 +3,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { AuthorizationService } from '@modules/authorization'; import { ClassService } from '@modules/class'; import { classFactory } from '@modules/class/domain/testing'; -import { ClassesRepo } from '@modules/class/repo'; import { GroupService } from '@modules/group'; -import { GroupRepo } from '@modules/group/repo/'; import { SchoolService } from '@modules/school'; import { schoolFactory } from '@modules/school/testing'; import { UserService } from '@modules/user'; @@ -23,8 +21,6 @@ import { CourseInfoDto } from './dto'; describe('CourseInfoUc', () => { let module: TestingModule; let uc: CourseInfoUc; - let groupRepo: DeepMocked; - let classesRepo: DeepMocked; let authorizationService: DeepMocked; let schoolService: DeepMocked; @@ -54,10 +50,6 @@ describe('CourseInfoUc', () => { provide: GroupService, useValue: createMock(), }, - { - provide: GroupRepo, - useValue: createMock(), - }, { provide: UserService, useValue: createMock(), @@ -66,10 +58,6 @@ describe('CourseInfoUc', () => { provide: ClassService, useValue: createMock(), }, - { - provide: ClassesRepo, - useValue: createMock(), - }, ], }).compile(); @@ -80,16 +68,18 @@ describe('CourseInfoUc', () => { groupService = module.get(GroupService); userService = module.get(UserService); classService = module.get(ClassService); - groupRepo = module.get(GroupRepo); - classesRepo = module.get(ClassesRepo); }); afterAll(async () => { await module.close(); }); + afterEach(() => { + jest.clearAllMocks(); + }); + describe('getCourseInfo', () => { - describe('when courses are found', () => { + describe('when calling getCourseInfo', () => { const setup = () => { const user = userFactory.withRoleByName(RoleName.TEACHER).buildWithId(); const teacher = userDoFactory.build({ id: user.id, firstName: 'firstName', lastName: 'lastName' }); @@ -102,20 +92,15 @@ describe('CourseInfoUc', () => { groupIds: [group.id], classIds: [clazz.id], }); - const pagination = { skip: 1, limit: 2 }; - const courseStatus: CourseStatus = CourseStatus.CURRENT; - const sortByField: CourseSortProps = CourseSortProps.NAME; - const sortOrder: SortOrder = SortOrder.asc; + const pagination = { skip: 0, limit: 5 }; const school = schoolFactory.build(); schoolService.getSchoolById.mockResolvedValueOnce(school); authorizationService.getUserWithPermissions.mockResolvedValue(adminUser); authorizationService.checkPermission.mockReturnValueOnce(undefined); - groupRepo.findGroupById.mockResolvedValue(group); groupService.findById.mockResolvedValue(group); userService.findById.mockResolvedValue(teacher); classService.findById.mockResolvedValue(clazz); - classesRepo.findClassById.mockResolvedValue(clazz); courseDoService.getCourseInfo.mockResolvedValueOnce(new Page(courses, 5)); return { @@ -125,46 +110,61 @@ describe('CourseInfoUc', () => { school, adminUser, group, - courseStatus, - sortByField, - sortOrder, clazz, }; }; - it('should return courses with sorted and filtered results', async () => { - const { clazz, group, school, adminUser, sortByField, courseStatus, pagination, sortOrder, user } = setup(); - const result: Page = await uc.getCourseInfo( - adminUser.id, - school.id, - sortByField, - courseStatus, - pagination, - sortOrder - ); + it('should call school service getSchoolById', async () => { + const { adminUser, school } = setup(); - const filter: CourseFilter = { schoolId: school.id, status: courseStatus }; - const options = { - pagination, - order: { - [sortByField]: sortOrder, - }, - }; + await uc.getCourseInfo(adminUser.id, school.id); expect(schoolService.getSchoolById).toHaveBeenCalledWith(school.id); + }); + + it('should call user service getUserWithPermissions', async () => { + const { adminUser, school } = setup(); + + await uc.getCourseInfo(adminUser.id, school.id); + expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(adminUser.id); - expect(authorizationService.checkPermission).toHaveBeenCalled(); - expect(courseDoService.getCourseInfo).toHaveBeenCalledWith(filter, options); + }); + + it('should call authorization service checkPermission', async () => { + const { adminUser, school } = setup(); + const expectedPermissions = { + action: 'read', + requiredPermissions: ['COURSE_ADMINISTRATION'], + }; + await uc.getCourseInfo(adminUser.id, school.id); + + expect(authorizationService.checkPermission).toHaveBeenCalledWith(adminUser, school, expectedPermissions); + }); + + it('should call user service findById', async () => { + const { user, adminUser, school } = setup(); + + await uc.getCourseInfo(adminUser.id, school.id); + expect(userService.findById).toHaveBeenCalledWith(user.id); + }); + + it('should call class service findById', async () => { + const { clazz, adminUser, school } = setup(); + + await uc.getCourseInfo(adminUser.id, school.id); + expect(classService.findById).toHaveBeenCalledWith(clazz.id); + }); + it('should call group service findById', async () => { + const { group, adminUser, school } = setup(); + + await uc.getCourseInfo(adminUser.id, school.id); + expect(groupService.findById).toHaveBeenCalledWith(group.id); - expect(result.total).toBe(5); - expect(result.data.length).toBe(5); - expect(result.data[0].classes).toStrictEqual(['1A', 'groupName']); - expect(result.data[0].teachers).toStrictEqual(['firstName lastName']); }); - it('should handle empty options gracefully', async () => { + it('should call with default options', async () => { const { adminUser, school } = setup(); const filter: CourseFilter = { schoolId: school.id, status: undefined }; const options: IFindOptions = { @@ -174,13 +174,92 @@ describe('CourseInfoUc', () => { pagination: undefined, }; - await uc.getCourseInfo(adminUser.id, school.id, undefined, undefined, undefined, undefined); + await uc.getCourseInfo(adminUser.id, school.id); - expect(schoolService.getSchoolById).toHaveBeenCalledWith(school.id); - expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(adminUser.id); - expect(authorizationService.checkPermission).toHaveBeenCalled(); expect(courseDoService.getCourseInfo).toHaveBeenCalledWith(filter, options); }); + + it('should call with non-default options and filter', async () => { + const { school, adminUser } = setup(); + const filter: CourseFilter = { schoolId: school.id, status: CourseStatus.ARCHIVE }; + const options: IFindOptions = { + order: { + name: SortOrder.asc, + }, + pagination: { skip: 0, limit: 5 }, + }; + + await uc.getCourseInfo( + adminUser.id, + school.id, + CourseSortProps.NAME, + CourseStatus.ARCHIVE, + { skip: 0, limit: 5 }, + SortOrder.asc + ); + + expect(courseDoService.getCourseInfo).toHaveBeenCalledWith(filter, options); + }); + }); + + describe('when courses are found', () => { + const setup = () => { + const user = userFactory.withRoleByName(RoleName.TEACHER).buildWithId(); + const teacher = userDoFactory.build({ id: user.id, firstName: 'firstName', lastName: 'lastName' }); + const { adminUser } = UserAndAccountTestFactory.buildAdmin({}, [Permission.COURSE_ADMINISTRATION]); + const group = groupFactory.build({ name: 'groupName' }); + const clazz = classFactory.build({ name: 'A', gradeLevel: 1 }); + const course1 = courseDoFactory.build({ + id: 'course1', + name: 'course1', + syncedWithGroup: group.id, + teacherIds: [user.id], + groupIds: [group.id], + classIds: [clazz.id], + }); + const course2 = courseDoFactory.build({ + id: 'course2', + name: 'course2', + teacherIds: [user.id], + groupIds: [group.id], + classIds: [clazz.id], + }); + const school = schoolFactory.build(); + + schoolService.getSchoolById.mockResolvedValueOnce(school); + authorizationService.getUserWithPermissions.mockResolvedValue(adminUser); + authorizationService.checkPermission.mockReturnValueOnce(undefined); + groupService.findById.mockResolvedValue(group); + userService.findById.mockResolvedValue(teacher); + classService.findById.mockResolvedValue(clazz); + courseDoService.getCourseInfo.mockResolvedValueOnce(new Page([course1, course2], 1)); + + return { + school, + adminUser, + }; + }; + + it('should return courses with sorted and filtered results', async () => { + const { school, adminUser } = setup(); + + const result: Page = await uc.getCourseInfo(adminUser.id, school.id); + + expect(result.data[0]).toMatchObject({ + id: 'course1', + name: 'course1', + teachers: ['firstName lastName'], + classes: ['1A', 'groupName'], + syncedGroupName: 'groupName', + }); + expect(result.data[1]).toMatchObject({ + id: 'course2', + name: 'course2', + teachers: ['firstName lastName'], + classes: ['1A', 'groupName'], + syncedGroupName: undefined, + }); + }); }); describe('when user does not have permission', () => { @@ -226,7 +305,6 @@ describe('CourseInfoUc', () => { const result = await uc.getCourseInfo(adminUserId, schoolId); expect(result.total).toBe(0); - expect(result.data.length).toBe(0); }); }); });