Skip to content

Commit

Permalink
Feat: offer course template (#951)
Browse files Browse the repository at this point in the history
* feat: offer course template

* fix: refactor course created for all instructors

* fix: update achievement context with course

* fix: instructors, delete funtions

* fix: update with requested changes

* fix: update mutation functions for course mutation

* fix: check for last achievement

---------

Co-authored-by: LomyW <[email protected]>
  • Loading branch information
LucasFalkowsky and LomyW authored Jan 23, 2024
1 parent 64f5837 commit 41392ec
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 20 deletions.
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[];
};
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;
}
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

0 comments on commit 41392ec

Please sign in to comment.