Skip to content

Commit

Permalink
feat: add profile/info route handling via nestjs
Browse files Browse the repository at this point in the history
  • Loading branch information
stas-laniuk committed Feb 1, 2024
1 parent 6259c7b commit a4a9d3f
Show file tree
Hide file tree
Showing 25 changed files with 2,608 additions and 511 deletions.
585 changes: 580 additions & 5 deletions client/src/api/api.ts

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions client/src/services/user.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { getApiConfiguration, getServerAxiosProps } from 'utils/axios';
import { EnglishLevel } from 'common/models';
import { ProfileApi, ProfileDto, UsersNotificationsApi, UpdateUserDtoLanguagesEnum } from 'api';
import { ProfileApi, ProfileWithCvDto, UsersNotificationsApi, UpdateUserDtoLanguagesEnum } from 'api';
import discordIntegration from '../configs/discord-integration';
import type {
ConfigurableProfilePermissions,
Expand Down Expand Up @@ -101,6 +101,10 @@ export class UserService {
}

async getProfileInfo(githubId?: string) {
const { data } = await this.profileApi.getProfileInfo(githubId);
return data;

// old implementation for regression verification purposes (to be deleted along with koa-related code in the final commit of PR)
const response = await this.axios.get<{ data: ProfileInfo }>(`/api/profile/info`, {
params: { githubId },
});
Expand Down Expand Up @@ -226,7 +230,7 @@ export type ProfileInfo = {
publicFeedback?: PublicFeedback[];
stageInterviewFeedback?: StageInterviewDetailedFeedback[];
discord: Discord | null;
} & ProfileDto;
} & ProfileWithCvDto;

export type ProfileMainCardData = {
location: Location | null;
Expand Down
2 changes: 1 addition & 1 deletion nestjs/src/courses/courses.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,6 @@ import { CourseMentorsController, CourseMentorsService } from './course-mentors'
TaskVerificationsService,
CourseMentorsService,
],
exports: [CourseTasksService, CourseUsersService, CoursesService, StudentsService],
exports: [CourseTasksService, CourseUsersService, CoursesService, StudentsService, InterviewsService],
})
export class CoursesModule {}
4 changes: 4 additions & 0 deletions nestjs/src/courses/courses.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ export class CoursesService {
relations,
});
}

