import { ERROR_MESSAGES, TACheckinPair, TACheckinTimesResponse, RegisterCourseParams, Role, UserPartial, EditCourseInfoParams, } from '@koh/common'; import { HttpException, HttpStatus, Injectable, BadRequestException, } from '@nestjs/common'; import { partition } from 'lodash'; import { EventModel, EventType } from 'profile/event-model.entity'; import { QuestionModel } from 'question/question.entity'; import { Between, Brackets, Connection, getRepository, In } from 'typeorm'; import { UserCourseModel } from '../profile/user-course.entity'; import { SemesterModel } from 'semester/semester.entity'; import { ProfSectionGroupsModel } from 'login/prof-section-groups.entity'; import { CourseSectionMappingModel } from 'login/course-section-mapping.entity'; import { LastRegistrationModel } from 'login/last-registration-model.entity'; import { LoginCourseService } from '../login/login-course.service'; import { CourseModel } from './course.entity'; import { UserModel } from 'profile/user.entity'; @Injectable() export class CourseService { constructor( private connection: Connection, private loginCourseService: LoginCourseService, ) {} async getTACheckInCheckOutTimes( courseId: number, startDate: string, endDate: string, ): Promise<TACheckinTimesResponse> { const startDateAsDate = new Date(startDate); const endDateAsDate = new Date(endDate); if (startDateAsDate.getUTCDate() === endDateAsDate.getUTCDate()) { endDateAsDate.setUTCDate(endDateAsDate.getUTCDate() + 1); } const taEvents = await EventModel.find({ where: { eventType: In([ EventType.TA_CHECKED_IN, EventType.TA_CHECKED_OUT, EventType.TA_CHECKED_OUT_FORCED, ]), time: Between(startDateAsDate, endDateAsDate), courseId, }, relations: ['user'], }); const [checkinEvents, otherEvents] = partition( taEvents, (e) => e.eventType === EventType.TA_CHECKED_IN, ); const taCheckinTimes: TACheckinPair[] = []; for (const checkinEvent of checkinEvents) { let closestEvent: EventModel = null; let mostRecentTime = new Date(); const originalDate = mostRecentTime; for (const checkoutEvent of otherEvents) { if ( checkoutEvent.userId === checkinEvent.userId && checkoutEvent.time > checkinEvent.time && checkoutEvent.time.getTime() - checkinEvent.time.getTime() < mostRecentTime.getTime() - checkinEvent.time.getTime() ) { closestEvent = checkoutEvent; mostRecentTime = checkoutEvent.time; } } const numHelped = await QuestionModel.count({ where: { taHelpedId: checkinEvent.userId, helpedAt: Between( checkinEvent.time, closestEvent?.time || new Date(), ), }, }); taCheckinTimes.push({ name: checkinEvent.user.name, checkinTime: checkinEvent.time, checkoutTime: closestEvent?.time, inProgress: mostRecentTime === originalDate, forced: closestEvent?.eventType === EventType.TA_CHECKED_OUT_FORCED, numHelped, }); } return { taCheckinTimes }; } async removeUserFromCourse(userCourse: UserCourseModel): Promise<void> { if (!userCourse) { throw new HttpException( ERROR_MESSAGES.courseController.courseNotFound, HttpStatus.NOT_FOUND, ); } try { await UserCourseModel.remove(userCourse); } catch (err) { console.error(err); throw new HttpException( ERROR_MESSAGES.courseController.removeCourse, HttpStatus.INTERNAL_SERVER_ERROR, ); } } async editCourse( courseId: number, coursePatch: EditCourseInfoParams, ): Promise<void> { const course = await CourseModel.findOne(courseId); if (course === null || course === undefined) { throw new HttpException( ERROR_MESSAGES.courseController.courseNotFound, HttpStatus.NOT_FOUND, ); } if (Object.values(coursePatch).some((x) => x === null || x === '')) { throw new BadRequestException( ERROR_MESSAGES.courseController.updateCourse, ); } for (const crn of new Set(coursePatch.crns)) { const courseCrnMaps = await CourseSectionMappingModel.find({ crn: crn, }); let courseCrnMapExists = false; for (const courseCrnMap of courseCrnMaps) { const conflictCourse = await CourseModel.findOne(courseCrnMap.courseId); if (conflictCourse && conflictCourse.semesterId === course.semesterId) { if (courseCrnMap.courseId !== courseId) { throw new BadRequestException( ERROR_MESSAGES.courseController.crnAlreadyRegistered( crn, courseId, ), ); } else { courseCrnMapExists = true; break; } } } if (!courseCrnMapExists) { try { await CourseSectionMappingModel.create({ crn: crn, courseId: course.id, }).save(); } catch (err) { console.error(err); throw new HttpException( ERROR_MESSAGES.courseController.createCourseMappings, HttpStatus.INTERNAL_SERVER_ERROR, ); } } } if (coursePatch.name) { course.name = coursePatch.name; } if (coursePatch.coordinator_email) { course.coordinator_email = coursePatch.coordinator_email; } if (coursePatch.icalURL) { course.icalURL = coursePatch.icalURL; } try { await course.save(); } catch (err) { console.error(err); throw new HttpException( ERROR_MESSAGES.courseController.updateCourse, HttpStatus.INTERNAL_SERVER_ERROR, ); } } async registerCourses( body: RegisterCourseParams[], userId: number, ): Promise<void> { // obtains the ProfSectionGroupsModel of the professor const profSectionGroups = await ProfSectionGroupsModel.findOne({ where: { profId: userId }, }); // iterate over each section group registration for (const courseParams of body) { // finds professor's section group with matching name const sectionGroup = profSectionGroups?.sectionGroups.find( (sg) => sg.name === courseParams.sectionGroupName, ); if (!sectionGroup) throw new BadRequestException( ERROR_MESSAGES.courseController.sectionGroupNotFound, ); const khourySemesterParsed = this.loginCourseService.parseKhourySemester( sectionGroup.semester, ); const semester = await SemesterModel.findOne({ where: { season: khourySemesterParsed.season, year: khourySemesterParsed.year, }, }); if (!semester) throw new BadRequestException( ERROR_MESSAGES.courseController.noSemesterFound, ); // checks that course hasn't already been created let course = await CourseModel.findOne({ where: { sectionGroupName: courseParams.sectionGroupName, semesterId: semester.id, }, }); if (course) throw new BadRequestException( ERROR_MESSAGES.courseController.courseAlreadyRegistered, courseParams.name, ); try { // create the submitted course course = await CourseModel.create({ name: courseParams.name, sectionGroupName: courseParams.sectionGroupName, coordinator_email: courseParams.coordinator_email, icalURL: courseParams.iCalURL, semesterId: semester.id, enabled: true, timezone: courseParams.timezone, }).save(); } catch (err) { console.error(err); throw new HttpException( ERROR_MESSAGES.courseController.createCourse, HttpStatus.INTERNAL_SERVER_ERROR, ); } try { // create CourseSectionMappings for each crn new Set(sectionGroup.crns).forEach(async (crn) => { await CourseSectionMappingModel.create({ crn: crn, courseId: course.id, }).save(); }); } catch (err) { console.error(err); throw new HttpException( ERROR_MESSAGES.courseController.createCourseMappings, HttpStatus.INTERNAL_SERVER_ERROR, ); } // Add UserCourse to course await UserCourseModel.create({ userId, courseId: course.id, role: Role.PROFESSOR, }).save(); } try { // Update professor's last registered semester to semester model's current semester let profLastRegistered: LastRegistrationModel; profLastRegistered = await LastRegistrationModel.findOne({ where: { profId: userId }, }); const lastRegisteredSemester = profSectionGroups?.sectionGroups[0]?.semester; if (profLastRegistered) { profLastRegistered.lastRegisteredSemester = lastRegisteredSemester; await profLastRegistered.save(); } else { profLastRegistered = await LastRegistrationModel.create({ profId: userId, lastRegisteredSemester, }).save(); } } catch (err) { console.error(err); throw new HttpException( ERROR_MESSAGES.courseController.updateProfLastRegistered, HttpStatus.INTERNAL_SERVER_ERROR, ); } } async getUserInfo( courseId: number, page: number, pageSize: number, search?: string, role?: Role, ): Promise<UserPartial[]> { const query = await getRepository(UserModel) .createQueryBuilder() .leftJoin( UserCourseModel, 'UserCourseModel', '"UserModel".id = "UserCourseModel"."userId"', ) .where('"UserCourseModel"."courseId" = :courseId', { courseId }); // check if searching for specific role if (role) { query.andWhere('"UserCourseModel".role = :role', { role }); } // check if searching for specific name if (search) { const likeSearch = `%${search.replace(' ', '')}%`.toUpperCase(); query.andWhere( new Brackets((q) => { q.where( 'CONCAT(UPPER("UserModel"."firstName"), UPPER("UserModel"."lastName")) like :searchString', { searchString: likeSearch, }, ); }), ); } // run query const users = query .select([ 'UserModel.id', 'UserModel.firstName', 'UserModel.lastName', 'UserModel.photoURL', 'UserModel.email', ]) .orderBy('UserModel.firstName') .skip((page - 1) * pageSize) .take(pageSize) .getMany(); return users; } }