Skip to content

Commit 2324b30

Browse files
authored
Merge branch 'main' into EW-1061
2 parents 38ff93e + 79deeab commit 2324b30

File tree

16 files changed

+243
-70
lines changed

16 files changed

+243
-70
lines changed

.github/workflows/push.yml

+4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ jobs:
6060
- name: Build and push ${{ github.repository }}
6161
if: ${{ env.IMAGE_EXISTS == 0 }}
6262
uses: docker/build-push-action@v6
63+
env:
64+
DOCKER_BUILD_RECORD_UPLOAD: false
6365
with:
6466
context: .
6567
file: ./Dockerfile
@@ -91,6 +93,8 @@ jobs:
9193
- name: Build and push ${{ github.repository }} (file preview)
9294
if: ${{ env.IMAGE_EXISTS == 0 }}
9395
uses: docker/build-push-action@v6
96+
env:
97+
DOCKER_BUILD_RECORD_UPLOAD: false
9498
with:
9599
build-args: |
96100
BASE_IMAGE=ghcr.io/${{ github.repository }}:${{ needs.branch_meta.outputs.sha }}

.github/workflows/tag.yml

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ jobs:
4040

4141
- name: Build and push ${{ github.repository }}
4242
uses: docker/build-push-action@v6
43+
env:
44+
DOCKER_BUILD_RECORD_UPLOAD: false
4345
with:
4446
context: .
4547
file: ./Dockerfile
@@ -61,6 +63,8 @@ jobs:
6163
org.opencontainers.image.title=schulcloud-file-storage
6264
- name: Build and push ${{ github.repository }} (file-storage)
6365
uses: docker/build-push-action@v6
66+
env:
67+
DOCKER_BUILD_RECORD_UPLOAD: false
6468
with:
6569
build-args: |
6670
BASE_IMAGE=quay.io/schulcloudverbund/schulcloud-server:${{ github.ref_name }}

apps/server/src/infra/schulconnex-client/response/schulconnex-group-role.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export enum SchulconnexGroupRole {
22
TEACHER = 'Lehr',
3+
SUBSTITUTE_TEACHER = 'VLehr',
34
STUDENT = 'Lern',
45
CLASS_LEADER = 'KlLeit',
56
SUPPORT_TEACHER = 'Foerd',

apps/server/src/infra/schulconnex-client/testing/schulconnex-response-factory.ts

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export const schulconnexResponseFactory = Factory.define<SchulconnexResponse>(()
5252
rollen: [SchulconnexGroupRole.STUDENT],
5353
ktid: 'ktid',
5454
},
55+
{
56+
rollen: [SchulconnexGroupRole.SUBSTITUTE_TEACHER],
57+
ktid: 'ktid1',
58+
},
5559
],
5660
},
5761
],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Migration } from '@mikro-orm/migrations-mongodb';
2+
3+
export class Migration20241121180221 extends Migration {
4+
async up(): Promise<void> {
5+
await this.driver.nativeInsert('roles', {
6+
name: 'groupSubstitutionTeacher',
7+
roles: [],
8+
permissions: [],
9+
});
10+
11+
// eslint-disable-next-line no-console
12+
console.info('Added groupSubstitutionTeacher role');
13+
}
14+
15+
async down(): Promise<void> {
16+
await this.driver.nativeDelete('roles', { name: 'groupSubstitutionTeacher' });
17+
18+
// eslint-disable-next-line no-console
19+
console.info('Rollback: Removed groupSubstitutionTeacher role');
20+
}
21+
}

apps/server/src/modules/group/repo/group.repo.spec.ts

+8
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ describe(GroupRepo.name, () => {
7777
userId: group.users[1].user.id,
7878
roleId: group.users[1].role.id,
7979
}),
80+
new GroupUser({
81+
userId: group.users[2].user.id,
82+
roleId: group.users[2].role.id,
83+
}),
8084
],
8185
organizationId: group.organization?.id,
8286
validPeriod: group.validPeriod,
@@ -706,6 +710,10 @@ describe(GroupRepo.name, () => {
706710
userId: groupEntity.users[1].user.id,
707711
roleId: groupEntity.users[1].role.id,
708712
}),
713+
new GroupUser({
714+
userId: groupEntity.users[2].user.id,
715+
roleId: groupEntity.users[2].role.id,
716+
}),
709717
],
710718
organizationId: groupEntity.organization?.id,
711719
validPeriod: groupEntity.validPeriod,

