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: migrate 'profile/info' route to nestjs #2420

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
843 changes: 804 additions & 39 deletions client/src/api/api.ts

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions client/src/pages/profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,25 @@ const ProfilePage = () => {
const fetchData = async () => {
try {
const githubId = router.query ? (router.query.githubId as string) : undefined;
const [profile, connections, { data }] = await Promise.all([
const [profileInfo, connections, { data }] = await Promise.all([
userService.getProfileInfo(githubId?.toLowerCase()),
notificationsService.getUserConnections().catch(() => ({})),
profileApi.getProfile(githubId?.toLowerCase() ?? session.githubId),
]);

const updateProfile = {
...profile,
const profileInfoExtendedWithCv = {
...profileInfo,
...data,
};
Comment on lines +60 to 63
Copy link
Contributor

Choose a reason for hiding this comment

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

I see nothing related with CV above, why do we have CV in name of this variable? πŸ™ƒ


let isProfileOwner = false;
if (profile?.generalInfo) {
if (profileInfo?.generalInfo) {
const userId = session.githubId;
const profileId = profile.generalInfo.githubId;
const profileId = profileInfo.generalInfo.githubId;
isProfileOwner = checkIsProfileOwner(userId, profileId);
}

setProfile(updateProfile);
setProfile(profileInfoExtendedWithCv);
setIsProfileOwner(isProfileOwner);
setConnections(connections as Connections);
} catch (e) {
Expand Down
10 changes: 4 additions & 6 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,10 +101,8 @@ export class UserService {
}

async getProfileInfo(githubId?: string) {
const response = await this.axios.get<{ data: ProfileInfo }>(`/api/profile/info`, {
params: { githubId },
});
return response.data.data;
const { data } = await this.profileApi.getProfileInfo(githubId);
return data;
}

async sendEmailConfirmationLink() {
Expand Down Expand Up @@ -226,7 +224,7 @@ export type ProfileInfo = {
publicFeedback?: PublicFeedback[];
stageInterviewFeedback?: StageInterviewDetailedFeedback[];
discord: Discord | null;
} & ProfileDto;
} & ProfileWithCvDto;

export type ProfileMainCardData = {
location: Location | null;
Expand Down
4 changes: 3 additions & 1 deletion common/models/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export interface GeneralInfo {
aboutMyself?: string | null;
location: Location;
educationHistory?: any | null;
englishLevel?: EnglishLevel | null;
// TODO: String type is too abstract for englishLevel.
Copy link
Contributor

Choose a reason for hiding this comment

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

Then why do we have it here? Probably you've misconfigured some BE API decorators, could you please check?

englishLevel?: EnglishLevel | null | string;
languages: string[];
}

Expand Down Expand Up @@ -144,6 +145,7 @@ export interface StageInterviewDetailedFeedback {
// This type have to updated to refer to `InterviewFeedbackStepData`, when profile is migrated to nestjs
feedback:
| LegacyFeedback
| object
| {
steps: Record<
string,
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;
Comment on lines +229 to +243
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
.map((feedbackData: FeedbackData) => {
const {
feedbackVersion,
decision,
interviewFeedbackDate,
interviewerFirstName,
courseFullName,
courseName,
interviewerLastName,
interviewerGithubId,
isGoodCandidate,
interviewScore,
interviewResultJson,
maxScore,
} = feedbackData;
.map(({
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,
};
Comment on lines +246 to +251
Copy link
Contributor

Choose a reason for hiding this comment

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

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


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);
}
Comment on lines +197 to +272
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should do something like this?

  private async getFeedbackData(githubId: string): Promise<FeedbackData[]> {
    return this.stageInterviewRepository
      .createQueryBuilder('stageInterview')
      // ... addSelects ...
      .leftJoin(Student, 'student', '"student"."id" = "stageInterview"."studentId"')
      // ... other leftJoins ...
      .where('"user"."githubId" = :githubId', { githubId })
      .andWhere('"stageInterview"."isCompleted" = true')
      .orderBy('"course"."updatedDate"', 'ASC')
      .getRawMany();
  }

  private parseFeedbackData(feedbackData: FeedbackData): StageInterviewDetailedFeedback {
    // ... existing mapping logic ...
  }

  async getStageInterviewFeedback(githubId: string): Promise<StageInterviewDetailedFeedback[]> {
    const feedbackData = await this.getFeedbackData(githubId);
    return feedbackData.map(feedbackData => this.parseFeedbackData(feedbackData)).filter(Boolean);
  }


/**
* @deprecated - should be removed once Artsiom A. makes migration of the legacy feedback format
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's better to attach the issue number instead of writing that Artsiom will do it.
#2281"

*/
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;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd propose to use more semantic name for this flag, like getAll or requestAll

}

class PartialStudentVisibilitySettings extends PublicVisibilitySettings {
@ApiProperty()
@IsBoolean()
student: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd propose to use more semantic name for this flag, like isStudent or withStudent or getStudent (depending on context)

}

class ContactsVisibilitySettings extends PublicVisibilitySettings {
@ApiProperty()
@IsBoolean()
student: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above

}

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

Choose a reason for hiding this comment

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

Same as above


@ApiProperty()
@IsBoolean()
student: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above

}

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
Loading