Skip to content

Commit 9e8cca9

Browse files
authored
Merge branch 'master' into feat-stats-dissolvedby-differentiate
2 parents f795d26 + 36ef43a commit 9e8cca9

File tree

32 files changed

+296
-87
lines changed

32 files changed

+296
-87
lines changed

.certificate-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
21a45888d9a49b70b1e98740eadfb745e2aacf84
1+
e2769b5f644fb1aa1b82b94df09d1aa8ab5a7c21

assets/instantCertificateTemplate.de.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ <h2>Deutsches Sofort-Zertifikat</h2>
1414
<%= NAMESTUDENT %> unterstützte <%= MATCHES_COUNT %> Schüler:innen in der 1:1 Lernunterstützung und führte <%= MATCH_APPOINTMENTS_COUNT %> Nachhilfetermine durch. Zudem agierte <%= NAMESTUDENT %> als Gruppenkurs-Leiter und unterstütze so <%= COURSE_PARTICIPANTS_COUNT %> Schüler:innen in <%= COURSE_APPOINTMENTS_COUNT %> Lektionen.
1515

1616
Insgesamt unterstützte <%= NAMESTUDENT %> bildungsbenachteiligte Kinder und Jugendliche in <%= TOTAL_APPOINTMENTS_DURATION %> Stunden in dieser Zeit.
17+
18+
<% if(HOMEWORK_HELP_DURATION) { %>
19+
Zusätzlich unterstützte <%= NAMESTUDENT %> Schüler:innen in der Hausaufgabenhilfe im Umfang von <%= HOMEWORK_HELP_DURATION %> Stunden.
20+
<% } %>
1721
<br/>
1822
<img src="<%=QR_CODE%>" alt="Verifizierung code" width="100px" />
1923

common/appointment/util.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as ics from 'ics';
44
import { EventAttributes } from 'ics';
55
import moment from 'moment-timezone';
66
import { prisma } from '../prisma';
7-
import { getUsers } from '../user';
7+
import { getUsers, UserID } from '../user';
88
import { getLogger } from '../logger/logger';
99

