From 2fc165e9b7b2589185a057803d5c84c19b5efa3e Mon Sep 17 00:00:00 2001 From: mrikallab <93978883+mrikallab@users.noreply.github.com> Date: Mon, 16 Oct 2023 15:09:24 +0200 Subject: [PATCH] N21-939-assign-groups-to-course (#4464) --- apps/server/src/apps/server.app.ts | 3 ++ .../controller/api-test/group.api.spec.ts | 26 ++++++++++-- .../dto/response/class-info.response.ts | 13 ++++++ .../mapper/group-response.mapper.ts | 3 ++ .../modules/group/uc/dto/class-info.dto.ts | 11 +++++ .../modules/group/uc/dto/class-root-type.ts | 4 ++ .../src/modules/group/uc/group.uc.spec.ts | 42 ++++++++++++++++--- apps/server/src/modules/group/uc/group.uc.ts | 14 +++++-- .../group/uc/mapper/group-uc.mapper.ts | 10 ++++- .../service/school-year.service.spec.ts | 32 ++++++++++++-- .../service/school-year.service.ts | 8 +++- .../service/feathers-roster.service.spec.ts | 2 + .../src/shared/domain/entity/course.entity.ts | 12 ++++++ .../course/course.repo.integration.spec.ts | 2 + config/default.schema.json | 5 +++ src/services/user-group/hooks/courses.js | 28 +++++++++++-- src/services/user-group/model.js | 1 + .../services/user-group/hooks/classes.test.js | 1 + 18 files changed, 195 insertions(+), 22 deletions(-) create mode 100644 apps/server/src/modules/group/uc/dto/class-root-type.ts diff --git a/apps/server/src/apps/server.app.ts b/apps/server/src/apps/server.app.ts index 6452ca1cd47..c486adc1915 100644 --- a/apps/server/src/apps/server.app.ts +++ b/apps/server/src/apps/server.app.ts @@ -12,6 +12,7 @@ import { TeamService } from '@src/modules/teams/service/team.service'; import { AccountValidationService } from '@src/modules/account/services/account.validation.service'; import { AccountUc } from '@src/modules/account/uc/account.uc'; import { CollaborativeStorageUc } from '@src/modules/collaborative-storage/uc/collaborative-storage.uc'; +import { GroupService } from '@src/modules/group'; import { RocketChatService } from '@src/modules/rocketchat'; import { ServerModule } from '@src/modules/server'; import express from 'express'; @@ -82,6 +83,8 @@ async function bootstrap() { feathersExpress.services['nest-team-service'] = nestApp.get(TeamService); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment feathersExpress.services['nest-feathers-roster-service'] = nestApp.get(FeathersRosterService); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + feathersExpress.services['nest-group-service'] = nestApp.get(GroupService); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment feathersExpress.services['nest-orm'] = orm; diff --git a/apps/server/src/modules/group/controller/api-test/group.api.spec.ts b/apps/server/src/modules/group/controller/api-test/group.api.spec.ts index f0561518c0c..39bb86a4caa 100644 --- a/apps/server/src/modules/group/controller/api-test/group.api.spec.ts +++ b/apps/server/src/modules/group/controller/api-test/group.api.spec.ts @@ -1,11 +1,12 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Role, RoleName, SchoolEntity, SortOrder, SystemEntity, User } from '@shared/domain'; +import { Role, RoleName, SchoolEntity, SchoolYearEntity, SortOrder, SystemEntity, User } from '@shared/domain'; import { groupEntityFactory, roleFactory, schoolFactory, + schoolYearFactory, systemFactory, TestApiClient, UserAndAccountTestFactory, @@ -15,6 +16,7 @@ import { ClassEntity } from '@src/modules/class/entity'; import { classEntityFactory } from '@src/modules/class/entity/testing/factory/class.entity.factory'; import { ServerTestModule } from '@src/modules/server'; import { GroupEntity, GroupEntityTypes } from '../../entity'; +import { ClassRootType } from '../../uc/dto/class-root-type'; import { ClassInfoSearchListResponse, ClassSortBy } from '../dto'; const baseRouteName = '/groups'; @@ -48,11 +50,13 @@ describe('Group (API)', () => { const teacherRole: Role = roleFactory.buildWithId({ name: RoleName.TEACHER }); const teacherUser: User = userFactory.buildWithId({ school, roles: [teacherRole] }); const system: SystemEntity = systemFactory.buildWithId(); + const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); const clazz: ClassEntity = classEntityFactory.buildWithId({ name: 'Group A', schoolId: school._id, teacherIds: [teacherUser._id], source: undefined, + year: schoolYear.id, }); const group: GroupEntity = groupEntityFactory.buildWithId({ name: 'Group B', @@ -70,7 +74,17 @@ describe('Group (API)', () => { ], }); - await em.persistAndFlush([school, adminAccount, adminUser, teacherRole, teacherUser, system, clazz, group]); + await em.persistAndFlush([ + school, + adminAccount, + adminUser, + teacherRole, + teacherUser, + system, + clazz, + group, + schoolYear, + ]); em.clear(); const adminClient = await testApiClient.login(adminAccount); @@ -82,11 +96,12 @@ describe('Group (API)', () => { system, adminUser, teacherUser, + schoolYear, }; }; it('should return the classes of his school', async () => { - const { adminClient, group, clazz, system, adminUser, teacherUser } = await setup(); + const { adminClient, group, clazz, system, adminUser, teacherUser, schoolYear } = await setup(); const response = await adminClient.get(`/class`).query({ skip: 0, @@ -99,13 +114,18 @@ describe('Group (API)', () => { total: 2, data: [ { + id: group.id, + type: ClassRootType.GROUP, name: group.name, externalSourceName: system.displayName, teachers: [adminUser.lastName], }, { + id: clazz.id, + type: ClassRootType.CLASS, name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, teachers: [teacherUser.lastName], + schoolYear: schoolYear.name, }, ], skip: 0, diff --git a/apps/server/src/modules/group/controller/dto/response/class-info.response.ts b/apps/server/src/modules/group/controller/dto/response/class-info.response.ts index a2d71333c04..62c52501b95 100644 --- a/apps/server/src/modules/group/controller/dto/response/class-info.response.ts +++ b/apps/server/src/modules/group/controller/dto/response/class-info.response.ts @@ -1,6 +1,13 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ClassRootType } from '../../../uc/dto/class-root-type'; export class ClassInfoResponse { + @ApiProperty() + id: string; + + @ApiProperty({ enum: ClassRootType }) + type: ClassRootType; + @ApiProperty() name: string; @@ -10,9 +17,15 @@ export class ClassInfoResponse { @ApiProperty({ type: [String] }) teachers: string[]; + @ApiPropertyOptional() + schoolYear?: string; + constructor(props: ClassInfoResponse) { + this.id = props.id; + this.type = props.type; this.name = props.name; this.externalSourceName = props.externalSourceName; this.teachers = props.teachers; + this.schoolYear = props.schoolYear; } } diff --git a/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts b/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts index 6fbb0c6dc65..958aeee2c6b 100644 --- a/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts +++ b/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts @@ -24,9 +24,12 @@ export class GroupResponseMapper { private static mapToClassInfoToResponse(classInfo: ClassInfoDto): ClassInfoResponse { const mapped = new ClassInfoResponse({ + id: classInfo.id, + type: classInfo.type, name: classInfo.name, externalSourceName: classInfo.externalSourceName, teachers: classInfo.teachers, + schoolYear: classInfo.schoolYear, }); return mapped; diff --git a/apps/server/src/modules/group/uc/dto/class-info.dto.ts b/apps/server/src/modules/group/uc/dto/class-info.dto.ts index 0d2b5adaf68..d17c0169c93 100644 --- a/apps/server/src/modules/group/uc/dto/class-info.dto.ts +++ b/apps/server/src/modules/group/uc/dto/class-info.dto.ts @@ -1,13 +1,24 @@ +import { ClassRootType } from './class-root-type'; + export class ClassInfoDto { + id: string; + + type: ClassRootType; + name: string; externalSourceName?: string; teachers: string[]; + schoolYear?: string; + constructor(props: ClassInfoDto) { + this.id = props.id; + this.type = props.type; this.name = props.name; this.externalSourceName = props.externalSourceName; this.teachers = props.teachers; + this.schoolYear = props.schoolYear; } } diff --git a/apps/server/src/modules/group/uc/dto/class-root-type.ts b/apps/server/src/modules/group/uc/dto/class-root-type.ts new file mode 100644 index 00000000000..b1a725a7ddc --- /dev/null +++ b/apps/server/src/modules/group/uc/dto/class-root-type.ts @@ -0,0 +1,4 @@ +export enum ClassRootType { + CLASS = 'class', + GROUP = 'group', +} diff --git a/apps/server/src/modules/group/uc/group.uc.spec.ts b/apps/server/src/modules/group/uc/group.uc.spec.ts index b4115d3739b..ed089007a72 100644 --- a/apps/server/src/modules/group/uc/group.uc.spec.ts +++ b/apps/server/src/modules/group/uc/group.uc.spec.ts @@ -2,11 +2,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, Page, Permission, SortOrder, User, UserDO } from '@shared/domain'; +import { LegacySchoolDo, Page, Permission, SchoolYearEntity, SortOrder, User, UserDO } from '@shared/domain'; import { groupFactory, legacySchoolDoFactory, roleDtoFactory, + schoolYearFactory, setupEntities, UserAndAccountTestFactory, userDoFactory, @@ -16,7 +17,7 @@ import { Action, AuthorizationContext, AuthorizationService } from '@src/modules import { ClassService } from '@src/modules/class'; import { Class } from '@src/modules/class/domain'; import { classFactory } from '@src/modules/class/domain/testing/factory/class.factory'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { LegacySchoolService, SchoolYearService } from '@src/modules/legacy-school'; import { RoleService } from '@src/modules/role'; import { RoleDto } from '@src/modules/role/service/dto/role.dto'; import { SystemDto, SystemService } from '@src/modules/system'; @@ -24,6 +25,7 @@ import { UserService } from '@src/modules/user'; import { Group } from '../domain'; import { GroupService } from '../service'; import { ClassInfoDto } from './dto'; +import { ClassRootType } from './dto/class-root-type'; import { GroupUc } from './group.uc'; describe('GroupUc', () => { @@ -37,6 +39,7 @@ describe('GroupUc', () => { let roleService: DeepMocked; let schoolService: DeepMocked; let authorizationService: DeepMocked; + let schoolYearService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -70,6 +73,10 @@ describe('GroupUc', () => { provide: AuthorizationService, useValue: createMock(), }, + { + provide: SchoolYearService, + useValue: createMock(), + }, ], }).compile(); @@ -81,6 +88,7 @@ describe('GroupUc', () => { roleService = module.get(RoleService); schoolService = module.get(LegacySchoolService); authorizationService = module.get(AuthorizationService); + schoolYearService = module.get(SchoolYearService); await setupEntities(); }); @@ -144,7 +152,13 @@ describe('GroupUc', () => { lastName: studentUser.lastName, roles: [{ id: studentUser.roles[0].id, name: studentUser.roles[0].name }], }); - const clazz: Class = classFactory.build({ name: 'A', teacherIds: [teacherUser.id], source: 'LDAP' }); + const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); + const clazz: Class = classFactory.build({ + name: 'A', + teacherIds: [teacherUser.id], + source: 'LDAP', + year: schoolYear.id, + }); const system: SystemDto = new SystemDto({ id: new ObjectId().toHexString(), displayName: 'External System', @@ -191,6 +205,7 @@ describe('GroupUc', () => { throw new Error(); }); + schoolYearService.findById.mockResolvedValue(schoolYear); return { teacherUser, @@ -199,6 +214,7 @@ describe('GroupUc', () => { group, groupWithSystem, system, + schoolYear, }; }; @@ -219,23 +235,30 @@ describe('GroupUc', () => { describe('when no pagination is given', () => { it('should return all classes sorted by name', async () => { - const { teacherUser, clazz, group, groupWithSystem, system } = setup(); + const { teacherUser, clazz, group, groupWithSystem, system, schoolYear } = setup(); const result: Page = await uc.findAllClassesForSchool(teacherUser.id, teacherUser.school.id); expect(result).toEqual>({ data: [ { + id: clazz.id, name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, + type: ClassRootType.CLASS, externalSourceName: clazz.source, teachers: [teacherUser.lastName], + schoolYear: schoolYear.name, }, { + id: group.id, name: group.name, + type: ClassRootType.GROUP, teachers: [teacherUser.lastName], }, { + id: groupWithSystem.id, name: groupWithSystem.name, + type: ClassRootType.GROUP, externalSourceName: system.displayName, teachers: [teacherUser.lastName], }, @@ -247,7 +270,7 @@ describe('GroupUc', () => { describe('when sorting by external source name in descending order', () => { it('should return all classes sorted by external source name in descending order', async () => { - const { teacherUser, clazz, group, groupWithSystem, system } = setup(); + const { teacherUser, clazz, group, groupWithSystem, system, schoolYear } = setup(); const result: Page = await uc.findAllClassesForSchool( teacherUser.id, @@ -261,17 +284,24 @@ describe('GroupUc', () => { expect(result).toEqual>({ data: [ { + id: clazz.id, name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, + type: ClassRootType.CLASS, externalSourceName: clazz.source, teachers: [teacherUser.lastName], + schoolYear: schoolYear.name, }, { + id: groupWithSystem.id, name: groupWithSystem.name, + type: ClassRootType.GROUP, externalSourceName: system.displayName, teachers: [teacherUser.lastName], }, { + id: group.id, name: group.name, + type: ClassRootType.GROUP, teachers: [teacherUser.lastName], }, ], @@ -296,7 +326,9 @@ describe('GroupUc', () => { expect(result).toEqual>({ data: [ { + id: group.id, name: group.name, + type: ClassRootType.GROUP, teachers: [teacherUser.lastName], }, ], diff --git a/apps/server/src/modules/group/uc/group.uc.ts b/apps/server/src/modules/group/uc/group.uc.ts index a179b8cb352..1d884c5a325 100644 --- a/apps/server/src/modules/group/uc/group.uc.ts +++ b/apps/server/src/modules/group/uc/group.uc.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { EntityId, LegacySchoolDo, Page, Permission, SortOrder, User, UserDO } from '@shared/domain'; +import { EntityId, LegacySchoolDo, Page, Permission, SchoolYearEntity, SortOrder, User, UserDO } from '@shared/domain'; import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; import { ClassService } from '@src/modules/class'; import { Class } from '@src/modules/class/domain'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { LegacySchoolService, SchoolYearService } from '@src/modules/legacy-school'; import { RoleService } from '@src/modules/role'; import { RoleDto } from '@src/modules/role/service/dto/role.dto'; import { SystemDto, SystemService } from '@src/modules/system'; @@ -23,7 +23,8 @@ export class GroupUc { private readonly userService: UserService, private readonly roleService: RoleService, private readonly schoolService: LegacySchoolService, - private readonly authorizationService: AuthorizationService + private readonly authorizationService: AuthorizationService, + private readonly schoolYearService: SchoolYearService ) {} public async findAllClassesForSchool( @@ -72,7 +73,12 @@ export class GroupUc { clazz.teacherIds.map((teacherId: EntityId) => this.userService.findById(teacherId)) ); - const mapped: ClassInfoDto = GroupUcMapper.mapClassToClassInfoDto(clazz, teachers); + let schoolYear: SchoolYearEntity | undefined; + if (clazz.year) { + schoolYear = await this.schoolYearService.findById(clazz.year); + } + + const mapped: ClassInfoDto = GroupUcMapper.mapClassToClassInfoDto(clazz, teachers, schoolYear); return mapped; }) diff --git a/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts b/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts index 1e1f11057ce..596302c4a5c 100644 --- a/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts +++ b/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts @@ -1,8 +1,9 @@ -import { RoleName, UserDO } from '@shared/domain'; +import { RoleName, SchoolYearEntity, UserDO } from '@shared/domain'; import { Class } from '@src/modules/class/domain'; import { SystemDto } from '@src/modules/system'; import { Group } from '../../domain'; import { ClassInfoDto, ResolvedGroupUser } from '../dto'; +import { ClassRootType } from '../dto/class-root-type'; export class GroupUcMapper { public static mapGroupToClassInfoDto( @@ -11,6 +12,8 @@ export class GroupUcMapper { system?: SystemDto ): ClassInfoDto { const mapped: ClassInfoDto = new ClassInfoDto({ + id: group.id, + type: ClassRootType.GROUP, name: group.name, externalSourceName: system?.displayName, teachers: resolvedUsers @@ -21,13 +24,16 @@ export class GroupUcMapper { return mapped; } - public static mapClassToClassInfoDto(clazz: Class, teachers: UserDO[]): ClassInfoDto { + public static mapClassToClassInfoDto(clazz: Class, teachers: UserDO[], schoolYear?: SchoolYearEntity): ClassInfoDto { const name = clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name; const mapped: ClassInfoDto = new ClassInfoDto({ + id: clazz.id, + type: ClassRootType.CLASS, name, externalSourceName: clazz.source, teachers: teachers.map((user: UserDO) => user.lastName), + schoolYear: schoolYear?.name, }); return mapped; diff --git a/apps/server/src/modules/legacy-school/service/school-year.service.spec.ts b/apps/server/src/modules/legacy-school/service/school-year.service.spec.ts index 00e47a6360f..041b80d41d1 100644 --- a/apps/server/src/modules/legacy-school/service/school-year.service.spec.ts +++ b/apps/server/src/modules/legacy-school/service/school-year.service.spec.ts @@ -1,10 +1,10 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { SchoolYearEntity } from '@shared/domain'; import { setupEntities } from '@shared/testing'; import { schoolYearFactory } from '@shared/testing/factory/schoolyear.factory'; -import { SchoolYearEntity } from '@shared/domain'; -import { SchoolYearService } from './school-year.service'; import { SchoolYearRepo } from '../repo'; +import { SchoolYearService } from './school-year.service'; describe('SchoolYearService', () => { let module: TestingModule; @@ -57,4 +57,30 @@ describe('SchoolYearService', () => { }); }); }); + + describe('findById', () => { + const setup = () => { + jest.setSystemTime(new Date('2022-06-01').getTime()); + const schoolYear: SchoolYearEntity = schoolYearFactory.build({ + startDate: new Date('2021-09-01'), + endDate: new Date('2022-12-31'), + }); + + schoolYearRepo.findById.mockResolvedValue(schoolYear); + + return { + schoolYear, + }; + }; + + describe('when called', () => { + it('should return the current school year', async () => { + const { schoolYear } = setup(); + + const currentSchoolYear: SchoolYearEntity = await service.findById(schoolYear.id); + + expect(currentSchoolYear).toEqual(schoolYear); + }); + }); + }); }); diff --git a/apps/server/src/modules/legacy-school/service/school-year.service.ts b/apps/server/src/modules/legacy-school/service/school-year.service.ts index 16cae1c1cff..c153122e5d1 100644 --- a/apps/server/src/modules/legacy-school/service/school-year.service.ts +++ b/apps/server/src/modules/legacy-school/service/school-year.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { SchoolYearEntity } from '@shared/domain'; +import { EntityId, SchoolYearEntity } from '@shared/domain'; import { SchoolYearRepo } from '../repo'; @Injectable() @@ -12,4 +12,10 @@ export class SchoolYearService { return current; } + + async findById(id: EntityId): Promise { + const year: SchoolYearEntity = await this.schoolYearRepo.findById(id); + + return year; + } } diff --git a/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts b/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts index 6c55067552d..ce4d5144a38 100644 --- a/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts +++ b/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts @@ -424,6 +424,8 @@ describe('FeathersRosterService', () => { students: [studentUser, studentUser2], teachers: [teacherUser], substitutionTeachers: [substitutionTeacherUser], + classes: [], + groups: [], }); courseService.findById.mockResolvedValue(courseA); diff --git a/apps/server/src/shared/domain/entity/course.entity.ts b/apps/server/src/shared/domain/entity/course.entity.ts index 1aea75aa3c0..e873ed05300 100644 --- a/apps/server/src/shared/domain/entity/course.entity.ts +++ b/apps/server/src/shared/domain/entity/course.entity.ts @@ -1,6 +1,8 @@ import { Collection, Entity, Enum, Index, ManyToMany, ManyToOne, OneToMany, Property, Unique } from '@mikro-orm/core'; import { InternalServerErrorException } from '@nestjs/common/exceptions/internal-server-error.exception'; import { IEntityWithSchool, ILearnroom } from '@shared/domain/interface'; +import { ClassEntity } from '@src/modules/class/entity/class.entity'; +import { GroupEntity } from '@src/modules/group/entity/group.entity'; import { EntityId, LearnroomMetadata, LearnroomTypes } from '../types'; import { BaseEntityWithTimestamps } from './base.entity'; import { CourseGroup } from './coursegroup.entity'; @@ -22,6 +24,8 @@ export interface ICourseProperties { untilDate?: Date; copyingSince?: Date; features?: CourseFeatures[]; + classes?: ClassEntity[]; + groups?: GroupEntity[]; } // that is really really shit default handling :D constructor, getter, js default, em default...what the hell @@ -95,6 +99,12 @@ export class Course @Enum({ nullable: true, array: true }) features?: CourseFeatures[]; + @ManyToMany(() => ClassEntity, undefined, { fieldName: 'classIds' }) + classes = new Collection(this); + + @ManyToMany(() => GroupEntity, undefined, { fieldName: 'groupIds' }) + groups = new Collection(this); + constructor(props: ICourseProperties) { super(); if (props.name) this.name = props.name; @@ -108,6 +118,8 @@ export class Course if (props.startDate) this.startDate = props.startDate; if (props.copyingSince) this.copyingSince = props.copyingSince; if (props.features) this.features = props.features; + this.classes.set(props.classes || []); + this.groups.set(props.groups || []); } public getStudentIds(): EntityId[] { diff --git a/apps/server/src/shared/repo/course/course.repo.integration.spec.ts b/apps/server/src/shared/repo/course/course.repo.integration.spec.ts index 42df9c6ba24..5474c4ec19d 100644 --- a/apps/server/src/shared/repo/course/course.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/course/course.repo.integration.spec.ts @@ -72,6 +72,8 @@ describe('course repo', () => { 'updatedAt', 'students', 'features', + 'classes', + 'groups', ].sort(); expect(keysOfFirstElements).toEqual(expectedResult); }); diff --git a/config/default.schema.json b/config/default.schema.json index 5aba0e9aad8..ef92d3f1db5 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1322,6 +1322,11 @@ "default": false, "description": "Enables the new class list view" }, + "FEATURE_GROUPS_IN_COURSE_ENABLED": { + "type": "boolean", + "default": false, + "description": "Enables groups of type class in courses" + }, "TSP_SCHOOL_SYNCER": { "type": "object", "description": "TSP School Syncer properties", diff --git a/src/services/user-group/hooks/courses.js b/src/services/user-group/hooks/courses.js index e5cdb9a7de4..41e2014feb0 100644 --- a/src/services/user-group/hooks/courses.js +++ b/src/services/user-group/hooks/courses.js @@ -1,4 +1,6 @@ const _ = require('lodash'); +const { Configuration } = require('@hpi-schul-cloud/commons/lib'); +const { service } = require('feathers-mongoose'); const { BadRequest } = require('../../../errors'); const globalHooks = require('../../../hooks'); @@ -10,17 +12,34 @@ const restrictToCurrentSchool = globalHooks.ifNotLocal(globalHooks.restrictToCur const restrictToUsersOwnCourses = globalHooks.ifNotLocal(globalHooks.restrictToUsersOwnCourses); const { checkScopePermissions } = require('../../helpers/scopePermissions/hooks'); - /** * adds all students to a course when a class is added to the course * @param hook - contains created/patched object and request body */ -const addWholeClassToCourse = (hook) => { +const addWholeClassToCourse = async (hook) => { + const { app } = hook; const requestBody = hook.data; const course = hook.result; - if (requestBody.classIds === undefined) { - return hook; + + if (Configuration.get('FEATURE_GROUPS_IN_COURSE_ENABLED') && (requestBody.groupIds || []).length > 0) { + await Promise.all( + requestBody.groupIds.map((groupId) => + app + .service('nest-group-service') + .findById(groupId) + .then((group) => group.users) + ) + ).then(async (groupUsers) => { + // flatten deep arrays and remove duplicates + const userIds = _.flattenDeep(groupUsers).map((groupUser) => groupUser.userId); + const uniqueUserIds = _.uniqWith(userIds, (a, b) => a === b); + + await CourseModel.update({ _id: course._id }, { $addToSet: { userIds: { $each: uniqueUserIds } } }).exec(); + + return undefined; + }); } + if ((requestBody.classIds || []).length > 0) { // just courses do have a property "classIds" return Promise.all( @@ -34,6 +53,7 @@ const addWholeClassToCourse = (hook) => { studentIds = _.uniqWith(_.flattenDeep(studentIds), (e1, e2) => JSON.stringify(e1) === JSON.stringify(e2)); await CourseModel.update({ _id: course._id }, { $addToSet: { userIds: { $each: studentIds } } }).exec(); + return hook; }); } diff --git a/src/services/user-group/model.js b/src/services/user-group/model.js index 70a7bb7bab3..2bdf688b2c3 100644 --- a/src/services/user-group/model.js +++ b/src/services/user-group/model.js @@ -45,6 +45,7 @@ const timeSchema = new Schema({ const courseSchema = getUserGroupSchema({ description: { type: String }, classIds: [{ type: Schema.Types.ObjectId, required: true, ref: 'class' }], + groupIds: [{ type: Schema.Types.ObjectId }], teacherIds: [{ type: Schema.Types.ObjectId, required: true, ref: 'user' }], substitutionIds: [{ type: Schema.Types.ObjectId, required: true, ref: 'user' }], ltiToolIds: [{ type: Schema.Types.ObjectId, required: true, ref: 'ltiTool' }], diff --git a/test/services/user-group/hooks/classes.test.js b/test/services/user-group/hooks/classes.test.js index 33f547e0e38..7353f1520f0 100644 --- a/test/services/user-group/hooks/classes.test.js +++ b/test/services/user-group/hooks/classes.test.js @@ -68,6 +68,7 @@ describe('class hooks', () => { configBefore = Configuration.toObject({}); app = await appPromise(); Configuration.set('TEACHER_STUDENT_VISIBILITY__IS_ENABLED_BY_DEFAULT', 'false'); + Configuration.set('FEATURE_GROUPS_IN_COURSE_ENABLED', 'false'); server = await app.listen(0); nestServices = await setupNestServices(app); });