apps/server/src/modules/learnroom/domain/do/course.ts

+4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ export class Course extends DomainObject<CourseProps> {
6767
return this.props.substitutionTeacherIds;
6868
}
6969

70+
set substitutionTeachers(value: EntityId[]) {
71+
this.props.substitutionTeacherIds = value;
72+
}
73+
7074
set classes(value: EntityId[]) {
7175
this.props.classIds = value;
7276
}

apps/server/src/modules/learnroom/service/course-sync.service.spec.ts

+82-18
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ describe(CourseSyncService.name, () => {
386386
};
387387

388388
it('should synchronize with the new group', async () => {
389-
const { course, newGroup, studentId, teacherId, substituteTeacherId } = setup();
389+
const { course, newGroup, studentId, teacherId } = setup();
390390

391391
await service.synchronizeCourseWithGroup(newGroup);
392392

@@ -398,22 +398,93 @@ describe(CourseSyncService.name, () => {
398398
untilDate: newGroup.validPeriod?.until,
399399
studentIds: [studentId],
400400
teacherIds: [teacherId],
401+
substitutionTeacherIds: [],
401402
classIds: [],
402403
groupIds: [],
404+
}),
405+
]);
406+
});
407+
});
408+
409+
describe('when synchronizing with a new group with substitute teacher', () => {
410+
const setup = () => {
411+
const studentId: string = new ObjectId().toHexString();
412+
const teacherId: string = new ObjectId().toHexString();
413+
const substituteTeacherId: string = new ObjectId().toHexString();
414+
const studentRoleId: string = new ObjectId().toHexString();
415+
const teacherRoleId: string = new ObjectId().toHexString();
416+
const substituteTeacherRoleId: string = new ObjectId().toHexString();
417+
const studentRole: RoleDto = roleDtoFactory.build({ id: studentRoleId });
418+
const teacherRole: RoleDto = roleDtoFactory.build({ id: teacherRoleId });
419+
const substituteTeacherRole: RoleDto = roleDtoFactory.build({ id: substituteTeacherRoleId });
420+
const newGroup: Group = groupFactory.build({
421+
users: [
422+
{
423+
userId: studentId,
424+
roleId: studentRoleId,
425+
},
426+
{
427+
userId: teacherId,
428+
roleId: teacherRoleId,
429+
},
430+
{
431+
userId: substituteTeacherId,
432+
roleId: substituteTeacherRoleId,
433+
},
434+
{
435+
userId: teacherId,
436+
roleId: substituteTeacherRoleId,
437+
},
438+
],
439+
});
440+
const course: Course = courseFactory.build({
441+
classIds: [new ObjectId().toHexString()],
442+
groupIds: [new ObjectId().toHexString()],
443+
substitutionTeacherIds: [],
444+
});
445+
446+
courseRepo.findBySyncedGroup.mockResolvedValueOnce([new Course(course.getProps())]);
447+
roleService.findByName
448+
.mockResolvedValueOnce(studentRole)
449+
.mockResolvedValueOnce(teacherRole)
450+
.mockResolvedValueOnce(substituteTeacherRole);
451+
452+
return {
453+
course,
454+
newGroup,
455+
studentId,
456+
teacherId,
457+
substituteTeacherId,
458+
};
459+
};
460+
461+
it('should synchronize the substitution teachers, without creating duplicates in teacherIds', async () => {
462+
const { course, newGroup, studentId, teacherId, substituteTeacherId } = setup();
463+
464+
await service.synchronizeCourseWithGroup(newGroup);
465+
466+
expect(courseRepo.saveAll).toHaveBeenCalledWith<[Course[]]>([
467+
new Course({
468+
...course.getProps(),
469+
syncedWithGroup: newGroup.id,
470+
startDate: newGroup.validPeriod?.from,
471+
untilDate: newGroup.validPeriod?.until,
472+
studentIds: [studentId],
473+
teacherIds: [teacherId],
403474
substitutionTeacherIds: [substituteTeacherId],
475+
classIds: [],
476+
groupIds: [],
404477
}),
405478
]);
406479
});
407480
});
408481

