Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: offer course template #951

Merged
merged 9 commits into from
Jan 23, 2024
9 changes: 6 additions & 3 deletions common/achievement/create.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -88,6 +88,10 @@ async function createNextUserAchievement<ID extends ActionID>(
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) {
Expand All @@ -99,13 +103,12 @@ async function createNextUserAchievement<ID extends ActionID>(
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 };
11 changes: 10 additions & 1 deletion common/achievement/metric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,16 @@ 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;
}),
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

Expand Down
4 changes: 2 additions & 2 deletions common/achievement/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,6 @@ export type ContextSubcourse = {

export type AchievementContextType = {
user?: User;
match: ContextMatch[];
subcourse: ContextSubcourse[];
match?: ContextMatch[];
subcourse?: ContextSubcourse[];
Comment on lines +117 to +118
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these optional now? 🤔

};
12 changes: 6 additions & 6 deletions common/achievement/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,14 @@ 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;
}
dhenkel92 marked this conversation as resolved.
Show resolved Hide resolved
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) => {
transformedJson[key] = json[key];
});
return transformedJson;
}
export async function getUserAchievementWithTemplate(id: number) {
Expand Down
29 changes: 28 additions & 1 deletion common/courses/states.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -70,12 +71,23 @@ 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] = subcourses;
subcourse.subcourse_instructors_student.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) {
Expand Down Expand Up @@ -152,6 +164,16 @@ 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) => userForStudent(instructor.student).userID),
},
context: { path: ['relation'], equals: `subcourse/${subcourse.id}` },
},
});
}

/* --------------- Modify Subcourse ------------------- */
Expand Down Expand Up @@ -231,5 +253,10 @@ export async function addSubcourseInstructor(user: User | null, subcourse: Subco
await addParticipant(newInstructorUser, subcourse.conversationId, subcourse.groupChatType as ChatType);
}

const { name } = await prisma.course.findUnique({ where: { id: subcourse.courseId }, select: { name: true } });
await Notification.actionTaken(userForStudent(newInstructor), 'instructor_course_created', {
courseName: name,
relation: `subcourse/${subcourse.id}`,
});
logger.info(`Student (${newInstructor.id}) was added as an instructor to Subcourse(${subcourse.id}) by User(${user?.userID})`);
}
20 changes: 16 additions & 4 deletions common/notification/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
40 changes: 39 additions & 1 deletion graphql/course/mutations.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -90,6 +92,7 @@ export class MutateCourseResolver {
@Arg('course') data: PublicCourseEditInput
): Promise<GraphQLModel.Course> {
const course = await getCourse(courseId);
const user = context.user;
await hasAccess(context, 'Course', course);

if (course.courseState === 'allowed') {
Expand All @@ -106,6 +109,30 @@ 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 usersSubcourseAchievements = await prisma.user_achievement.findMany({
where: { context: { path: ['relation'], equals: `subcourse/${subcourse.id}` }, userId: user.userID },
include: { template: true },
});
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;
}

Expand Down Expand Up @@ -193,6 +220,17 @@ 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 },
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}`,
});
});
return true;
}

Expand Down
15 changes: 13 additions & 2 deletions graphql/subcourse/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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;
}
Expand All @@ -110,7 +115,6 @@ export class MutateSubcourseResolver {
@Arg('subcourseId') subcourseId: number,
@Arg('studentId') studentId: number
): Promise<boolean> {
const { user } = context;
const subcourse = await getSubcourse(subcourseId);
await hasAccess(context, 'Subcourse', subcourse);

Expand All @@ -127,7 +131,6 @@ export class MutateSubcourseResolver {
@Arg('subcourseId') subcourseId: number,
@Arg('studentId') studentId: number
): Promise<boolean> {
const { user } = context;
const subcourse = await getSubcourse(subcourseId);
await hasAccess(context, 'Subcourse', subcourse);
const instructorToBeRemoved = await getStudent(studentId);
Expand All @@ -138,6 +141,14 @@ export class MutateSubcourseResolver {
await removeParticipantFromCourseChat(instructorUser, subcourse.conversationId);
}
logger.info(`Student(${studentId}) was deleted from Subcourse(${subcourseId}) by User(${context.user.userID})`);

await prisma.user_achievement.deleteMany({
where: {
userId: `student/${studentId}`,
context: { path: ['relation'], equals: `subcourse/${subcourseId}` },
},
});

return true;
}

Expand Down
Loading
Loading