1010
const language = 'de-DE';
@@ -59,10 +59,10 @@ export async function getDisplayName(appointment: Appointment, isOrganizer: bool
5959
switch (appointment.appointmentType) {
6060
case lecture_appointmenttype_enum.match: {
6161
if (isOrganizer) {
62-
const [tutee] = await getUsers(appointment.participantIds);
62+
const [tutee] = await getUsers(appointment.participantIds as UserID[]);
6363
return `${tutee.firstname} ${tutee.lastname}`;
6464
} else {
65-
const [tutor] = await getUsers(appointment.organizerIds);
65+
const [tutor] = await getUsers(appointment.organizerIds as UserID[]);
6666
return `${tutor.firstname} ${tutor.lastname}`;
6767
}
6868
}

common/certificate-of-conduct/certificateOfConduct.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { prisma } from '../prisma';
33
import { deactivateStudent } from '../student/activation';
44
import { getStudent } from '../../graphql/util';
55
import * as Notification from '../notification';
6-
import { userForStudent } from '../user';
6+
import { DeactivationReason, userForStudent } from '../user';
77
import moment from 'moment';
88
import { cancelRemissionRequest } from '../remission-request';
99

@@ -25,7 +25,7 @@ export async function create(dateOfInspection: Date, dateOfIssue: Date, criminal
2525
expirationDate: moment(dateOfIssue).add(3, 'years').format('DD.MM.YYYY'),
2626
});
2727
if (criminalRecords) {
28-
await deactivateStudent(student);
28+
await deactivateStudent(student, false, DeactivationReason.hasCriminalRecord);
2929
} else {
3030
await Notification.actionTaken(userForStudent(student), 'student_coc_approved', {});
3131
}

common/certificate/index.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { randomBytes } from 'crypto';
55
import EJS from 'ejs';
66
import * as Notification from '../notification';
77
import {
8+
course_category_enum,
89
instant_certificate as InstantCertificate,
910
participation_certificate as ParticipationCertificate,
1011
Prisma,
@@ -124,13 +125,24 @@ export async function createInstantCertificate(requester: Student, lang: Languag
124125
_sum: { duration: true },
125126
});
126127

127-
const [matchesCount, matchAppointmentsCount, courseParticipants, courseAppointmentsCount, totalAppointmentsDuration] = await Promise.all([
128-
matchesCountPromise,
129-
matchAppointmentsCountPromise,
130-
uniqueCourseParticipantsPromise,
131-
courseAppointmentsCountPromise,
132-
totalAppointmentsDurationPromise,
133-
]);
128+
const homeworkHelpDurationPromise = prisma.lecture.aggregate({
129+
where: {
130+
isCanceled: false,
131+
subcourse: { course: { category: course_category_enum.homework_help } },
132+
joinedBy: { has: userForStudent(requester).userID },
133+
},
134+
_sum: { duration: true },
135+
});
136+
137+
const [matchesCount, matchAppointmentsCount, courseParticipants, courseAppointmentsCount, totalAppointmentsDuration, homeworkHelpDuration] =
138+
await Promise.all([
139+
matchesCountPromise,
140+
matchAppointmentsCountPromise,
141+
uniqueCourseParticipantsPromise,
142+
courseAppointmentsCountPromise,
143+
totalAppointmentsDurationPromise,
144+
homeworkHelpDurationPromise,
145+
]);
134146
const courseParticipantsCount = courseParticipants.length;
135147

136148
const certificate = await prisma.instant_certificate.create({
@@ -143,6 +155,7 @@ export async function createInstantCertificate(requester: Student, lang: Languag
143155
courseParticipantsCount,
144156
courseAppointmentsCount,
145157
totalAppointmentsDuration: totalAppointmentsDuration._sum.duration ?? 0,
158+
homeworkHelpDuration: homeworkHelpDuration._sum.duration === 0 ? undefined : homeworkHelpDuration._sum.duration,
146159
},
147160
include: { student: true },
148161
});
@@ -265,6 +278,7 @@ export async function getConfirmationPage(certificateId: string, lang: Language,
265278
COURSE_PARTICIPANTS_COUNT: certificate.courseParticipantsCount,
266279
COURSE_APPOINTMENTS_COUNT: certificate.courseAppointmentsCount,
267280
TOTAL_APPOINTMENTS_DURATION: formatFloat(certificate.totalAppointmentsDuration / 60, lang),
281+
HOMEWORK_HELP_DURATION: certificate.homeworkHelpDuration ? formatFloat(certificate.homeworkHelpDuration / 60, lang) : undefined,
268282
DATUMHEUTE: moment(certificate.createdAt).format('D.M.YYYY'),
269283
});
270284
}
@@ -471,6 +485,7 @@ async function createInstantPDFBinary(certificate: InstantCertificate & { studen
471485
COURSE_PARTICIPANTS_COUNT: certificate.courseParticipantsCount,
472486
COURSE_APPOINTMENTS_COUNT: certificate.courseAppointmentsCount,
473487
TOTAL_APPOINTMENTS_DURATION: formatFloat(certificate.totalAppointmentsDuration / 60, lang),
488+
HOMEWORK_HELP_DURATION: certificate.homeworkHelpDuration ? formatFloat(certificate.homeworkHelpDuration / 60, lang) : undefined,
474489
DATUMHEUTE: moment().format('D.M.YYYY'),
475490
QR_CODE: await QRCode.toDataURL(link),
476491
});

common/chat/create.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { User } from '../user';
1+
import { User, UserID } from '../user';
22
import { ChatMetaData, ContactReason, Conversation, ConversationInfos, FinishedReason, SystemMessage, TJConversation } from './types';
33
import { checkChatMembersAccessRights, convertTJConversation, createOneOnOneId, userIdToTalkJsId } from './helper';
44
import { createConversation, getConversation, markConversationAsWriteable, sendSystemMessage, updateConversation } from './conversation';
@@ -10,6 +10,15 @@ import { getLogger } from '../logger/logger';
1010
import assert from 'assert';
1111
import { createHmac } from 'crypto';
1212

