From 761fbe0615856e77d720cbfbe77e64a364a8762f Mon Sep 17 00:00:00 2001 From: Malte Berg Date: Wed, 4 Oct 2023 16:26:23 +0200 Subject: [PATCH 01/13] add id to ClassInfo --- .../group/controller/dto/response/class-info.response.ts | 4 ++++ .../modules/group/controller/mapper/group-response.mapper.ts | 1 + apps/server/src/modules/group/uc/dto/class-info.dto.ts | 3 +++ apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts | 2 ++ 4 files changed, 10 insertions(+) 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..69b58af7619 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,9 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class ClassInfoResponse { + @ApiPropertyOptional() + id?: string; + @ApiProperty() name: string; @@ -11,6 +14,7 @@ export class ClassInfoResponse { teachers: string[]; constructor(props: ClassInfoResponse) { + this.id = props.id; this.name = props.name; this.externalSourceName = props.externalSourceName; this.teachers = props.teachers; 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..9c72779451e 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,6 +24,7 @@ export class GroupResponseMapper { private static mapToClassInfoToResponse(classInfo: ClassInfoDto): ClassInfoResponse { const mapped = new ClassInfoResponse({ + id: classInfo.id, name: classInfo.name, externalSourceName: classInfo.externalSourceName, teachers: classInfo.teachers, 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..da3922a7c90 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,4 +1,6 @@ export class ClassInfoDto { + id?: string; + name: string; externalSourceName?: string; @@ -6,6 +8,7 @@ export class ClassInfoDto { teachers: string[]; constructor(props: ClassInfoDto) { + this.id = props.id; this.name = props.name; this.externalSourceName = props.externalSourceName; this.teachers = props.teachers; 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..7511984a6c8 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 @@ -11,6 +11,7 @@ export class GroupUcMapper { system?: SystemDto ): ClassInfoDto { const mapped: ClassInfoDto = new ClassInfoDto({ + id: group.id, name: group.name, externalSourceName: system?.displayName, teachers: resolvedUsers @@ -25,6 +26,7 @@ export class GroupUcMapper { const name = clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name; const mapped: ClassInfoDto = new ClassInfoDto({ + id: clazz.id, name, externalSourceName: clazz.source, teachers: teachers.map((user: UserDO) => user.lastName), From cf050b1946056a201ec2a6261d4cbb5985040bcd Mon Sep 17 00:00:00 2001 From: Malte Berg Date: Thu, 5 Oct 2023 14:28:51 +0200 Subject: [PATCH 02/13] add schoolyear as parameter --- .../group/controller/dto/response/class-info.response.ts | 4 ++++ .../modules/group/controller/mapper/group-response.mapper.ts | 1 + apps/server/src/modules/group/uc/dto/class-info.dto.ts | 3 +++ 3 files changed, 8 insertions(+) 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 69b58af7619..5cc4f9d7084 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 @@ -13,10 +13,14 @@ export class ClassInfoResponse { @ApiProperty({ type: [String] }) teachers: string[]; + @ApiPropertyOptional() + schoolYear?: string; + constructor(props: ClassInfoResponse) { this.id = props.id; 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 9c72779451e..1b9c81e8228 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 @@ -28,6 +28,7 @@ export class GroupResponseMapper { 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 da3922a7c90..f3b6e78c524 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 @@ -7,10 +7,13 @@ export class ClassInfoDto { teachers: string[]; + schoolYear?: string; + constructor(props: ClassInfoDto) { this.id = props.id; this.name = props.name; this.externalSourceName = props.externalSourceName; this.teachers = props.teachers; + this.schoolYear = props.schoolYear; } } From 751d49dc58a59840a261e03aa56efd8ddbe892d2 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Fri, 6 Oct 2023 09:34:54 +0200 Subject: [PATCH 03/13] N21-939 nest service in feathers --- apps/server/src/apps/server.app.ts | 3 +++ src/services/user-group/hooks/courses.js | 18 ++++++++++++++++-- test/utils/setup.nest.services.js | 3 +++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/server/src/apps/server.app.ts b/apps/server/src/apps/server.app.ts index 6452ca1cd47..8cbc251e89e 100644 --- a/apps/server/src/apps/server.app.ts +++ b/apps/server/src/apps/server.app.ts @@ -27,6 +27,7 @@ import { addPrometheusMetricsMiddlewaresIfEnabled, createAndStartPrometheusMetricsAppIfEnabled, } from './helpers/prometheus-metrics'; +import { GroupUc } from '../modules/group/uc'; async function bootstrap() { sourceMapInstall(); @@ -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-groups-uc'] = nestApp.get(GroupUc); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment feathersExpress.services['nest-orm'] = orm; diff --git a/src/services/user-group/hooks/courses.js b/src/services/user-group/hooks/courses.js index e5cdb9a7de4..f68b3c5cda3 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,28 @@ 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) => { + console.log(hook); const requestBody = hook.data; const course = hook.result; if (requestBody.classIds === undefined) { + console.log(hook); return hook; } + console.log(hook); + + if (Configuration.get('FEATURE_GROUPS_IN_COURSE')) { + const { app } = hook; + const groupUc = await app + .service('nest-groups-service') + .addWholeClassToCourse(hook.result, hook.data, requestBody.classIds); + return groupUc; + } + console.log(hook); if ((requestBody.classIds || []).length > 0) { // just courses do have a property "classIds" return Promise.all( @@ -34,6 +47,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(); + console.log(hook); return hook; }); } diff --git a/test/utils/setup.nest.services.js b/test/utils/setup.nest.services.js index 4de3bce181f..96e721fb915 100644 --- a/test/utils/setup.nest.services.js +++ b/test/utils/setup.nest.services.js @@ -15,6 +15,7 @@ const { DB_PASSWORD, DB_URL, DB_USERNAME } = require('../../dist/apps/server/con const { ALL_ENTITIES } = require('../../dist/apps/server/shared/domain/entity/all-entities'); const { TeamService } = require('../../dist/apps/server/modules/teams/service/team.service'); const { TeamsApiModule } = require('../../dist/apps/server/modules/teams/teams-api.module'); +const { GroupUc } = require('../../dist/apps/server/modules/group/uc'); const setupNestServices = async (app) => { const module = await Test.createTestingModule({ @@ -39,11 +40,13 @@ const setupNestServices = async (app) => { const accountService = nestApp.get(AccountService); const accountValidationService = nestApp.get(AccountValidationService); const teamService = nestApp.get(TeamService); + const groupUc = nestApp.get(GroupUc); app.services['nest-account-uc'] = accountUc; app.services['nest-account-service'] = accountService; app.services['nest-account-validation-service'] = accountValidationService; app.services['nest-team-service'] = teamService; + app.services['nest-groups-uc'] = groupUc; app.services['nest-orm'] = orm; return { nestApp, orm, accountUc, accountService }; From c5e1c53f300672d626ef4f61f8f9ee87acd6970f Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Fri, 6 Oct 2023 14:01:08 +0200 Subject: [PATCH 04/13] N21-939 add groupIds to courseSchema --- src/services/user-group/hooks/courses.js | 2 +- src/services/user-group/model.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/user-group/hooks/courses.js b/src/services/user-group/hooks/courses.js index f68b3c5cda3..e463b811036 100644 --- a/src/services/user-group/hooks/courses.js +++ b/src/services/user-group/hooks/courses.js @@ -30,7 +30,7 @@ const addWholeClassToCourse = async (hook) => { const { app } = hook; const groupUc = await app .service('nest-groups-service') - .addWholeClassToCourse(hook.result, hook.data, requestBody.classIds); + .addWholeClassToCourse('userId', hook.result.id, requestBody.classIds, requestBody.groupIds); return groupUc; } console.log(hook); diff --git a/src/services/user-group/model.js b/src/services/user-group/model.js index 70a7bb7bab3..3798cdc4f80 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, required: true }], 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' }], From 5b2d5216fffb2e5831f5e5675f2f32205f9e58b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Fri, 6 Oct 2023 15:34:35 +0200 Subject: [PATCH 05/13] implement adding groups to courses --- apps/server/src/apps/server.app.ts | 10 ++--- .../controller/api-test/group.api.spec.ts | 26 ++++++++++-- .../dto/response/class-info.response.ts | 9 +++- .../mapper/group-response.mapper.ts | 1 + .../modules/group/uc/dto/class-info.dto.ts | 7 +++- .../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 | 8 +++- .../service/school-year.service.spec.ts | 32 ++++++++++++-- .../service/school-year.service.ts | 8 +++- .../src/shared/domain/entity/course.entity.ts | 12 ++++++ src/services/user-group/hooks/courses.js | 36 +++++++++------- src/services/user-group/model.js | 1 + .../services/user-group/hooks/classes.test.js | 2 +- test/utils/setup.nest.services.js | 3 -- 16 files changed, 170 insertions(+), 45 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 8cbc251e89e..6a4094abfd2 100644 --- a/apps/server/src/apps/server.app.ts +++ b/apps/server/src/apps/server.app.ts @@ -8,26 +8,26 @@ import { enableOpenApiDocs } from '@shared/controller/swagger'; import { Mail, MailService } from '@shared/infra/mail'; import { LegacyLogger, Logger } from '@src/core/logger'; import { AccountService } from '@src/modules/account/services/account.service'; -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 { FeathersRosterService } from '@src/modules/pseudonym'; import { RocketChatService } from '@src/modules/rocketchat'; import { ServerModule } from '@src/modules/server'; +import { TeamService } from '@src/modules/teams/service/team.service'; import express from 'express'; import { join } from 'path'; // register source-map-support for debugging import { install as sourceMapInstall } from 'source-map-support'; -import { FeathersRosterService } from '@src/modules/pseudonym'; -import legacyAppPromise = require('../../../../src/app'); import { AppStartLoggable } from './helpers/app-start-loggable'; import { addPrometheusMetricsMiddlewaresIfEnabled, createAndStartPrometheusMetricsAppIfEnabled, } from './helpers/prometheus-metrics'; -import { GroupUc } from '../modules/group/uc'; +import legacyAppPromise = require('../../../../src/app'); async function bootstrap() { sourceMapInstall(); @@ -84,7 +84,7 @@ async function bootstrap() { // 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-groups-uc'] = nestApp.get(GroupUc); + 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 5cc4f9d7084..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,8 +1,12 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ClassRootType } from '../../../uc/dto/class-root-type'; export class ClassInfoResponse { - @ApiPropertyOptional() - id?: string; + @ApiProperty() + id: string; + + @ApiProperty({ enum: ClassRootType }) + type: ClassRootType; @ApiProperty() name: string; @@ -18,6 +22,7 @@ export class ClassInfoResponse { constructor(props: ClassInfoResponse) { this.id = props.id; + this.type = props.type; this.name = props.name; this.externalSourceName = props.externalSourceName; this.teachers = props.teachers; 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 1b9c81e8228..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 @@ -25,6 +25,7 @@ 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, 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 f3b6e78c524..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,5 +1,9 @@ +import { ClassRootType } from './class-root-type'; + export class ClassInfoDto { - id?: string; + id: string; + + type: ClassRootType; name: string; @@ -11,6 +15,7 @@ export class ClassInfoDto { constructor(props: ClassInfoDto) { this.id = props.id; + this.type = props.type; this.name = props.name; this.externalSourceName = props.externalSourceName; this.teachers = props.teachers; 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 7511984a6c8..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( @@ -12,6 +13,7 @@ export class GroupUcMapper { ): ClassInfoDto { const mapped: ClassInfoDto = new ClassInfoDto({ id: group.id, + type: ClassRootType.GROUP, name: group.name, externalSourceName: system?.displayName, teachers: resolvedUsers @@ -22,14 +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/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/src/services/user-group/hooks/courses.js b/src/services/user-group/hooks/courses.js index f68b3c5cda3..c3c6415b4a4 100644 --- a/src/services/user-group/hooks/courses.js +++ b/src/services/user-group/hooks/courses.js @@ -17,23 +17,29 @@ const { checkScopePermissions } = require('../../helpers/scopePermissions/hooks' * @param hook - contains created/patched object and request body */ const addWholeClassToCourse = async (hook) => { - console.log(hook); + const { app } = hook; const requestBody = hook.data; const course = hook.result; - if (requestBody.classIds === undefined) { - console.log(hook); - return hook; - } - console.log(hook); - - if (Configuration.get('FEATURE_GROUPS_IN_COURSE')) { - const { app } = hook; - const groupUc = await app - .service('nest-groups-service') - .addWholeClassToCourse(hook.result, hook.data, requestBody.classIds); - return groupUc; + + if (Configuration.get('FEATURE_GROUPS_IN_COURSE') && (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; + }); } - console.log(hook); + if ((requestBody.classIds || []).length > 0) { // just courses do have a property "classIds" return Promise.all( @@ -47,7 +53,7 @@ const addWholeClassToCourse = async (hook) => { studentIds = _.uniqWith(_.flattenDeep(studentIds), (e1, e2) => JSON.stringify(e1) === JSON.stringify(e2)); await CourseModel.update({ _id: course._id }, { $addToSet: { userIds: { $each: studentIds } } }).exec(); - console.log(hook); + 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..dff3a4dbbe3 100644 --- a/test/services/user-group/hooks/classes.test.js +++ b/test/services/user-group/hooks/classes.test.js @@ -11,7 +11,7 @@ const { restrictToUsersOwnClasses, } = require('../../../../src/services/user-group/hooks/classes'); -describe('class hooks', () => { +describe.only('class hooks', () => { describe('sorting method', () => { const defaultQuery = { year: 1, gradeLevel: 1, name: 1 }; diff --git a/test/utils/setup.nest.services.js b/test/utils/setup.nest.services.js index 96e721fb915..4de3bce181f 100644 --- a/test/utils/setup.nest.services.js +++ b/test/utils/setup.nest.services.js @@ -15,7 +15,6 @@ const { DB_PASSWORD, DB_URL, DB_USERNAME } = require('../../dist/apps/server/con const { ALL_ENTITIES } = require('../../dist/apps/server/shared/domain/entity/all-entities'); const { TeamService } = require('../../dist/apps/server/modules/teams/service/team.service'); const { TeamsApiModule } = require('../../dist/apps/server/modules/teams/teams-api.module'); -const { GroupUc } = require('../../dist/apps/server/modules/group/uc'); const setupNestServices = async (app) => { const module = await Test.createTestingModule({ @@ -40,13 +39,11 @@ const setupNestServices = async (app) => { const accountService = nestApp.get(AccountService); const accountValidationService = nestApp.get(AccountValidationService); const teamService = nestApp.get(TeamService); - const groupUc = nestApp.get(GroupUc); app.services['nest-account-uc'] = accountUc; app.services['nest-account-service'] = accountService; app.services['nest-account-validation-service'] = accountValidationService; app.services['nest-team-service'] = teamService; - app.services['nest-groups-uc'] = groupUc; app.services['nest-orm'] = orm; return { nestApp, orm, accountUc, accountService }; From 2d6254fe26a1d0c447b0ac1d85b71b5798eed283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= Date: Fri, 6 Oct 2023 16:07:46 +0200 Subject: [PATCH 06/13] add feature correctly --- config/default.schema.json | 5 +++++ src/services/user-group/hooks/courses.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config/default.schema.json b/config/default.schema.json index 9103cfd7d4f..5b299b83ca2 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1293,6 +1293,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 c3c6415b4a4..41e2014feb0 100644 --- a/src/services/user-group/hooks/courses.js +++ b/src/services/user-group/hooks/courses.js @@ -21,7 +21,7 @@ const addWholeClassToCourse = async (hook) => { const requestBody = hook.data; const course = hook.result; - if (Configuration.get('FEATURE_GROUPS_IN_COURSE') && (requestBody.groupIds || []).length > 0) { + if (Configuration.get('FEATURE_GROUPS_IN_COURSE_ENABLED') && (requestBody.groupIds || []).length > 0) { await Promise.all( requestBody.groupIds.map((groupId) => app From 6e6e0eb96d22a4400fd14db391695acc56c1170e Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Thu, 12 Oct 2023 08:06:40 +0200 Subject: [PATCH 07/13] N21-939 fix nest cov --- .../modules/pseudonym/service/feathers-roster.service.spec.ts | 2 ++ 1 file changed, 2 insertions(+) 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); From bace8ed52d02115a563c0a3d32359c4ac865b899 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Thu, 12 Oct 2023 08:08:30 +0200 Subject: [PATCH 08/13] N21-939 review changes --- apps/server/src/apps/server.app.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/server/src/apps/server.app.ts b/apps/server/src/apps/server.app.ts index 6a4094abfd2..c486adc1915 100644 --- a/apps/server/src/apps/server.app.ts +++ b/apps/server/src/apps/server.app.ts @@ -8,26 +8,26 @@ import { enableOpenApiDocs } from '@shared/controller/swagger'; import { Mail, MailService } from '@shared/infra/mail'; import { LegacyLogger, Logger } from '@src/core/logger'; import { AccountService } from '@src/modules/account/services/account.service'; +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 { FeathersRosterService } from '@src/modules/pseudonym'; import { RocketChatService } from '@src/modules/rocketchat'; import { ServerModule } from '@src/modules/server'; -import { TeamService } from '@src/modules/teams/service/team.service'; import express from 'express'; import { join } from 'path'; // register source-map-support for debugging import { install as sourceMapInstall } from 'source-map-support'; +import { FeathersRosterService } from '@src/modules/pseudonym'; +import legacyAppPromise = require('../../../../src/app'); import { AppStartLoggable } from './helpers/app-start-loggable'; import { addPrometheusMetricsMiddlewaresIfEnabled, createAndStartPrometheusMetricsAppIfEnabled, } from './helpers/prometheus-metrics'; -import legacyAppPromise = require('../../../../src/app'); async function bootstrap() { sourceMapInstall(); From 11928803a622d6d3fc2413542e32ea9bee1ad100 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Thu, 12 Oct 2023 08:17:40 +0200 Subject: [PATCH 09/13] N21-939 review changes --- test/services/user-group/hooks/classes.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/services/user-group/hooks/classes.test.js b/test/services/user-group/hooks/classes.test.js index dff3a4dbbe3..7353f1520f0 100644 --- a/test/services/user-group/hooks/classes.test.js +++ b/test/services/user-group/hooks/classes.test.js @@ -11,7 +11,7 @@ const { restrictToUsersOwnClasses, } = require('../../../../src/services/user-group/hooks/classes'); -describe.only('class hooks', () => { +describe('class hooks', () => { describe('sorting method', () => { const defaultQuery = { year: 1, gradeLevel: 1, name: 1 }; @@ -68,6 +68,7 @@ describe.only('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); }); From 0c115fceccd98f2eefc034eaad5a39141b841fc2 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Thu, 12 Oct 2023 08:35:21 +0200 Subject: [PATCH 10/13] N21-939 fix course repo test --- .../src/shared/repo/course/course.repo.integration.spec.ts | 2 ++ 1 file changed, 2 insertions(+) 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); }); From 29a8d3644a4230bdb0580230ee8ca82ca21c66a7 Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Thu, 12 Oct 2023 09:38:08 +0200 Subject: [PATCH 11/13] N21-939 tests --- test/services/courses/hooks/index.test.js | 96 ++++++++++++++++++- .../services/courses/services/courses.test.js | 34 +++++++ test/utils/setup.nest.services.js | 7 +- 3 files changed, 135 insertions(+), 2 deletions(-) diff --git a/test/services/courses/hooks/index.test.js b/test/services/courses/hooks/index.test.js index 449009c8fbc..cf700e90b43 100644 --- a/test/services/courses/hooks/index.test.js +++ b/test/services/courses/hooks/index.test.js @@ -1,7 +1,12 @@ const { expect } = require('chai'); +const { Configuration } = require('@hpi-schul-cloud/commons/lib'); const appPromise = require('../../../../src/app'); const testObjects = require('../../helpers/testObjects')(appPromise()); -const { restrictChangesToArchivedCourse } = require('../../../../src/services/user-group/hooks/courses'); +const { + restrictChangesToArchivedCourse, + addWholeClassToCourse, +} = require('../../../../src/services/user-group/hooks/courses'); +const { setupNestServices } = require('../../../utils/setup.nest.services'); const oneHour = 600000; const twoDays = 172800000; @@ -9,8 +14,11 @@ const twoDays = 172800000; describe('course hooks', () => { let app; let server; + let nestServices; + before(async () => { app = await appPromise(); + nestServices = await setupNestServices(app); server = app.listen(0); }); @@ -82,4 +90,90 @@ describe('course hooks', () => { } }); }); + + describe('add whole class to course', () => { + describe('when FEATURE_GROUPS_IN_COURSE_ENABLED is enabled and group are given', async () => { + const setup = async () => { + Configuration.set('FEATURE_GROUPS_IN_COURSE_ENABLED', true); + const teacher = await testObjects.createTestUser({ roles: ['teacher'] }); + const student = await testObjects.createTestUser({ roles: ['student'] }); + const student1 = await testObjects.createTestUser({ roles: ['student'] }); + const student2 = await testObjects.createTestUser({ roles: ['student'] }); + const class1 = await testObjects.createTestClass({ teacherIds: [teacher._id] }); + const class2 = await testObjects.createTestClass({ teacherIds: [teacher._id], userIds: [student2._id] }); + const group1 = await testObjects.createTestCourseGroup({ + teacherIds: [teacher._id], + userIds: [student._id, student1._id], + }); + + return { teacher, student, class1, class2, group1 }; + }; + it('should add classmembers and groupmembers to course hook', async () => { + const { class1, class2, group1 } = await setup(); + const result = addWholeClassToCourse({ + app, + method: 'update', + id: '123', + data: { groupIds: [group1._id], classIds: [class1._id, class2._id] }, + }); + expect(result).to.not.equal(undefined); + expect(result.teacherIds).to.have.length(1); + expect(result.userIds).to.have.length(3); + }); + }); + + describe('when FEATURE_GROUPS_IN_COURSE_ENABLED is enabled and group are not given', async () => { + const setup = async () => { + Configuration.set('FEATURE_GROUPS_IN_COURSE_ENABLED', true); + const teacher = await testObjects.createTestUser({ roles: ['teacher'] }); + const student = await testObjects.createTestUser({ roles: ['student'] }); + const class1 = await testObjects.createTestClass({ teacherIds: [teacher._id] }); + const class2 = await testObjects.createTestClass({ teacherIds: [teacher._id] }); + + return { teacher, student, class1, class2 }; + }; + + it('should add classmembers and groupmembers to course hook', async () => { + const { class1, class2 } = await setup(); + const result = addWholeClassToCourse({ + app, + method: 'update', + id: '123', + data: { groupIds: undefined, classIds: [class1._id, class2._id] }, + }); + expect(result).to.not.equal(undefined); + expect(result.teacherIds).to.have.length(1); + expect(result.userIds).to.have.length(0); + }); + }); + + describe('when FEATURE_GROUPS_IN_COURSE_ENABLED is disabled', async () => { + const setup = async () => { + Configuration.set('FEATURE_GROUPS_IN_COURSE_ENABLED', false); + const teacher = await testObjects.createTestUser({ roles: ['teacher'] }); + const student = await testObjects.createTestUser({ roles: ['student'] }); + const student1 = await testObjects.createTestUser({ roles: ['student'] }); + const student2 = await testObjects.createTestUser({ roles: ['student'] }); + const class1 = await testObjects.createTestClass({ teacherIds: [teacher._id] }); + const group1 = await testObjects.createTestCourseGroup({ + teacherIds: [teacher._id], + userIds: [student._id, student1._id, student2._id], + }); + + return { teacher, student, class1, group1 }; + }; + it('should add classmembers and groupmembers to course hook', async () => { + const { class1, group1 } = await setup(); + const result = addWholeClassToCourse({ + app, + method: 'update', + id: '123', + data: { groupIds: [group1._id], classIds: [class1._id] }, + }); + expect(result.teacherIds).to.not.equal(undefined); + expect(result.teacherIds).to.have.length(1); + expect(result.userIds).to.have.length(0); + }); + }); + }); }); diff --git a/test/services/courses/services/courses.test.js b/test/services/courses/services/courses.test.js index 82aced56e70..ee6f8afd04c 100644 --- a/test/services/courses/services/courses.test.js +++ b/test/services/courses/services/courses.test.js @@ -1,4 +1,5 @@ const { expect } = require('chai'); +const { Configuration } = require('@hpi-schul-cloud/commons/lib'); const appPromise = require('../../../../src/app'); const testObjects = require('../../helpers/testObjects')(appPromise()); @@ -8,14 +9,18 @@ describe('course service', () => { let app; let courseService; let nestServices; + let server; + let nestGroupService; before(async () => { app = await appPromise(); nestServices = await setupNestServices(app); courseService = app.service('courses'); + nestGroupService = app.service('nest-group-service'); }); after(async () => { + await server.close(); await closeNestServices(nestServices); }); @@ -86,4 +91,33 @@ describe('course service', () => { expect(err.code).to.eq(403); } }); + + describe('Patch Course', () => { + describe('when FEATURE_GROUPS_IN_COURSE_ENABLED is enabled', async () => { + const setup = async () => { + Configuration.set('FEATURE_GROUPS_IN_COURSE_ENABLED', true); + const teacher = await testObjects.createTestUser({ roles: ['teacher'] }); + const student = await testObjects.createTestUser({ roles: ['student'] }); + const class1 = await testObjects.createTestClass({ teacherIds: [teacher._id] }); + const group1 = await testObjects.createTestCourseGroup({ teacherIds: [teacher._id], userIds: [student._id] }); + + const course = await testObjects.createTestCourse({ + name: 'courseNotChanged', + teacherIds: [teacher._id], + classIds: [class1._id], + groupIds: [group1._id], + }); + const params = await testObjects.generateRequestParamsFromUser(teacher); + + return { teacher, course, params }; + }; + + it('should add whole classmembers and groupmembers to course', async () => { + const { course, params } = await setup(); + const result = await courseService.patch(course._id, { name: 'courseChanged' }, params); + expect(result.name).to.equal('courseChanged'); + expect(result.userIds).to.have.lengthOf(1); + }); + }); + }); }); diff --git a/test/utils/setup.nest.services.js b/test/utils/setup.nest.services.js index 4de3bce181f..5aef553eb53 100644 --- a/test/utils/setup.nest.services.js +++ b/test/utils/setup.nest.services.js @@ -11,10 +11,12 @@ const { AccountService } = require('../../dist/apps/server/modules/account/servi const { AccountValidationService, } = require('../../dist/apps/server/modules/account/services/account.validation.service'); +const { GroupService } = require('../../dist/apps/server/modules/group/service/group.service'); const { DB_PASSWORD, DB_URL, DB_USERNAME } = require('../../dist/apps/server/config/database.config'); const { ALL_ENTITIES } = require('../../dist/apps/server/shared/domain/entity/all-entities'); const { TeamService } = require('../../dist/apps/server/modules/teams/service/team.service'); const { TeamsApiModule } = require('../../dist/apps/server/modules/teams/teams-api.module'); +const { GroupApiModule } = require('../../dist/apps/server/modules/group/group-api.module'); const setupNestServices = async (app) => { const module = await Test.createTestingModule({ @@ -31,6 +33,7 @@ const setupNestServices = async (app) => { ConfigModule.forRoot({ ignoreEnvFile: true, ignoreEnvVars: true, isGlobal: true }), AccountApiModule, TeamsApiModule, + GroupApiModule, ], }).compile(); const nestApp = await module.createNestApplication().init(); @@ -39,14 +42,16 @@ const setupNestServices = async (app) => { const accountService = nestApp.get(AccountService); const accountValidationService = nestApp.get(AccountValidationService); const teamService = nestApp.get(TeamService); + const groupService = nestApp.get(GroupService); app.services['nest-account-uc'] = accountUc; app.services['nest-account-service'] = accountService; app.services['nest-account-validation-service'] = accountValidationService; app.services['nest-team-service'] = teamService; + app.services['nest-group-service'] = groupService; app.services['nest-orm'] = orm; - return { nestApp, orm, accountUc, accountService }; + return { nestApp, orm, accountUc, accountService, groupService }; }; const closeNestServices = async (nestServices) => { From 153e66ff999a0ae2041ea2c7b88a399ca4bf6539 Mon Sep 17 00:00:00 2001 From: Arne Gnisa Date: Thu, 12 Oct 2023 10:02:27 +0200 Subject: [PATCH 12/13] test adjustment --- test/services/courses/hooks/index.test.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/test/services/courses/hooks/index.test.js b/test/services/courses/hooks/index.test.js index cf700e90b43..3d51ac393b8 100644 --- a/test/services/courses/hooks/index.test.js +++ b/test/services/courses/hooks/index.test.js @@ -6,7 +6,7 @@ const { restrictChangesToArchivedCourse, addWholeClassToCourse, } = require('../../../../src/services/user-group/hooks/courses'); -const { setupNestServices } = require('../../../utils/setup.nest.services'); +const { setupNestServices, closeNestServices } = require('../../../utils/setup.nest.services'); const oneHour = 600000; const twoDays = 172800000; @@ -15,15 +15,18 @@ describe('course hooks', () => { let app; let server; let nestServices; + let nestGroupService; before(async () => { app = await appPromise(); - nestServices = await setupNestServices(app); server = app.listen(0); + nestServices = setupNestServices(app); + nestGroupService = app.service('nest-group-service'); }); - after(() => { + after(async () => { server.close(); + await closeNestServices(nestServices); }); describe('restrict changes to archived course', () => { @@ -108,14 +111,17 @@ describe('course hooks', () => { return { teacher, student, class1, class2, group1 }; }; + it('should add classmembers and groupmembers to course hook', async () => { const { class1, class2, group1 } = await setup(); + const result = addWholeClassToCourse({ app, method: 'update', id: '123', data: { groupIds: [group1._id], classIds: [class1._id, class2._id] }, }); + expect(result).to.not.equal(undefined); expect(result.teacherIds).to.have.length(1); expect(result.userIds).to.have.length(3); @@ -135,12 +141,14 @@ describe('course hooks', () => { it('should add classmembers and groupmembers to course hook', async () => { const { class1, class2 } = await setup(); + const result = addWholeClassToCourse({ app, method: 'update', id: '123', data: { groupIds: undefined, classIds: [class1._id, class2._id] }, }); + expect(result).to.not.equal(undefined); expect(result.teacherIds).to.have.length(1); expect(result.userIds).to.have.length(0); @@ -162,14 +170,17 @@ describe('course hooks', () => { return { teacher, student, class1, group1 }; }; + it('should add classmembers and groupmembers to course hook', async () => { const { class1, group1 } = await setup(); + const result = addWholeClassToCourse({ app, method: 'update', id: '123', data: { groupIds: [group1._id], classIds: [class1._id] }, }); + expect(result.teacherIds).to.not.equal(undefined); expect(result.teacherIds).to.have.length(1); expect(result.userIds).to.have.length(0); From 6cb9490a87ca3a6c8617cad99d6f51ea954516ec Mon Sep 17 00:00:00 2001 From: Mrika Llabani Date: Thu, 12 Oct 2023 15:21:37 +0200 Subject: [PATCH 13/13] N21-939 revert feathers test --- test/services/courses/hooks/index.test.js | 109 +----------------- .../services/courses/services/courses.test.js | 34 ------ test/utils/setup.nest.services.js | 7 +- 3 files changed, 3 insertions(+), 147 deletions(-) diff --git a/test/services/courses/hooks/index.test.js b/test/services/courses/hooks/index.test.js index 3d51ac393b8..449009c8fbc 100644 --- a/test/services/courses/hooks/index.test.js +++ b/test/services/courses/hooks/index.test.js @@ -1,12 +1,7 @@ const { expect } = require('chai'); -const { Configuration } = require('@hpi-schul-cloud/commons/lib'); const appPromise = require('../../../../src/app'); const testObjects = require('../../helpers/testObjects')(appPromise()); -const { - restrictChangesToArchivedCourse, - addWholeClassToCourse, -} = require('../../../../src/services/user-group/hooks/courses'); -const { setupNestServices, closeNestServices } = require('../../../utils/setup.nest.services'); +const { restrictChangesToArchivedCourse } = require('../../../../src/services/user-group/hooks/courses'); const oneHour = 600000; const twoDays = 172800000; @@ -14,19 +9,13 @@ const twoDays = 172800000; describe('course hooks', () => { let app; let server; - let nestServices; - let nestGroupService; - before(async () => { app = await appPromise(); server = app.listen(0); - nestServices = setupNestServices(app); - nestGroupService = app.service('nest-group-service'); }); - after(async () => { + after(() => { server.close(); - await closeNestServices(nestServices); }); describe('restrict changes to archived course', () => { @@ -93,98 +82,4 @@ describe('course hooks', () => { } }); }); - - describe('add whole class to course', () => { - describe('when FEATURE_GROUPS_IN_COURSE_ENABLED is enabled and group are given', async () => { - const setup = async () => { - Configuration.set('FEATURE_GROUPS_IN_COURSE_ENABLED', true); - const teacher = await testObjects.createTestUser({ roles: ['teacher'] }); - const student = await testObjects.createTestUser({ roles: ['student'] }); - const student1 = await testObjects.createTestUser({ roles: ['student'] }); - const student2 = await testObjects.createTestUser({ roles: ['student'] }); - const class1 = await testObjects.createTestClass({ teacherIds: [teacher._id] }); - const class2 = await testObjects.createTestClass({ teacherIds: [teacher._id], userIds: [student2._id] }); - const group1 = await testObjects.createTestCourseGroup({ - teacherIds: [teacher._id], - userIds: [student._id, student1._id], - }); - - return { teacher, student, class1, class2, group1 }; - }; - - it('should add classmembers and groupmembers to course hook', async () => { - const { class1, class2, group1 } = await setup(); - - const result = addWholeClassToCourse({ - app, - method: 'update', - id: '123', - data: { groupIds: [group1._id], classIds: [class1._id, class2._id] }, - }); - - expect(result).to.not.equal(undefined); - expect(result.teacherIds).to.have.length(1); - expect(result.userIds).to.have.length(3); - }); - }); - - describe('when FEATURE_GROUPS_IN_COURSE_ENABLED is enabled and group are not given', async () => { - const setup = async () => { - Configuration.set('FEATURE_GROUPS_IN_COURSE_ENABLED', true); - const teacher = await testObjects.createTestUser({ roles: ['teacher'] }); - const student = await testObjects.createTestUser({ roles: ['student'] }); - const class1 = await testObjects.createTestClass({ teacherIds: [teacher._id] }); - const class2 = await testObjects.createTestClass({ teacherIds: [teacher._id] }); - - return { teacher, student, class1, class2 }; - }; - - it('should add classmembers and groupmembers to course hook', async () => { - const { class1, class2 } = await setup(); - - const result = addWholeClassToCourse({ - app, - method: 'update', - id: '123', - data: { groupIds: undefined, classIds: [class1._id, class2._id] }, - }); - - expect(result).to.not.equal(undefined); - expect(result.teacherIds).to.have.length(1); - expect(result.userIds).to.have.length(0); - }); - }); - - describe('when FEATURE_GROUPS_IN_COURSE_ENABLED is disabled', async () => { - const setup = async () => { - Configuration.set('FEATURE_GROUPS_IN_COURSE_ENABLED', false); - const teacher = await testObjects.createTestUser({ roles: ['teacher'] }); - const student = await testObjects.createTestUser({ roles: ['student'] }); - const student1 = await testObjects.createTestUser({ roles: ['student'] }); - const student2 = await testObjects.createTestUser({ roles: ['student'] }); - const class1 = await testObjects.createTestClass({ teacherIds: [teacher._id] }); - const group1 = await testObjects.createTestCourseGroup({ - teacherIds: [teacher._id], - userIds: [student._id, student1._id, student2._id], - }); - - return { teacher, student, class1, group1 }; - }; - - it('should add classmembers and groupmembers to course hook', async () => { - const { class1, group1 } = await setup(); - - const result = addWholeClassToCourse({ - app, - method: 'update', - id: '123', - data: { groupIds: [group1._id], classIds: [class1._id] }, - }); - - expect(result.teacherIds).to.not.equal(undefined); - expect(result.teacherIds).to.have.length(1); - expect(result.userIds).to.have.length(0); - }); - }); - }); }); diff --git a/test/services/courses/services/courses.test.js b/test/services/courses/services/courses.test.js index ee6f8afd04c..82aced56e70 100644 --- a/test/services/courses/services/courses.test.js +++ b/test/services/courses/services/courses.test.js @@ -1,5 +1,4 @@ const { expect } = require('chai'); -const { Configuration } = require('@hpi-schul-cloud/commons/lib'); const appPromise = require('../../../../src/app'); const testObjects = require('../../helpers/testObjects')(appPromise()); @@ -9,18 +8,14 @@ describe('course service', () => { let app; let courseService; let nestServices; - let server; - let nestGroupService; before(async () => { app = await appPromise(); nestServices = await setupNestServices(app); courseService = app.service('courses'); - nestGroupService = app.service('nest-group-service'); }); after(async () => { - await server.close(); await closeNestServices(nestServices); }); @@ -91,33 +86,4 @@ describe('course service', () => { expect(err.code).to.eq(403); } }); - - describe('Patch Course', () => { - describe('when FEATURE_GROUPS_IN_COURSE_ENABLED is enabled', async () => { - const setup = async () => { - Configuration.set('FEATURE_GROUPS_IN_COURSE_ENABLED', true); - const teacher = await testObjects.createTestUser({ roles: ['teacher'] }); - const student = await testObjects.createTestUser({ roles: ['student'] }); - const class1 = await testObjects.createTestClass({ teacherIds: [teacher._id] }); - const group1 = await testObjects.createTestCourseGroup({ teacherIds: [teacher._id], userIds: [student._id] }); - - const course = await testObjects.createTestCourse({ - name: 'courseNotChanged', - teacherIds: [teacher._id], - classIds: [class1._id], - groupIds: [group1._id], - }); - const params = await testObjects.generateRequestParamsFromUser(teacher); - - return { teacher, course, params }; - }; - - it('should add whole classmembers and groupmembers to course', async () => { - const { course, params } = await setup(); - const result = await courseService.patch(course._id, { name: 'courseChanged' }, params); - expect(result.name).to.equal('courseChanged'); - expect(result.userIds).to.have.lengthOf(1); - }); - }); - }); }); diff --git a/test/utils/setup.nest.services.js b/test/utils/setup.nest.services.js index 5aef553eb53..4de3bce181f 100644 --- a/test/utils/setup.nest.services.js +++ b/test/utils/setup.nest.services.js @@ -11,12 +11,10 @@ const { AccountService } = require('../../dist/apps/server/modules/account/servi const { AccountValidationService, } = require('../../dist/apps/server/modules/account/services/account.validation.service'); -const { GroupService } = require('../../dist/apps/server/modules/group/service/group.service'); const { DB_PASSWORD, DB_URL, DB_USERNAME } = require('../../dist/apps/server/config/database.config'); const { ALL_ENTITIES } = require('../../dist/apps/server/shared/domain/entity/all-entities'); const { TeamService } = require('../../dist/apps/server/modules/teams/service/team.service'); const { TeamsApiModule } = require('../../dist/apps/server/modules/teams/teams-api.module'); -const { GroupApiModule } = require('../../dist/apps/server/modules/group/group-api.module'); const setupNestServices = async (app) => { const module = await Test.createTestingModule({ @@ -33,7 +31,6 @@ const setupNestServices = async (app) => { ConfigModule.forRoot({ ignoreEnvFile: true, ignoreEnvVars: true, isGlobal: true }), AccountApiModule, TeamsApiModule, - GroupApiModule, ], }).compile(); const nestApp = await module.createNestApplication().init(); @@ -42,16 +39,14 @@ const setupNestServices = async (app) => { const accountService = nestApp.get(AccountService); const accountValidationService = nestApp.get(AccountValidationService); const teamService = nestApp.get(TeamService); - const groupService = nestApp.get(GroupService); app.services['nest-account-uc'] = accountUc; app.services['nest-account-service'] = accountService; app.services['nest-account-validation-service'] = accountValidationService; app.services['nest-team-service'] = teamService; - app.services['nest-group-service'] = groupService; app.services['nest-orm'] = orm; - return { nestApp, orm, accountUc, accountService, groupService }; + return { nestApp, orm, accountUc, accountService }; }; const closeNestServices = async (nestServices) => {