409482
describe('when the course name is the same as the old group name', () => {
410483
const setup = () => {
411-
const substituteTeacherId = new ObjectId().toHexString();
412484
const course: Course = courseFactory.build({
413485
name: 'Course Name',
414486
classIds: [new ObjectId().toHexString()],
415487
groupIds: [new ObjectId().toHexString()],
416-
substitutionTeacherIds: [substituteTeacherId],
417488
});
418489
const studentRole: RoleDto = roleDtoFactory.build();
419490
const teacherRole: RoleDto = roleDtoFactory.build();
@@ -431,12 +502,11 @@ describe(CourseSyncService.name, () => {
431502
course,
432503
newGroup,
433504
oldGroup,
434-
substituteTeacherId,
435505
};
436506
};
437507

438508
it('should synchronize the group name', async () => {
439-
const { course, newGroup, oldGroup, substituteTeacherId } = setup();
509+
const { course, newGroup, oldGroup } = setup();
440510

441511
await service.synchronizeCourseWithGroup(newGroup, oldGroup);
442512
expect(courseRepo.saveAll).toHaveBeenCalledWith<[Course[]]>([
@@ -450,20 +520,18 @@ describe(CourseSyncService.name, () => {
450520
teacherIds: [],
451521
classIds: [],
452522
groupIds: [],
453-
substitutionTeacherIds: [substituteTeacherId],
523+
substitutionTeacherIds: [],
454524
}),
455525
]);
456526
});
457527
});
458528

459529
describe('when the course name is different from the old group name', () => {
460530
const setup = () => {
461-
const substituteTeacherId = new ObjectId().toHexString();
462531
const course: Course = courseFactory.build({
463532
name: 'Custom Course Name',
464533
classIds: [new ObjectId().toHexString()],
465534
groupIds: [new ObjectId().toHexString()],
466-
substitutionTeacherIds: [substituteTeacherId],
467535
});
468536
const studentRole: RoleDto = roleDtoFactory.build();
469537
const teacherRole: RoleDto = roleDtoFactory.build();
@@ -481,12 +549,11 @@ describe(CourseSyncService.name, () => {
481549
course,
482550
newGroup,
483551
oldGroup,
484-
substituteTeacherId,
485552
};
486553
};
487554

488555
it('should keep the old course name', async () => {
489-
const { course, newGroup, oldGroup, substituteTeacherId } = setup();
556+
const { course, newGroup, oldGroup } = setup();
490557

491558
await service.synchronizeCourseWithGroup(newGroup, oldGroup);
492559
expect(courseRepo.saveAll).toHaveBeenCalledWith<[Course[]]>([
@@ -500,15 +567,14 @@ describe(CourseSyncService.name, () => {
500567
teacherIds: [],
501568
classIds: [],
502569
groupIds: [],
503-
substitutionTeacherIds: [substituteTeacherId],
570+
substitutionTeacherIds: [],
504571
}),
505572
]);
506573
});
507574
});
508575

509576
describe('when the teachers are not synced from group', () => {
510577
const setup = () => {
511-
const substituteTeacherId = new ObjectId().toHexString();
512578
const studentUserId = new ObjectId().toHexString();
513579
const teacherUserId = new ObjectId().toHexString();
514580
const studentRoleId: string = new ObjectId().toHexString();
@@ -526,7 +592,6 @@ describe(CourseSyncService.name, () => {
526592

527593
const course: Course = courseFactory.build({
528594
syncedWithGroup: newGroup.id,
529-
substitutionTeacherIds: [substituteTeacherId],
530595
teacherIds: [teacherUserId],
531596
excludeFromSync: [],
532597
});
@@ -538,13 +603,12 @@ describe(CourseSyncService.name, () => {
538603
course,
539604
newGroup,
540605
teacherUserId,
541-
substituteTeacherId,
542606
studentUserId,
543607
};
544608
};
545609

546610
it('should not sync group students', async () => {
547-
const { course, newGroup, teacherUserId, substituteTeacherId } = setup();
611+
const { course, newGroup, teacherUserId } = setup();
548612

549613
await service.synchronizeCourseWithGroup(newGroup);
550614
expect(courseRepo.saveAll).toHaveBeenCalledWith<[Course[]]>([
@@ -559,7 +623,7 @@ describe(CourseSyncService.name, () => {
559623
classIds: [],
560624
groupIds: [],
561625
excludeFromSync: [],
562-
substitutionTeacherIds: [substituteTeacherId],
626+
substitutionTeacherIds: [],
563627
}),
564628
]);
565629
});
@@ -607,7 +671,7 @@ describe(CourseSyncService.name, () => {
607671
};
608672

609673
it('should not sync group teachers', async () => {
610-
const { course, newGroup, substituteTeacherId, teacherUserId, studentUserId } = setup();
674+
const { course, newGroup, teacherUserId, studentUserId } = setup();
611675

612676
await service.synchronizeCourseWithGroup(newGroup);
613677
expect(courseRepo.saveAll).toHaveBeenCalledWith<[Course[]]>([
@@ -622,7 +686,7 @@ describe(CourseSyncService.name, () => {
622686
classIds: [],
623687
groupIds: [],
624688
excludeFromSync: [SyncAttribute.TEACHERS],
625-
substitutionTeacherIds: [substituteTeacherId],
689+
substitutionTeacherIds: [],
626690
}),
627691
]);
628692
});

apps/server/src/modules/learnroom/service/course-sync.service.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,21 @@ export class CourseSyncService {
5757
}
5858

5959
private async synchronize(courses: Course[], group: Group, oldGroup?: Group): Promise<void> {
60-
const [studentRole, teacherRole] = await Promise.all([
60+
const [studentRole, teacherRole, substituteTeacherRole] = await Promise.all([
6161
this.roleService.findByName(RoleName.STUDENT),
6262
this.roleService.findByName(RoleName.TEACHER),
63+
this.roleService.findByName(RoleName.GROUPSUBSTITUTIONTEACHER),
6364
]);
6465

65-
const studentIds = group.users
66+
const studentIds: EntityId[] = group.users
6667
.filter((user: GroupUser) => user.roleId === studentRole.id)
67-
.map((student) => student.userId);
68-
const teacherIds = group.users
68+
.map((student: GroupUser) => student.userId);
69+
const teacherIds: EntityId[] = group.users
6970
.filter((user: GroupUser) => user.roleId === teacherRole.id)
70-
.map((teacher) => teacher.userId);
71+
.map((teacher: GroupUser) => teacher.userId);
72+
const substituteTeacherIds: EntityId[] = group.users
73+
.filter((user: GroupUser) => user.roleId === substituteTeacherRole.id)
74+
.map((substituteTeacher: GroupUser) => substituteTeacher.userId);
7175

7276
for (const course of courses) {
7377
course.syncedWithGroup = group.id;
@@ -80,14 +84,22 @@ export class CourseSyncService {
8084
course.name = group.name;
8185
}
8286

83-
const excludedFromSync = new Set(course.excludeFromSync || []);
87+
const excludedFromSync: Set<SyncAttribute> = new Set(course.excludeFromSync || []);
8488

8589
if (excludedFromSync.has(SyncAttribute.TEACHERS)) {
8690
course.students = studentIds;
8791
} else {
8892
course.teachers = teacherIds.length > 0 ? teacherIds : course.teachers;
8993
course.students = teacherIds.length > 0 ? studentIds : [];
9094
}
95+
96+
// To ensure unique teachers per course, filter out already assigned teachers from the substitution teacher list
97+
const teacherSet: Set<EntityId> = new Set(course.teachers);
98+
const filteredSubstituteTeacherIds: string[] = substituteTeacherIds.filter(
99+
(substituteTeacherId: EntityId) => !teacherSet.has(substituteTeacherId)
100+
);
101+
102+
course.substitutionTeachers = filteredSubstituteTeacherIds;
91103
}
92104

93105
await this.courseRepo.saveAll(courses);

0 commit comments

Comments
 (0)