Skip to content

Commit

Permalink
N21-939-assign-groups-to-course (#4464)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrikallab authored Oct 16, 2023
1 parent 264d8ff commit 2fc165e
Show file tree
Hide file tree
Showing 18 changed files with 195 additions and 22 deletions.
3 changes: 3 additions & 0 deletions apps/server/src/apps/server.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -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',
Expand All @@ -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);
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/modules/group/uc/dto/class-info.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 4 additions & 0 deletions apps/server/src/modules/group/uc/dto/class-root-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum ClassRootType {
CLASS = 'class',
GROUP = 'group',
}
42 changes: 37 additions & 5 deletions apps/server/src/modules/group/uc/group.uc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -16,14 +17,15 @@ 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';
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', () => {
Expand All @@ -37,6 +39,7 @@ describe('GroupUc', () => {
let roleService: DeepMocked<RoleService>;
let schoolService: DeepMocked<LegacySchoolService>;
let authorizationService: DeepMocked<AuthorizationService>;
let schoolYearService: DeepMocked<SchoolYearService>;

beforeAll(async () => {
module = await Test.createTestingModule({
Expand Down Expand Up @@ -70,6 +73,10 @@ describe('GroupUc', () => {
provide: AuthorizationService,
useValue: createMock<AuthorizationService>(),
},
{
provide: SchoolYearService,
useValue: createMock<SchoolYearService>(),
},
],
}).compile();

Expand All @@ -81,6 +88,7 @@ describe('GroupUc', () => {
roleService = module.get(RoleService);
schoolService = module.get(LegacySchoolService);
authorizationService = module.get(AuthorizationService);
schoolYearService = module.get(SchoolYearService);

await setupEntities();
});
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -191,6 +205,7 @@ describe('GroupUc', () => {

throw new Error();
});
schoolYearService.findById.mockResolvedValue(schoolYear);

return {
teacherUser,
Expand All @@ -199,6 +214,7 @@ describe('GroupUc', () => {
group,
groupWithSystem,
system,
schoolYear,
};
};

Expand All @@ -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<ClassInfoDto> = await uc.findAllClassesForSchool(teacherUser.id, teacherUser.school.id);

expect(result).toEqual<Page<ClassInfoDto>>({
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],
},
Expand All @@ -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<ClassInfoDto> = await uc.findAllClassesForSchool(
teacherUser.id,
Expand All @@ -261,17 +284,24 @@ describe('GroupUc', () => {
expect(result).toEqual<Page<ClassInfoDto>>({
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],
},
],
Expand All @@ -296,7 +326,9 @@ describe('GroupUc', () => {
expect(result).toEqual<Page<ClassInfoDto>>({
data: [
{
id: group.id,
name: group.name,
type: ClassRootType.GROUP,
teachers: [teacherUser.lastName],
},
],
Expand Down
14 changes: 10 additions & 4 deletions apps/server/src/modules/group/uc/group.uc.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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(
Expand Down Expand Up @@ -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;
})
Expand Down
Loading

0 comments on commit 2fc165e

Please sign in to comment.