From aab748df1761b296eaf959ac99241f62f42a5550 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 16 Jan 2024 09:06:48 +0100 Subject: [PATCH 1/7] feat: offer course template --- common/achievement/metric.ts | 10 +++ common/achievement/util.ts | 8 ++- common/notification/actions.ts | 20 ++++-- graphql/course/mutations.ts | 18 ++++++ graphql/subcourse/mutations.ts | 5 ++ seed-db.ts | 109 +++++++++++++++++++++++++++++++++ 6 files changed, 165 insertions(+), 5 deletions(-) diff --git a/common/achievement/metric.ts b/common/achievement/metric.ts index f646cb5a8..24a91ef69 100644 --- a/common/achievement/metric.ts +++ b/common/achievement/metric.ts @@ -65,6 +65,16 @@ const batchOfMetrics = [ }), // TODO: add offer course metric listening to 2 actions - screening_success and course_created + /* OFFER COURSE */ + createMetric('student_create_course', ['instructor_course_created'], () => { + return 1; + }), + createMetric('student_submit_course', ['instructor_course_submitted'], () => { + return 1; + }), + createMetric('student_approve_course', ['instructor_course_approved'], () => { + return 1; + }), // TODO: new match metric listening to 2 actions - screening_success and match_requested diff --git a/common/achievement/util.ts b/common/achievement/util.ts index 54678fc8c..01f3b689b 100644 --- a/common/achievement/util.ts +++ b/common/achievement/util.ts @@ -94,7 +94,8 @@ export async function getBucketContext(userID: string, relation?: string): Promi } export function transformPrismaJson(user: User, json: Prisma.JsonValue): AchievementContextType | null { - if (!json['match'] && !json['subcourse']) { + const keys = Object.keys(json); + if (!keys) { return null; } const transformedJson: AchievementContextType = { @@ -102,6 +103,11 @@ export function transformPrismaJson(user: User, json: Prisma.JsonValue): Achieve match: json['match'] ? json['match'] : undefined, subcourse: json['subcourse'] ? json['subcourse'] : undefined, }; + keys.forEach((key) => { + if (key !== 'match' && key !== 'subcourse') { + transformedJson[key] = json[key]; + } + }); return transformedJson; } diff --git a/common/notification/actions.ts b/common/notification/actions.ts index 161b2a2bd..1cc3fb687 100644 --- a/common/notification/actions.ts +++ b/common/notification/actions.ts @@ -210,10 +210,22 @@ const _notificationActions = { instructor_course_created: { description: 'Instructor / Course created (not yet published)', sampleContext: { - course: { - name: 'Hallo Welt', - description: 'Ein Kurs', - }, + courseName: 'Beispielkurs', + relation: 'subcourse/1', + }, + }, + instructor_course_submitted: { + description: 'Instructor / Course submitted for review', + sampleContext: { + courseName: 'Beispielkurs', + relation: 'subcourse/1', + }, + }, + instructor_course_approved: { + description: 'Instructor / Course approved', + sampleContext: { + courseName: 'Beispielkurs', + relation: 'subcourse/1', }, }, instructor_course_cancelled: { diff --git a/graphql/course/mutations.ts b/graphql/course/mutations.ts index f4943b55f..b314f31c4 100644 --- a/graphql/course/mutations.ts +++ b/graphql/course/mutations.ts @@ -11,12 +11,14 @@ import { GraphQLContext } from '../context'; import * as GraphQLModel from '../generated/models'; import { getCourse, getStudent, getSubcoursesForCourse } from '../util'; import { putFile, DEFAULT_BUCKET } from '../../common/file-bucket'; +import * as Notification from '../../common/notification'; import { course_schooltype_enum as CourseSchooltype, course_subject_enum as CourseSubject, course_coursestate_enum as CourseState } from '../generated'; import { ForbiddenError } from '../error'; import { addCourseInstructor, allowCourse, denyCourse, subcourseOver } from '../../common/courses/states'; import { getCourseImageKey } from '../../common/courses/util'; import { createCourseTag } from '../../common/courses/tags'; +import { userForStudent } from '../../common/user'; @InputType() class PublicCourseCreateInput { @@ -193,13 +195,29 @@ export class MutateCourseResolver { await hasAccess(context, 'Course', course); await prisma.course.update({ data: { courseState: 'submitted' }, where: { id: courseId } }); logger.info(`Course (${courseId}) submitted by Student (${context.user.studentId})`); + + const subcourse = await prisma.subcourse.findFirst({ where: { courseId: courseId } }); + await Notification.actionTaken(context.user, 'instructor_course_submitted', { + courseName: course.name, + relation: `subcourse/${subcourse.id}`, + }); + // TODO: Was nutzen? subcourse oder course? Wie unterscheiden die sich? return true; } @Mutation((returns) => Boolean) @Authorized(Role.ADMIN) async courseAllow(@Arg('courseId') courseId: number, @Arg('screeningComment', { nullable: true }) screeningComment?: string | null): Promise { + const course = await getCourse(courseId); await allowCourse(await getCourse(courseId), screeningComment); + + const student = await prisma.course.findUnique({ where: { id: courseId } }).student(); + const subcourse = await prisma.subcourse.findFirst({ where: { courseId: courseId } }); + await Notification.actionTaken(userForStudent(student), 'instructor_course_approved', { + courseName: course.name, + relation: `subcourse/${subcourse.id}`, + }); + return true; } diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index 8b73e2df1..fcb755b7b 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -17,6 +17,7 @@ import { getCourse, getPupil, getStudent, getSubcourse } from '../util'; import { chat_type } from '../generated'; import { markConversationAsReadOnly, removeParticipantFromCourseChat } from '../../common/chat/conversation'; import { sendPupilCoursePromotion } from '../../common/courses/notifications'; +import * as Notification from '../../common/notification'; const logger = getLogger('MutateCourseResolver'); @@ -99,6 +100,10 @@ export class MutateSubcourseResolver { const student = await getSessionStudent(context, studentId); await prisma.subcourse_instructors_student.create({ data: { subcourseId: result.id, studentId: student.id } }); + await Notification.actionTaken(userForStudent(student), 'instructor_course_created', { + courseName: course.name, + relation: `subcourse/${result.id}`, + }); logger.info(`Subcourse(${result.id}) was created for Course(${courseId}) and Student(${student.id})`); return result; } diff --git a/seed-db.ts b/seed-db.ts index 3d71a3ef0..45453e88e 100644 --- a/seed-db.ts +++ b/seed-db.ts @@ -1297,6 +1297,115 @@ void (async function setupDevDB() { }, }); + // STUDENT OFFER COURSE + await prisma.achievement_template.create({ + data: { + name: 'Kurs anbieten', + metrics: ['student_create_course'], + templateFor: achievement_template_for_enum.Course, + group: 'student_offer_course', + groupOrder: 1, + stepName: 'Kurs entwerfen', + type: achievement_type_enum.SEQUENTIAL, + subtitle: 'Vermittle Wissen', + description: 'Dieser Text muss noch geliefert werden.', + image: 'gamification/achievements/tmp/offer_course/offer_course.jpg', + achievedImage: '', + actionName: 'Kurs anlegen', + actionRedirectLink: '/create-course', + actionType: achievement_action_type_enum.Action, + condition: 'student_create_course_events > 0', + conditionDataAggregations: { + student_create_course_events: { + metric: 'student_create_course', + aggregator: 'count', + }, + }, + isActive: true, + }, + }); + await prisma.achievement_template.create({ + data: { + name: 'Kurs anbieten', + metrics: ['student_submit_course'], + templateFor: achievement_template_for_enum.Course, + group: 'student_offer_course', + groupOrder: 2, + stepName: 'Kurs zur Prüfung freigeben', + type: achievement_type_enum.SEQUENTIAL, + subtitle: '{{courseName}}', + description: + 'Dieser Text muss noch geliefert werden! Wie cool, dass du dich ehrenamtlich engagieren möchtest, indem du Schüler:innen durch Nachhilfeunterricht unterstützt. Um mit der Lernunterstützung zu starten sind mehrere Aktionen nötig. Schließe jetzt den nächsten Schritt ab und komme dem Ziel einer neuen Lernunterstüzung ein Stück näher.', + image: 'gamification/achievements/tmp/offer_course/offer_course.jpg', + achievedImage: '', + actionName: 'Kurs freigeben', + actionRedirectLink: '/single-course/{{courseId}}', + actionType: achievement_action_type_enum.Action, + condition: 'student_submit_course_events > 0', + conditionDataAggregations: { + student_submit_course_events: { + metric: 'student_submit_course', + aggregator: 'count', + }, + }, + isActive: true, + }, + }); + await prisma.achievement_template.create({ + data: { + name: 'Kurs anbieten', + metrics: ['student_approve_course'], + templateFor: achievement_template_for_enum.Course, + group: 'student_offer_course', + groupOrder: 3, + stepName: 'Freigabe erhalten', + type: achievement_type_enum.SEQUENTIAL, + subtitle: '{{courseName}}', + description: + 'Dieser Text muss noch geliefert werden! Wie cool, dass du dich ehrenamtlich engagieren möchtest, indem du Schüler:innen durch Nachhilfeunterricht unterstützt. Um mit der Lernunterstützung zu starten sind mehrere Aktionen nötig. Schließe jetzt den nächsten Schritt ab und komme dem Ziel einer neuen Lernunterstüzung ein Stück näher.', + image: 'gamification/achievements/tmp/offer_course/offer_course.jpg', + achievedImage: '', + actionName: 'Kurs absagen', + actionRedirectLink: '/single-course/{{courseId}}', + actionType: achievement_action_type_enum.Wait, + condition: 'student_approve_course_events > 0', + conditionDataAggregations: { + student_approve_course_events: { + metric: 'student_approve_course', + aggregator: 'count', + }, + }, + isActive: true, + }, + }); + await prisma.achievement_template.create({ + data: { + name: 'Kurs anbieten', + metrics: ['student_approve_course'], + templateFor: achievement_template_for_enum.Course, + group: 'student_offer_course', + groupOrder: 4, + stepName: 'Kurs erstellt!', + type: achievement_type_enum.SEQUENTIAL, + subtitle: '{{courseName}}', + description: + 'Dieser Text muss noch geliefert werden! Wie cool, dass du dich ehrenamtlich engagieren möchtest, indem du Schüler:innen durch Nachhilfeunterricht unterstützt. Um mit der Lernunterstützung zu starten sind mehrere Aktionen nötig. Schließe jetzt den nächsten Schritt ab und komme dem Ziel einer neuen Lernunterstüzung ein Stück näher.', + image: 'gamification/achievements/tmp/offer_course/offer_course.jpg', + achievedImage: '', + actionName: null, + actionRedirectLink: null, + actionType: null, + condition: 'student_approve_course_events > 0', + conditionDataAggregations: { + student_approve_course_events: { + metric: 'student_approve_course', + aggregator: 'count', + }, + }, + isActive: true, + }, + }); + // Add Instructors and Participants after adding Lectures, so that they are also added to the lectures: await addSubcourseInstructor(null, subcourse1, student1); await addSubcourseInstructor(null, subcourse1, student2); From 2dddd1265604d674723d193129744bbfd6518d98 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 16 Jan 2024 10:11:21 +0100 Subject: [PATCH 2/7] fix: refactor course created for all instructors --- common/achievement/metric.ts | 1 - common/courses/states.ts | 16 ++++++++++++++++ graphql/course/mutations.ts | 28 +++++++++++++++------------- graphql/subcourse/mutations.ts | 9 ++++++--- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/common/achievement/metric.ts b/common/achievement/metric.ts index 24a91ef69..41ba2bf5c 100644 --- a/common/achievement/metric.ts +++ b/common/achievement/metric.ts @@ -64,7 +64,6 @@ const batchOfMetrics = [ return 1; }), - // TODO: add offer course metric listening to 2 actions - screening_success and course_created /* OFFER COURSE */ createMetric('student_create_course', ['instructor_course_created'], () => { return 1; diff --git a/common/courses/states.ts b/common/courses/states.ts index 2a50c8acc..e355a3950 100644 --- a/common/courses/states.ts +++ b/common/courses/states.ts @@ -21,6 +21,7 @@ import { cancelAppointment } from '../appointment/cancel'; import { User, userForStudent } from '../user'; import { addGroupAppointmentsOrganizer } from '../appointment/participants'; import { sendPupilCoursePromotion, sendSubcourseCancelNotifications } from './notifications'; +import * as Notification from '../../common/notification'; const logger = getLogger('Course States'); @@ -76,6 +77,15 @@ export async function allowCourse(course: Course, screeningComment: string | nul await publishSubcourse(subcourse); } } + + const subcourse = await prisma.subcourse.findFirst({ where: { courseId: course.id } }); + const instructors = await prisma.course_instructors_student.findMany({ where: { courseId: course.id }, select: { student: true } }); + instructors.forEach(async (instructor) => { + await Notification.actionTaken(userForStudent(instructor.student), 'instructor_course_approved', { + courseName: course.name, + relation: `subcourse/${subcourse.id}`, + }); + }); } export async function denyCourse(course: Course, screeningComment: string | null) { @@ -219,6 +229,12 @@ export async function editSubcourse(subcourse: Subcourse, update: Partial { + await Notification.actionTaken(userForStudent(instructor.student), 'instructor_course_created', { + courseName: course.name, + relation: `subcourse/${subcourse.id}`, + }); }); - // TODO: Was nutzen? subcourse oder course? Wie unterscheiden die sich? return true; } @Mutation((returns) => Boolean) @Authorized(Role.ADMIN) async courseAllow(@Arg('courseId') courseId: number, @Arg('screeningComment', { nullable: true }) screeningComment?: string | null): Promise { - const course = await getCourse(courseId); await allowCourse(await getCourse(courseId), screeningComment); - - const student = await prisma.course.findUnique({ where: { id: courseId } }).student(); - const subcourse = await prisma.subcourse.findFirst({ where: { courseId: courseId } }); - await Notification.actionTaken(userForStudent(student), 'instructor_course_approved', { - courseName: course.name, - relation: `subcourse/${subcourse.id}`, - }); - return true; } diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index fcb755b7b..196f9b10c 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -100,9 +100,12 @@ export class MutateSubcourseResolver { const student = await getSessionStudent(context, studentId); await prisma.subcourse_instructors_student.create({ data: { subcourseId: result.id, studentId: student.id } }); - await Notification.actionTaken(userForStudent(student), 'instructor_course_created', { - courseName: course.name, - relation: `subcourse/${result.id}`, + const instructors = await prisma.course_instructors_student.findMany({ where: { courseId }, select: { student: true } }); + instructors.forEach(async (instructor) => { + await Notification.actionTaken(userForStudent(instructor.student), 'instructor_course_created', { + courseName: course.name, + relation: `subcourse/${result.id}`, + }); }); logger.info(`Subcourse(${result.id}) was created for Course(${courseId}) and Student(${student.id})`); return result; From 7775a84addc4836543f17e3a9edc6826c780fb89 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 16 Jan 2024 10:30:49 +0100 Subject: [PATCH 3/7] fix: update achievement context with course --- graphql/course/mutations.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/graphql/course/mutations.ts b/graphql/course/mutations.ts index 663dfa2ea..cc58a3cc2 100644 --- a/graphql/course/mutations.ts +++ b/graphql/course/mutations.ts @@ -108,6 +108,15 @@ export class MutateCourseResolver { } const result = await prisma.course.update({ data, where: { id: courseId } }); logger.info(`Course (${result.id}) updated by Student (${context.user.studentId})`); + + const instructors = await prisma.course_instructors_student.findMany({ where: { courseId }, select: { student: true } }); + const { context: contextData } = await prisma.user_achievement.findFirst({ where: { group: 'student_offer_course' } }); + contextData['courseName'] = result.name; + await prisma.user_achievement.updateMany({ + where: { userId: { in: instructors.map((it) => `student/${it.student}`) } }, + data: { context: { contextData } }, + }); + return result; } From 03ce2c39b1c338e356069b98592bc69a3442fdb7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 16 Jan 2024 14:30:30 +0100 Subject: [PATCH 4/7] fix: instructors, delete funtions --- common/achievement/index.ts | 5 ++++- common/courses/states.ts | 27 ++++++++++++++++++++------- graphql/course/mutations.ts | 33 ++++++++++++++++++--------------- graphql/subcourse/mutations.ts | 21 +++++++++++++-------- 4 files changed, 55 insertions(+), 31 deletions(-) diff --git a/common/achievement/index.ts b/common/achievement/index.ts index 14420a0a3..1fc41aa8b 100644 --- a/common/achievement/index.ts +++ b/common/achievement/index.ts @@ -129,7 +129,10 @@ async function checkUserAchievement(userAchievement: UserAc const evaluationResultValue = typeof evaluationResult.resultObject[dataAggregationKey] === 'number' ? Number(evaluationResult.resultObject[dataAggregationKey]) : null; const awardedAchievement = await rewardUser(evaluationResultValue, userAchievement, event); - await createAchievement(awardedAchievement.template, userAchievement.userId, event.context); + const newAchievement = await createAchievement(awardedAchievement.template, userAchievement.userId, event.context); + if (newAchievement) { + await checkUserAchievement(newAchievement, event); + } } else { await prisma.user_achievement.update({ where: { id: userAchievement.id }, diff --git a/common/courses/states.ts b/common/courses/states.ts index e355a3950..695338916 100644 --- a/common/courses/states.ts +++ b/common/courses/states.ts @@ -79,7 +79,7 @@ export async function allowCourse(course: Course, screeningComment: string | nul } const subcourse = await prisma.subcourse.findFirst({ where: { courseId: course.id } }); - const instructors = await prisma.course_instructors_student.findMany({ where: { courseId: course.id }, select: { student: true } }); + const instructors = await prisma.subcourse_instructors_student.findMany({ where: { subcourseId: subcourse.id }, select: { student: true } }); instructors.forEach(async (instructor) => { await Notification.actionTaken(userForStudent(instructor.student), 'instructor_course_approved', { courseName: course.name, @@ -162,6 +162,20 @@ export async function cancelSubcourse(user: User, subcourse: Subcourse) { } await sendSubcourseCancelNotifications(course, subcourse); logger.info(`Subcourse (${subcourse.id}) was cancelled`); + + const courseInstructors = await prisma.subcourse_instructors_student.findMany({ where: { subcourseId: subcourse.id }, select: { student: true } }); + await prisma.user_achievement.deleteMany({ + where: { + userId: { + in: courseInstructors.map((instructor) => { + const user = userForStudent(instructor.student); + return user.userID; + }), + }, + group: 'student_offer_course', + context: { path: ['relation'], equals: `subcourse/${subcourse.id}` }, + }, + }); } /* --------------- Modify Subcourse ------------------- */ @@ -229,12 +243,6 @@ export async function editSubcourse(subcourse: Subcourse, update: Partial `student/${it.student}`) } }, - data: { context: { contextData } }, + where: { + userId: { + in: instructors.map((it) => { + const user = userForStudent(it.student); + return user.userID; + }), + }, + group: 'student_offer_course', + context: { path: ['relation'], equals: `subcourse/${subcourse.id}` }, + }, + data: { context: contextData }, }); return result; @@ -194,15 +206,6 @@ export class MutateCourseResolver { await prisma.course_instructors_student.delete({ where: { courseId_studentId: { courseId, studentId } } }); logger.info(`Student (${studentId}) was deleted from Course(${courseId}) by User(${context.user.userID})`); - - const metrics = ['student_create_course', 'student_submit_course', 'student_approve_course']; - await prisma.user_achievement.deleteMany({ - where: { - template: { metrics: { hasSome: metrics } }, - userId: `student/${studentId}`, - }, - }); - return true; } @@ -215,9 +218,9 @@ export class MutateCourseResolver { logger.info(`Course (${courseId}) submitted by Student (${context.user.studentId})`); const subcourse = await prisma.subcourse.findFirst({ where: { courseId: courseId } }); - const instructors = await prisma.course_instructors_student.findMany({ where: { courseId }, select: { student: true } }); + const instructors = await prisma.subcourse_instructors_student.findMany({ where: { subcourseId: subcourse.id }, select: { student: true } }); instructors.forEach(async (instructor) => { - await Notification.actionTaken(userForStudent(instructor.student), 'instructor_course_created', { + await Notification.actionTaken(userForStudent(instructor.student), 'instructor_course_submitted', { courseName: course.name, relation: `subcourse/${subcourse.id}`, }); diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index 196f9b10c..0ae8de8b4 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -100,12 +100,9 @@ export class MutateSubcourseResolver { const student = await getSessionStudent(context, studentId); await prisma.subcourse_instructors_student.create({ data: { subcourseId: result.id, studentId: student.id } }); - const instructors = await prisma.course_instructors_student.findMany({ where: { courseId }, select: { student: true } }); - instructors.forEach(async (instructor) => { - await Notification.actionTaken(userForStudent(instructor.student), 'instructor_course_created', { - courseName: course.name, - relation: `subcourse/${result.id}`, - }); + await Notification.actionTaken(userForStudent(student), 'instructor_course_created', { + courseName: course.name, + relation: `subcourse/${result.id}`, }); logger.info(`Subcourse(${result.id}) was created for Course(${courseId}) and Student(${student.id})`); return result; @@ -118,7 +115,6 @@ export class MutateSubcourseResolver { @Arg('subcourseId') subcourseId: number, @Arg('studentId') studentId: number ): Promise { - const { user } = context; const subcourse = await getSubcourse(subcourseId); await hasAccess(context, 'Subcourse', subcourse); @@ -135,7 +131,6 @@ export class MutateSubcourseResolver { @Arg('subcourseId') subcourseId: number, @Arg('studentId') studentId: number ): Promise { - const { user } = context; const subcourse = await getSubcourse(subcourseId); await hasAccess(context, 'Subcourse', subcourse); const instructorToBeRemoved = await getStudent(studentId); @@ -146,6 +141,16 @@ export class MutateSubcourseResolver { await removeParticipantFromCourseChat(instructorUser, subcourse.conversationId); } logger.info(`Student(${studentId}) was deleted from Subcourse(${subcourseId}) by User(${context.user.userID})`); + + const metrics = ['student_create_course', 'student_submit_course', 'student_approve_course']; + await prisma.user_achievement.deleteMany({ + where: { + template: { metrics: { hasSome: metrics } }, + userId: `student/${studentId}`, + context: { path: ['relation'], equals: `subcourse/${subcourseId}` }, + }, + }); + return true; } From 3ac109b31c760f98ecb12c47537a07fe7813e594 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 19 Jan 2024 11:27:24 +0100 Subject: [PATCH 5/7] fix: update with requested changes --- common/achievement/types.ts | 4 ++-- common/achievement/util.ts | 10 ++-------- common/courses/states.ts | 16 +++++++--------- graphql/course/mutations.ts | 15 ++++++++++----- graphql/subcourse/mutations.ts | 2 -- 5 files changed, 21 insertions(+), 26 deletions(-) diff --git a/common/achievement/types.ts b/common/achievement/types.ts index 7b6d9e84e..c4309cd70 100644 --- a/common/achievement/types.ts +++ b/common/achievement/types.ts @@ -114,6 +114,6 @@ export type ContextSubcourse = { export type AchievementContextType = { user?: User; - match: ContextMatch[]; - subcourse: ContextSubcourse[]; + match?: ContextMatch[]; + subcourse?: ContextSubcourse[]; }; diff --git a/common/achievement/util.ts b/common/achievement/util.ts index 01f3b689b..84fcf94ca 100644 --- a/common/achievement/util.ts +++ b/common/achievement/util.ts @@ -98,15 +98,9 @@ export function transformPrismaJson(user: User, json: Prisma.JsonValue): Achieve if (!keys) { return null; } - const transformedJson: AchievementContextType = { - user: user, - match: json['match'] ? json['match'] : undefined, - subcourse: json['subcourse'] ? json['subcourse'] : undefined, - }; + const transformedJson: AchievementContextType = { user: user }; keys.forEach((key) => { - if (key !== 'match' && key !== 'subcourse') { - transformedJson[key] = json[key]; - } + transformedJson[key] = json[key]; }); return transformedJson; } diff --git a/common/courses/states.ts b/common/courses/states.ts index 695338916..b68decdd0 100644 --- a/common/courses/states.ts +++ b/common/courses/states.ts @@ -71,16 +71,18 @@ export async function allowCourse(course: Course, screeningComment: string | nul // Usually when a new course is created, instructors also create a proper subcourse with it // and then forget to publish it after it was approved. Thus we just publish during approval, // assuming the subcourses are ready: - const subcourses = await prisma.subcourse.findMany({ where: { courseId: course.id } }); + const subcourses = await prisma.subcourse.findMany({ + where: { courseId: course.id }, + include: { subcourse_instructors_student: { select: { student: true } } }, + }); for (const subcourse of subcourses) { if (await canPublish(subcourse)) { await publishSubcourse(subcourse); } } - const subcourse = await prisma.subcourse.findFirst({ where: { courseId: course.id } }); - const instructors = await prisma.subcourse_instructors_student.findMany({ where: { subcourseId: subcourse.id }, select: { student: true } }); - instructors.forEach(async (instructor) => { + const [subcourse] = subcourses; + subcourse.subcourse_instructors_student.forEach(async (instructor) => { await Notification.actionTaken(userForStudent(instructor.student), 'instructor_course_approved', { courseName: course.name, relation: `subcourse/${subcourse.id}`, @@ -167,12 +169,8 @@ export async function cancelSubcourse(user: User, subcourse: Subcourse) { await prisma.user_achievement.deleteMany({ where: { userId: { - in: courseInstructors.map((instructor) => { - const user = userForStudent(instructor.student); - return user.userID; - }), + in: courseInstructors.map((instructor) => userForStudent(instructor.student).userID), }, - group: 'student_offer_course', context: { path: ['relation'], equals: `subcourse/${subcourse.id}` }, }, }); diff --git a/graphql/course/mutations.ts b/graphql/course/mutations.ts index 57f7bb488..905741012 100644 --- a/graphql/course/mutations.ts +++ b/graphql/course/mutations.ts @@ -109,8 +109,11 @@ export class MutateCourseResolver { const result = await prisma.course.update({ data, where: { id: courseId } }); logger.info(`Course (${result.id}) updated by Student (${context.user.studentId})`); - const subcourse = await prisma.subcourse.findFirst({ where: { courseId: courseId } }); - const instructors = await prisma.subcourse_instructors_student.findMany({ where: { subcourseId: subcourse.id }, select: { student: true } }); + const subcourse = await prisma.subcourse.findFirst({ + where: { courseId: courseId }, + include: { subcourse_instructors_student: { select: { student: true } } }, + }); + const { subcourse_instructors_student: instructors } = subcourse; const { context: contextData } = await prisma.user_achievement.findFirst({ where: { group: 'student_offer_course', context: { path: ['relation'], equals: `subcourse/${subcourse.id}` } }, }); @@ -217,9 +220,11 @@ export class MutateCourseResolver { await prisma.course.update({ data: { courseState: 'submitted' }, where: { id: courseId } }); logger.info(`Course (${courseId}) submitted by Student (${context.user.studentId})`); - const subcourse = await prisma.subcourse.findFirst({ where: { courseId: courseId } }); - const instructors = await prisma.subcourse_instructors_student.findMany({ where: { subcourseId: subcourse.id }, select: { student: true } }); - instructors.forEach(async (instructor) => { + const subcourse = await prisma.subcourse.findFirst({ + where: { courseId: courseId }, + include: { subcourse_instructors_student: { select: { student: true } } }, + }); + subcourse.subcourse_instructors_student.forEach(async (instructor) => { await Notification.actionTaken(userForStudent(instructor.student), 'instructor_course_submitted', { courseName: course.name, relation: `subcourse/${subcourse.id}`, diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index 0ae8de8b4..7351b4eca 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -142,10 +142,8 @@ export class MutateSubcourseResolver { } logger.info(`Student(${studentId}) was deleted from Subcourse(${subcourseId}) by User(${context.user.userID})`); - const metrics = ['student_create_course', 'student_submit_course', 'student_approve_course']; await prisma.user_achievement.deleteMany({ where: { - template: { metrics: { hasSome: metrics } }, userId: `student/${studentId}`, context: { path: ['relation'], equals: `subcourse/${subcourseId}` }, }, From 464250780e4ca883165d0588f751cbadfb3aaa61 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 22 Jan 2024 09:14:40 +0100 Subject: [PATCH 6/7] fix: update mutation functions for course mutation --- graphql/course/mutations.ts | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/graphql/course/mutations.ts b/graphql/course/mutations.ts index 905741012..107e97e5b 100644 --- a/graphql/course/mutations.ts +++ b/graphql/course/mutations.ts @@ -1,4 +1,4 @@ -import { course_category_enum } from '@prisma/client'; +import { course_category_enum, user_achievement } from '@prisma/client'; import { UserInputError } from 'apollo-server-express'; import { getFile, removeFile } from '../files'; import { getLogger } from '../../common/logger/logger'; @@ -92,6 +92,7 @@ export class MutateCourseResolver { @Arg('course') data: PublicCourseEditInput ): Promise { const course = await getCourse(courseId); + const user = context.user; await hasAccess(context, 'Course', course); if (course.courseState === 'allowed') { @@ -111,25 +112,25 @@ export class MutateCourseResolver { const subcourse = await prisma.subcourse.findFirst({ where: { courseId: courseId }, - include: { subcourse_instructors_student: { select: { student: true } } }, }); - const { subcourse_instructors_student: instructors } = subcourse; - const { context: contextData } = await prisma.user_achievement.findFirst({ - where: { group: 'student_offer_course', context: { path: ['relation'], equals: `subcourse/${subcourse.id}` } }, + const usersSubcourseAchievements = await prisma.user_achievement.findMany({ + where: { context: { path: ['relation'], equals: `subcourse/${subcourse.id}` }, userId: user.userID }, + include: { template: true }, }); - contextData['courseName'] = result.name; - await prisma.user_achievement.updateMany({ - where: { - userId: { - in: instructors.map((it) => { - const user = userForStudent(it.student); - return user.userID; - }), - }, - group: 'student_offer_course', - context: { path: ['relation'], equals: `subcourse/${subcourse.id}` }, - }, - data: { context: contextData }, + const subcourseAchievements = await Promise.all( + usersSubcourseAchievements.map(async (usersSubcourseAchievement) => { + return await prisma.user_achievement.findMany({ + where: { context: { path: ['relation'], equals: `subcourse/${subcourse.id}` }, templateId: usersSubcourseAchievement.template.id }, + }); + }) + ); + subcourseAchievements.flat().forEach(async (achievement) => { + const { context } = achievement; + context['courseName'] = result.name; + await prisma.user_achievement.update({ + where: { id: achievement.id }, + data: { context }, + }); }); return result; From 8598903c51e061cf2fede8763f303c36ddb717d9 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 22 Jan 2024 11:32:36 +0100 Subject: [PATCH 7/7] fix: check for last achievement --- common/achievement/create.ts | 9 ++++++--- common/achievement/index.ts | 5 +---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/common/achievement/create.ts b/common/achievement/create.ts index 5fed0964b..71c1b7485 100644 --- a/common/achievement/create.ts +++ b/common/achievement/create.ts @@ -1,4 +1,4 @@ -import { Prisma } from '@prisma/client'; +import { Prisma, achievement_type_enum } from '@prisma/client'; import { Achievement_template, achievement_template_for_enum } from '../../graphql/generated'; import { ActionID, SpecificNotificationContext } from '../notification/actions'; import { prisma } from '../prisma'; @@ -88,6 +88,10 @@ async function createNextUserAchievement( return; } const nextStepTemplate = templatesForGroup[nextStepIndex]; + const achievedAt = + templatesForGroup.length === nextStepIndex && templatesForGroup[nextStepIndex].type === achievement_type_enum.SEQUENTIAL + ? JSON.stringify(new Date()) + : null; // Here a user template is created for the next template in the group. This is done to always have the data availible for the next step. // This could mean to, for example, have the name of a match partner that is not yet availible due to a unfinished matching process. if (nextStepTemplate) { @@ -99,13 +103,12 @@ async function createNextUserAchievement( context: context ? context : Prisma.JsonNull, template: { connect: { id: nextStepTemplate.id } }, recordValue: nextStepTemplate.type === 'STREAK' ? 0 : null, + achievedAt: achievedAt, }, select: { id: true, userId: true, context: true, template: true, achievedAt: true, recordValue: true }, }); return createdUserAchievement; } - const nextUserAchievement = await createNextUserAchievement(templatesForGroup, nextStepIndex + 1, userId, context); - return nextUserAchievement; } export { getOrCreateUserAchievement, createAchievement }; diff --git a/common/achievement/index.ts b/common/achievement/index.ts index 1fc41aa8b..14420a0a3 100644 --- a/common/achievement/index.ts +++ b/common/achievement/index.ts @@ -129,10 +129,7 @@ async function checkUserAchievement(userAchievement: UserAc const evaluationResultValue = typeof evaluationResult.resultObject[dataAggregationKey] === 'number' ? Number(evaluationResult.resultObject[dataAggregationKey]) : null; const awardedAchievement = await rewardUser(evaluationResultValue, userAchievement, event); - const newAchievement = await createAchievement(awardedAchievement.template, userAchievement.userId, event.context); - if (newAchievement) { - await checkUserAchievement(newAchievement, event); - } + await createAchievement(awardedAchievement.template, userAchievement.userId, event.context); } else { await prisma.user_achievement.update({ where: { id: userAchievement.id },