public async getByDisciplineIds(disciplinesIds: number[] = []) {
return this.repository.find({ where: { disciplineId: In(disciplinesIds) } });
}
}
135 changes: 129 additions & 6 deletions nestjs/src/courses/interviews/interviews.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { In, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { StageInterviewFeedbackJson } from '@common/models';
import { CourseTask } from '@entities/courseTask';
import { StageInterview } from '@entities/stageInterview';
import { TaskInterviewStudent } from '@entities/taskInterviewStudent';
import { UsersService } from 'src/users/users.service';
import { StageInterviewStudent, Student } from '@entities/index';
import { AvailableStudentDto } from './dto/available-student.dto';
import { StageInterviewFeedbackJson, StageInterviewDetailedFeedback } from '@common/models';
import { CourseTask } from '@entities/courseTask';
import {
Course,
Mentor,
StageInterview,
StageInterviewFeedback,
StageInterviewStudent,
Student,
TaskInterviewStudent,
User,
} from '@entities/index';
import { TaskType } from '@entities/task';
import { AvailableStudentDto } from './dto/available-student.dto';

@Injectable()
export class InterviewsService {
Expand All @@ -19,6 +26,8 @@ export class InterviewsService {
readonly taskInterviewStudentRepository: Repository<TaskInterviewStudent>,
@InjectRepository(Student)
readonly studentRepository: Repository<Student>,
@InjectRepository(StageInterview)
readonly stageInterviewRepository: Repository<StageInterview>,
readonly userService: UsersService,
) {}

Expand Down Expand Up @@ -184,4 +193,118 @@ export class InterviewsService {
private isGoodCandidate(stageInterviews: StageInterview[]) {
return stageInterviews.some(i => i.isCompleted && i.isGoodCandidate);
}

async getStageInterviewFeedback(githubId: string): Promise<StageInterviewDetailedFeedback[]> {
const data = await this.stageInterviewRepository
.createQueryBuilder('stageInterview')
.select('"stageInterview"."decision" AS "decision"')
.addSelect('"stageInterview"."isGoodCandidate" AS "isGoodCandidate"')
.addSelect('"stageInterview"."score" AS "interviewScore"')
.addSelect('"course"."name" AS "courseName"')
.addSelect('"course"."fullName" AS "courseFullName"')
.addSelect('"stageInterviewFeedback"."json" AS "interviewResultJson"')
.addSelect('"stageInterviewFeedback"."updatedDate" AS "interviewFeedbackDate"')
.addSelect('"stageInterviewFeedback"."version" AS "feedbackVersion"')
.addSelect('"userMentor"."firstName" AS "interviewerFirstName"')
.addSelect('"userMentor"."lastName" AS "interviewerLastName"')
.addSelect('"userMentor"."githubId" AS "interviewerGithubId"')
.addSelect('"courseTask"."maxScore" AS "maxScore"')
.leftJoin(Student, 'student', '"student"."id" = "stageInterview"."studentId"')
.leftJoin(User, 'user', '"user"."id" = "student"."userId"')
.leftJoin(Course, 'course', '"course"."id" = "stageInterview"."courseId"')
.leftJoin(
StageInterviewFeedback,
'stageInterviewFeedback',
'"stageInterview"."id" = "stageInterviewFeedback"."stageInterviewId"',
)
.leftJoin(CourseTask, 'courseTask', '"courseTask"."id" = "stageInterview"."courseTaskId"')
.leftJoin(Mentor, 'mentor', '"mentor"."id" = "stageInterview"."mentorId"')
.leftJoin(User, 'userMentor', '"userMentor"."id" = "mentor"."userId"')
.where('"user"."githubId" = :githubId', { githubId })
.andWhere('"stageInterview"."isCompleted" = true')
.orderBy('"course"."updatedDate"', 'ASC')
.getRawMany();

return data
.map((feedbackData: FeedbackData) => {
const {
feedbackVersion,
decision,
interviewFeedbackDate,
interviewerFirstName,
courseFullName,
courseName,
interviewerLastName,
interviewerGithubId,
isGoodCandidate,
interviewScore,
interviewResultJson,
maxScore,
} = feedbackData;
const feedbackTemplate = JSON.parse(interviewResultJson) as any;

const { score, feedback } = !feedbackVersion
? InterviewsService.parseLegacyFeedback(feedbackTemplate)
: {
feedback: feedbackTemplate,
score: interviewScore ?? 0,
};

return {
version: feedbackVersion ?? 0,
date: interviewFeedbackDate,
decision,
isGoodCandidate,
courseName,
courseFullName,
feedback,
score,
interviewer: {
name:
this.userService.getFullName({ firstName: interviewerFirstName, lastName: interviewerLastName }) ||
interviewerGithubId,
githubId: interviewerGithubId,
},
maxScore,
};
})
.filter(Boolean);
}

/**
* @deprecated - should be removed once Artsiom A. makes migration of the legacy feedback format
*/
private static parseLegacyFeedback(interviewResult: StageInterviewFeedbackJson) {
const { english, programmingTask, resume } = interviewResult;
const { rating, htmlCss, common, dataStructures } = InterviewsService.getInterviewRatings(interviewResult);

return {
score: rating,
feedback: {
english: english.levelMentorOpinion ? english.levelMentorOpinion : english.levelStudentOpinion,
programmingTask,
comment: resume.comment,
skills: {
htmlCss,
common,
dataStructures,
},
},
};
}
}

type FeedbackData = {
decision: string;
isGoodCandidate: boolean;
courseName: string;
courseFullName: string;
interviewResultJson: any;
interviewFeedbackDate: string;
interviewerFirstName: string;
interviewerLastName: string;
interviewerGithubId: string;
feedbackVersion: null | number;
interviewScore: null | number;
maxScore: number;
};
4 changes: 4 additions & 0 deletions nestjs/src/disciplines/disciplines.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,8 @@ export class DisciplinesService {
public async delete(id: number): Promise<void> {
await this.repository.softDelete(id);
}

public async getByNames(disciplineNames: string[]) {
return this.repository.find({ where: { name: In(disciplineNames) } });
}
}
2 changes: 2 additions & 0 deletions nestjs/src/profile/dto/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './profile-course.dto';
export * from './update-user.dto';
export * from './update-profile.dto';
export * from './profile-info.dto';
export * from './update-profile-info.dto';
119 changes: 119 additions & 0 deletions nestjs/src/profile/dto/permissions.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator';

export interface Permissions {
isProfileVisible: boolean;
isAboutVisible: boolean;
isEducationVisible: boolean;
isEnglishVisible: boolean;
isEmailVisible: boolean;
isTelegramVisible: boolean;
isSkypeVisible: boolean;
isWhatsAppVisible: boolean;
isPhoneVisible: boolean;
isContactsNotesVisible: boolean;
isLinkedInVisible: boolean;
isPublicFeedbackVisible: boolean;
isMentorStatsVisible: boolean;
isStudentStatsVisible: boolean;
isStageInterviewFeedbackVisible: boolean;
isCoreJsFeedbackVisible: boolean;
isConsentsVisible: boolean;
isExpellingReasonVisible: boolean;
}

class PublicVisibilitySettings {
@ApiProperty()
@IsBoolean()
all: boolean;
}

class PartialStudentVisibilitySettings extends PublicVisibilitySettings {
@ApiProperty()
@IsBoolean()
student: boolean;
}

class ContactsVisibilitySettings extends PublicVisibilitySettings {
@ApiProperty()
@IsBoolean()
student: boolean;
}

class VisibilitySettings extends PublicVisibilitySettings {
@ApiProperty()
@IsBoolean()
mentor: boolean;

@ApiProperty()
@IsBoolean()
student: boolean;
}

export class ConfigurableProfilePermissions {
@ApiProperty({ required: false, type: PublicVisibilitySettings })
@Type(() => PublicVisibilitySettings)
@IsOptional()
isProfileVisible?: PublicVisibilitySettings;

@ApiProperty({ required: false, type: VisibilitySettings })
@Type(() => VisibilitySettings)
@IsOptional()
isAboutVisible?: VisibilitySettings;

@ApiProperty({ required: false, type: VisibilitySettings })
@Type(() => VisibilitySettings)
@IsOptional()
isEducationVisible?: VisibilitySettings;

@ApiProperty({ required: false, type: PartialStudentVisibilitySettings })
@Type(() => PartialStudentVisibilitySettings)
@IsOptional()
isEnglishVisible?: PartialStudentVisibilitySettings;

@ApiProperty({ required: false, type: ContactsVisibilitySettings })
@Type(() => ContactsVisibilitySettings)
@IsOptional()
isEmailVisible?: ContactsVisibilitySettings;

@ApiProperty({ required: false, type: ContactsVisibilitySettings })
@Type(() => ContactsVisibilitySettings)
@IsOptional()
isTelegramVisible?: ContactsVisibilitySettings;

@ApiProperty({ required: false, type: ContactsVisibilitySettings })
@Type(() => ContactsVisibilitySettings)
@IsOptional()
isSkypeVisible?: ContactsVisibilitySettings;

@ApiProperty({ required: false, type: ContactsVisibilitySettings })
@Type(() => ContactsVisibilitySettings)
@IsOptional()
isPhoneVisible?: ContactsVisibilitySettings;

@ApiProperty({ required: false, type: ContactsVisibilitySettings })
@Type(() => ContactsVisibilitySettings)
@IsOptional()
isContactsNotesVisible?: ContactsVisibilitySettings;

@ApiProperty({ required: false, type: VisibilitySettings })
@Type(() => VisibilitySettings)
@IsOptional()
isLinkedInVisible?: VisibilitySettings;

@ApiProperty({ required: false, type: VisibilitySettings })
@Type(() => VisibilitySettings)
@IsOptional()
isPublicFeedbackVisible?: VisibilitySettings;

@ApiProperty({ required: false, type: VisibilitySettings })
@Type(() => VisibilitySettings)
@IsOptional()
isMentorStatsVisible?: VisibilitySettings;

@ApiProperty({ required: false, type: PartialStudentVisibilitySettings })
@Type(() => PartialStudentVisibilitySettings)
@IsOptional()
isStudentStatsVisible?: PartialStudentVisibilitySettings;
}
Loading

0 comments on commit a4a9d3f

Please sign in to comment.