13+
// We do not replicate TalkJS data in the Backend but instead always fetch their REST API which is particularily slow.
14+
// As Users cannot be deleted in TalkJS, if a user once had an account, we can cache their TalkJS signature.
15+
const signatureCache = new Map<UserID, string>();
16+
17+
// If it returns true, the user definetly has a TalkJS user. If it returns false, the TalkJS API needs to be checked
18+
export function hasCachedChatUser(user: User) {
19+
return signatureCache.has(user.userID);
20+
}
21+
1322
const logger = getLogger('Chat');
1423
const getOrCreateOneOnOneConversation = async (
1524
participants: [User, User],
@@ -57,19 +66,28 @@ const getOrCreateOneOnOneConversation = async (
5766

5867
async function ensureChatUsersExist(participants: [User, User] | User[]): Promise<void> {
5968
await Promise.all(
60-
participants.map(async (participant) => {
61-
await getOrCreateChatUser(participant);
62-
})
69+
participants
70+
.filter((it) => !hasCachedChatUser(it))
71+
.map(async (participant) => {
72+
await getOrCreateChatUser(participant);
73+
})
6374
);
6475
}
6576

6677
const createChatSignature = async (user: User): Promise<string> => {
78+
if (signatureCache.has(user.userID)) {
79+
return signatureCache.get(user.userID);
80+
}
81+
6782
const TALKJS_SECRET_KEY = process.env.TALKJS_API_KEY;
6883
assert(TALKJS_SECRET_KEY, `No TalkJS secret key to create a chat signature for user ${user.userID}.`);
6984
const userId = (await getOrCreateChatUser(user)).id;
7085
const key = TALKJS_SECRET_KEY;
7186
const hash = createHmac('sha256', key).update(userIdToTalkJsId(userId));
72-
return hash.digest('hex');
87+
const signature = hash.digest('hex');
88+
89+
signatureCache.set(user.userID, signature);
90+
return signature;
7391
};
7492

7593
async function handleExistingConversation(

common/chat/helper.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
import { match } from '@prisma/client';
22
import { prisma } from '../prisma';
3-
import { User, getUser } from '../user';
3+
import { User, UserID, getUser } from '../user';
44
import { sha1 } from 'object-hash';
55
import { truncate } from 'lodash';
66
import { Subcourse } from '../../graphql/generated';
77
import { ChatMetaData, Conversation, ConversationInfos, TJConversation } from './types';
88
import { type MatchContactPupil, type MatchContactStudent } from './contacts';
99

1010
type TalkJSUserId = `${'pupil' | 'student'}_${number}`;
11-
export type UserId = `${'pupil' | 'student'}/${number}`;
1211

1312
const userIdToTalkJsId = (userId: string): TalkJSUserId => {
1413
return userId.replace('/', '_') as TalkJSUserId;
1514
};
1615

17-
const talkJsIdToUserId = (userId: string): UserId => {
18-
return userId.replace('_', '/') as UserId;
16+
const talkJsIdToUserId = (userId: string): UserID => {
17+
return userId.replace('_', '/') as UserID;
1918
};
2019

2120
function createOneOnOneId(userA: Pick<User, 'userID'>, userB: Pick<User, 'userID'>): string {

common/notification/actions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,10 +568,18 @@ const _notificationActions = {
568568
description: 'Pupil / Account deactivated by admin',
569569
sampleContext: {},
570570
},
571+
pupil_account_deactivated_no_more_interest: {
572+
description: 'Pupil / Account deactivated - no more interest',
573+
sampleContext: {},
574+
},
571575
student_account_deactivated: {
572576
description: 'Student / Account deactivated',
573577
sampleContext: {},
574578
},
579+
student_account_deactivated_no_more_interest: {
580+
description: 'Student / Account deactivated - no more interest',
581+
sampleContext: {},
582+
},
575583

576584
'user-verify-email': {
577585
description: 'User / Verify E-Mail',

common/notification/hooks.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@ import { registerPupilHook, registerStudentHook } from './hook';
55
import { deactivateStudent } from '../student/activation';
66
import { cancelRemissionRequest } from '../remission-request';
77
import { prisma } from '../prisma';
8-
import { userForStudent } from '../user';
8+
import { DeactivationReason, userForStudent } from '../user';
99
import * as Notification from '../../common/notification';
1010
import { SpecificNotificationContext } from './actions';
1111
import { dissolve_reason } from '@prisma/client';
12+
import { deletePupilMatchRequest } from '../match/request';
13+
import { deactivatePupil } from '../pupil/activation';
1214

1315
registerStudentHook(
1416
'deactivate-student',
1517
'Account gets deactivated, matches are dissolved, courses are cancelled',
1618
async (student) => {
17-
await deactivateStudent(student, true, 'missing coc', [dissolve_reason.accountDeactivatedNoCoC]);
19+
await deactivateStudent(student, true, DeactivationReason.missingCoC, undefined, [dissolve_reason.accountDeactivatedNoCoC]);
1820
} // the hook does not send out a notification again, the user already knows that their account was deactivated
1921
);
2022

@@ -44,13 +46,10 @@ registerStudentHook(
4446
}
4547
);
4648

47-
import { deletePupilMatchRequest } from '../match/request';
48-
import { deactivatePupil } from '../pupil/activation';
49-
5049
registerPupilHook('revoke-pupil-match-request', 'Match Request is taken back, pending Pupil Screenings are invalidated', async (pupil) => {
5150
await deletePupilMatchRequest(pupil);
5251
});
5352

5453
registerPupilHook('deactivate-pupil', 'Account gets deactivated, matches are dissolved, courses are left', async (pupil) => {
55-
await deactivatePupil(pupil, true, 'deactivated by admin', true);
54+
await deactivatePupil(pupil, true, DeactivationReason.deactivatedByAdmin, undefined, true);
5655
});

common/pupil/activation.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { dissolveMatch } from '../match/dissolve';
44
import { RedundantError } from '../util/error';
55
import * as Notification from '../notification';
66
import { logTransaction } from '../transactionlog/log';
7-
import { userForPupil } from '../user';
7+
import { DeactivationReason, userForPupil } from '../user';
88
import { dissolved_by_enum } from '../../graphql/generated';
99
import { leaveSubcourse } from '../courses/participants';
1010
import { getLogger } from '../logger/logger';
@@ -28,13 +28,18 @@ export async function activatePupil(pupil: Pupil) {
2828
return updatedPupil;
2929
}
3030

31-
export async function deactivatePupil(pupil: Pupil, silent = false, reason?: string, byAdmin = false) {
31+
export async function deactivatePupil(pupil: Pupil, silent = false, reason?: DeactivationReason, otherReason?: string, byAdmin = false) {
3232
if (!pupil.active) {
3333
throw new RedundantError('Pupil was already deactivated');
3434
}
3535

3636
if (!silent) {
37-
const action = byAdmin ? 'pupil_account_deactivated_by_admin' : 'pupil_account_deactivated';
37+
let action;
38+
if (reason === DeactivationReason.noMoreInterest) {
39+
action = 'pupil_account_deactivated_no_more_interest';
40+
} else {
41+
action = byAdmin ? 'pupil_account_deactivated_by_admin' : 'pupil_account_deactivated';
42+
}
3843
await Notification.actionTaken(userForPupil(pupil), action, {});
3944
}
4045

@@ -74,7 +79,7 @@ export async function deactivatePupil(pupil: Pupil, silent = false, reason?: str
7479
where: { id: pupil.id },
7580
});
7681

77-
await logTransaction('deActivate', userForPupil(pupil), { newStatus: false, deactivationReason: reason });
82+
await logTransaction('deActivate', userForPupil(pupil), { newStatus: false, deactivationReason: reason, otherReason });
7883
logger.info(`Deactivated Pupil(${pupil.id})`);
7984

8085
return updatedPupil;

0 commit comments

Comments
 (0)