diff --git a/client/src/components/Profile/LegacyScreeningFeedback.tsx b/client/src/components/Profile/LegacyScreeningFeedback.tsx new file mode 100644 index 0000000000..e9ddcc9414 --- /dev/null +++ b/client/src/components/Profile/LegacyScreeningFeedback.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { Tag, Typography, Table } from 'antd'; +import { Rating } from 'components/Rating'; +import { LegacyFeedback } from 'common/models/profile'; +import { ENGLISH_LEVELS } from 'data/english'; +import { CODING_LEVELS, SKILLS_LEVELS } from 'data/interviews/technical-screening'; + +const { Text } = Typography; + +enum SKILL_NAME { + htmlCss = 'HTML/CSS', + dataStructures = 'Data structures', + common = 'Common of CS / Programming', +} + +/** + * this feedback template will live here until we will migrate all feedbacks to new template + */ +export function LegacyScreeningFeedback({ feedback }: { feedback: LegacyFeedback }) { + const { comment, skills, programmingTask, english } = feedback; + + const skillSet = [ + ...(Object.keys(skills) as any[]).map((key: keyof typeof skills) => ({ + rating: skills[key], + name: SKILL_NAME[key], + key, + isNotCodeWritingLevel: true, + })), + { + rating: programmingTask.codeWritingLevel, + name: 'Code writing level', + key: 'codeWritingLevel', + isNotCodeWritingLevel: false, + }, + ]; + const englishLevel = typeof english === 'number' ? ENGLISH_LEVELS[english] : english; + + return ( + <> + {comment && ( +

+ Comment: + {comment} +

+ )} +

+ Programming task(s):
{programmingTask.task} +

+

+ Has the student solved the task(s)?{' '} + {programmingTask.resolved === 1 ? ( + Yes + ) : programmingTask.resolved === 2 ? ( + Yes (with tips) + ) : ( + No + )} +

+

Comments about coding level: {programmingTask.comment}

+

Estimated English level: {englishLevel?.toString().toUpperCase()}

+ ( + + ), + }, + ]} + /> + + ); +} diff --git a/client/src/components/Profile/PreScreeningIviewCard.tsx b/client/src/components/Profile/PreScreeningIviewCard.tsx index 93679d8bda..507ab8f6c8 100644 --- a/client/src/components/Profile/PreScreeningIviewCard.tsx +++ b/client/src/components/Profile/PreScreeningIviewCard.tsx @@ -9,6 +9,7 @@ import PreScreeningIviewModal from './PreScreeningIviewModal'; const { Text } = Typography; import { QuestionCircleOutlined, FullscreenOutlined } from '@ant-design/icons'; +import { getRating } from 'domain/interview'; type Props = { data: StageInterviewDetailedFeedback[]; @@ -36,11 +37,12 @@ class PreScreeningIviewsCard extends React.PureComponent { render() { const interviews = this.props.data; const { isPreScreeningIviewModalVisible, courseIndex } = this.state; + const interviewResult = interviews[courseIndex]; return ( <> @@ -51,13 +53,13 @@ class PreScreeningIviewsCard extends React.PureComponent { ( + renderItem={({ courseName, interviewer, score, maxScore, date, isGoodCandidate, version }, idx) => (

{courseName}

- +

Date: {formatDate(date)}

{isGoodCandidate != null ? (

diff --git a/client/src/components/Profile/PreScreeningIviewModal.tsx b/client/src/components/Profile/PreScreeningIviewModal.tsx index 7956665f75..dcf8f3601f 100644 --- a/client/src/components/Profile/PreScreeningIviewModal.tsx +++ b/client/src/components/Profile/PreScreeningIviewModal.tsx @@ -1,49 +1,22 @@ import * as React from 'react'; -import { Modal, Tag, Typography, Table } from 'antd'; +import { Modal, Tag } from 'antd'; import { formatDate } from 'services/formatter'; import { Rating } from 'components/Rating'; -import { StageInterviewDetailedFeedback } from 'common/models/profile'; -import { CODING_LEVELS, SKILLS_LEVELS, SKILL_NAME } from 'services/reference-data/stageInterview'; -import { ENGLISH_LEVELS } from 'data/english'; - -const { Text } = Typography; +import { LegacyFeedback, StageInterviewDetailedFeedback } from 'common/models/profile'; +import { LegacyScreeningFeedback } from './LegacyScreeningFeedback'; +import { PrescreeningFeedback } from './PrescreeningFeedback'; +import { getRating } from 'domain/interview'; type Props = { - feedback: StageInterviewDetailedFeedback; + interviewResult: StageInterviewDetailedFeedback; isVisible: boolean; onHide: () => void; }; class PreScreeningIviewModal extends React.PureComponent { render() { - const { feedback, isVisible, onHide } = this.props; - const { - courseName, - courseFullName, - date, - rating, - interviewer, - isGoodCandidate, - comment, - skills, - programmingTask, - english, - } = feedback; - const skillSet = [ - ...(Object.keys(skills) as any[]).map((key: keyof typeof skills) => ({ - rating: skills[key], - name: SKILL_NAME[key], - key: `stageInterview-${courseName}-${date}-skills-${key}`, - isNotCodeWritingLevel: true, - })), - { - rating: programmingTask.codeWritingLevel, - name: 'Code writing level', - key: `stageInterview-${courseName}-${date}-skills-codeWritingLevel`, - isNotCodeWritingLevel: false, - }, - ]; - const englishLevel = typeof english === 'number' ? ENGLISH_LEVELS[english] : english; + const { interviewResult, isVisible, onHide } = this.props; + const { courseFullName, date, score, interviewer, isGoodCandidate, feedback, version, maxScore } = interviewResult; return ( { footer={null} width={'80%'} > - +

Date: {formatDate(date)}

{isGoodCandidate != null ? (

@@ -62,46 +35,8 @@ class PreScreeningIviewModal extends React.PureComponent {

Interviewer: {interviewer.name}

- {comment && ( -

- Comment: - {comment} -

- )} -

- Programming task(s):
{programmingTask.task} -

-

- Has the student solved the task(s)?{' '} - {programmingTask.resolved === 1 ? ( - Yes - ) : programmingTask.resolved === 2 ? ( - Yes (with tips) - ) : ( - No - )} -

-

Comments about coding level: {programmingTask.comment}

-

Estimated English level: {englishLevel?.toString().toUpperCase()}

-
( - - ), - }, - ]} - /> + {version === 0 && } + {version === 1 && } ); } diff --git a/client/src/components/Profile/PrescreeningFeedback.tsx b/client/src/components/Profile/PrescreeningFeedback.tsx new file mode 100644 index 0000000000..f903e73c9d --- /dev/null +++ b/client/src/components/Profile/PrescreeningFeedback.tsx @@ -0,0 +1,127 @@ +import * as React from 'react'; +import { Typography, Table, Row, Space } from 'antd'; +import { Rating } from 'components/Rating'; +import { StageInterviewDetailedFeedback } from 'common/models/profile'; +import { + CODING_LEVELS, + FeedbackStepId, + InterviewFeedbackStepData, + InterviewFeedbackValues, + InterviewQuestion, + SKILLS_LEVELS, +} from 'data/interviews/technical-screening'; + +const { Text, Title } = Typography; + +export function PrescreeningFeedback({ feedback }: { feedback: StageInterviewDetailedFeedback['feedback'] }) { + const { steps } = feedback as { steps: Record }; + + const { theory, practice, english, decision, intro } = steps; + const isRejected = intro.values?.interviewResult === 'missed'; + + if (isRejected) { + return ( + + {intro.values?.comment && ( + + Comment: + {intro.values?.comment as string} + + )} + + ); + } + + return ( + <> + + {decision.values?.redFlags && ( + + Red flags: + {decision.values?.redFlags as string} + + )} + {decision.values?.comment && ( + + Comment: + {decision.values?.comment as string} + + )} + {english.values && ( + <> + + Certified level of English: + {english.values?.englishCertificate as string} + + + English level by interviewers opinion: + {english.values?.selfAssessment as string} + + + )} + {english.values?.comment && ( + + Where did the student learn English: + {english.values?.comment as string} + + )} + + + + + ); +} + +function SkillSection({ + skills, + title, + tooltips, +}: { + skills: InterviewFeedbackValues | undefined; + title: string; + tooltips: string[]; +}) { + if (!skills) return null; + + return ( + + {title} + + {skills.comment && ( + + Comment:  {skills.comment as string} + + )} + + ); +} + +function SkillTable({ skills, tooltips }: { skills: InterviewQuestion[]; tooltips: string[] }) { + return ( +
( + <> + {record.topic && ( + + {record.topic} + + )} + {record.title} + + ), + }, + { + dataIndex: 'value', + render: rating => , + }, + ]} + /> + ); +} diff --git a/client/src/components/Profile/__test__/PreScreeningIviewCard.test.tsx b/client/src/components/Profile/__test__/PreScreeningIviewCard.test.tsx index ab2b51be84..4c6f6b64a8 100644 --- a/client/src/components/Profile/__test__/PreScreeningIviewCard.test.tsx +++ b/client/src/components/Profile/__test__/PreScreeningIviewCard.test.tsx @@ -10,21 +10,25 @@ describe('PreScreeningIviewCard', () => { isGoodCandidate: true, courseName: 'rs-2018-q1', courseFullName: 'Rolling Scopes School 2018 Q1', - rating: 3.43, - comment: 'Not bad', - english: 'a2', - date: '2018-12-01', - programmingTask: { - task: 'aaa === 3a', - codeWritingLevel: 3, - resolved: 2, - comment: 'Not bad coder', - }, - skills: { - htmlCss: 3, - common: 2, - dataStructures: 4, + score: 34, + maxScore: 50, + feedback: { + comment: 'Not bad', + english: 'a2', + programmingTask: { + task: 'aaa === 3a', + codeWritingLevel: 3, + resolved: 2, + comment: 'Not bad coder', + }, + skills: { + htmlCss: 3, + common: 2, + dataStructures: 4, + }, }, + date: '2018-12-01', + version: 0, interviewer: { name: 'Dima Vasilyev', githubId: 'dva', @@ -35,20 +39,24 @@ describe('PreScreeningIviewCard', () => { isGoodCandidate: true, courseName: 'rs-2019-q1', courseFullName: 'Rolling Scopes School 2019 Q1', - rating: 4.23, comment: 'Not bad', english: 'a2+', date: '2019-12-01', - programmingTask: { - task: 'aaa === 3a', - codeWritingLevel: 4, - resolved: 1, - comment: 'Not bad coder', - }, - skills: { - htmlCss: 3, - common: 4, - dataStructures: 3, + score: 34, + maxScore: 50, + version: 0, + feedback: { + programmingTask: { + task: 'aaa === 3a', + codeWritingLevel: 4, + resolved: 1, + comment: 'Not bad coder', + }, + skills: { + htmlCss: 3, + common: 4, + dataStructures: 3, + }, }, interviewer: { name: 'Alex Smirnov', diff --git a/client/src/components/Profile/__test__/PreScreeningIviewModal.test.tsx b/client/src/components/Profile/__test__/PreScreeningIviewModal.test.tsx index 171adc3bbe..3265c704a6 100644 --- a/client/src/components/Profile/__test__/PreScreeningIviewModal.test.tsx +++ b/client/src/components/Profile/__test__/PreScreeningIviewModal.test.tsx @@ -10,28 +10,33 @@ describe('PreScreeningIviewModal', () => { isGoodCandidate: true, courseName: 'rs-2019-q1', courseFullName: 'Rolling Scopes School 2019 Q1', - rating: 4.23, - comment: 'Not bad', - english: 'a2+', - date: '2019-12-01', - programmingTask: { - task: 'aaa === 3a', - codeWritingLevel: 4, - resolved: 1, - comment: 'Not bad coder', - }, - skills: { - htmlCss: 3, - common: 4, - dataStructures: 3, + score: 10, + maxScore: 50, + feedback: { + comment: 'Not bad', + english: 'a2+', + programmingTask: { + task: 'aaa === 3a', + codeWritingLevel: 4, + resolved: 1, + comment: 'Not bad coder', + }, + skills: { + htmlCss: 3, + common: 4, + dataStructures: 3, + }, }, + date: '2019-12-01', interviewer: { name: 'Alex Smirnov', githubId: 'alexs', }, } as StageInterviewDetailedFeedback; - const { container } = render(); + const { container } = render( + , + ); expect(container).toMatchSnapshot(); }); }); diff --git a/client/src/components/Profile/__test__/__snapshots__/PreScreeningIviewCard.test.tsx.snap b/client/src/components/Profile/__test__/__snapshots__/PreScreeningIviewCard.test.tsx.snap index 0206a8d4cb..fba51d25bb 100644 --- a/client/src/components/Profile/__test__/__snapshots__/PreScreeningIviewCard.test.tsx.snap +++ b/client/src/components/Profile/__test__/__snapshots__/PreScreeningIviewCard.test.tsx.snap @@ -382,7 +382,7 @@ exports[`PreScreeningIviewCard Should render correctly 1`] = ` class="ant-typography ant-rate-text css-dev-only-do-not-override-w8mnev" style="font-weight: bold;" > - 3.43 + 3.40

  • - 4.23 + 3.40

    Partially rated; - } - return ( <> step.id === FeedbackStepId.Decision); + const introduction = steps.find(step => step.id === FeedbackStepId.Introduction); + const isInterviewConducted = !isInterviewRejected(FeedbackStepId.Introduction, introduction?.values); return { - score: (decision?.values?.finalScore as number) ?? undefined, + score: isInterviewConducted ? (decision?.values?.finalScore as number) : 0, decision: getDecision(), isGoodCandidate: getIsGoodCandidate(), }; @@ -178,9 +181,6 @@ function getInterviewSummary(feedback: Feedback) { } function getDecision() { - const introduction = steps.find(step => step.id === FeedbackStepId.Introduction); - const isInterviewConducted = !isInterviewRejected(FeedbackStepId.Introduction, introduction?.values); - if (!isInterviewConducted) { // if the interview was missed, return the reason return introduction?.values?.['missed'] as string; @@ -193,8 +193,11 @@ function getInterviewSummary(feedback: Feedback) { function isInterviewCompleted(feedback: Feedback) { const { steps } = feedback; const introduction = feedback.steps.find(step => step.id === FeedbackStepId.Introduction); + const decision = feedback.steps.find(step => step.id === FeedbackStepId.Decision); + return ( - (introduction && isInterviewRejected(introduction.id, introduction.values)) || steps.every(step => step.isCompleted) + (introduction && isInterviewRejected(introduction.id, introduction.values)) || + (steps.every(step => step.isCompleted) && decision?.values?.decision !== Decision.Draft) ); } diff --git a/client/src/modules/Mentor/pages/InterviewWaitingList/index.tsx b/client/src/modules/Mentor/pages/InterviewWaitingList/index.tsx index 23b5846b94..77ef8d5a18 100644 --- a/client/src/modules/Mentor/pages/InterviewWaitingList/index.tsx +++ b/client/src/modules/Mentor/pages/InterviewWaitingList/index.tsx @@ -18,7 +18,7 @@ import { CoursePageProps } from 'services/models'; import { isCourseManager, isMentor } from 'domain/user'; import { AvailableStudentDto, CoursesInterviewsApi, InterviewDto } from 'api'; import { getApiConfiguration } from 'utils/axios'; -import { stageInterviewType } from 'domain/interview'; +import { getRating, stageInterviewType } from 'domain/interview'; const api = new CoursesInterviewsApi(getApiConfiguration()); @@ -94,7 +94,10 @@ export function InterviewWaitingList({ session, course, interview }: PageProps) dataIndex: 'rating', sorter: numberSorter('rating'), width: 210, - render: (value: number) => (value != null ? : null), + render: (value: number, record: AvailableStudentDto) => + value != null ? ( + + ) : null, }, ] : []), diff --git a/client/src/pages/course/student/interviews.tsx b/client/src/pages/course/student/interviews.tsx index 3ed7f00d76..9b12426ec7 100644 --- a/client/src/pages/course/student/interviews.tsx +++ b/client/src/pages/course/student/interviews.tsx @@ -9,7 +9,8 @@ import { useAsync } from 'react-use'; import { CourseService, Interview } from 'services/course'; import { CoursePageProps } from 'services/models'; import { formatShortDate } from 'services/formatter'; -import { friendlyStageInterviewVerdict, InterviewDetails, InterviewStatus, stageInterviewType } from 'domain/interview'; +import { getInterviewResult, InterviewDetails, InterviewStatus, stageInterviewType } from 'domain/interview'; +import { Decision } from 'data/interviews/technical-screening'; function Page(props: CoursePageProps) { const courseService = useMemo(() => new CourseService(props.course.id), [props.course.id]); @@ -126,7 +127,7 @@ function Page(props: CoursePageProps) { - {friendlyStageInterviewVerdict(item.result as any) ?? '-'} + {getInterviewResult(item.result as Decision) ?? '-'} diff --git a/common/models/profile.ts b/common/models/profile.ts index d50c980595..d077dd8d0e 100644 --- a/common/models/profile.ts +++ b/common/models/profile.ts @@ -133,25 +133,26 @@ export interface StageInterviewDetailedFeedback { isGoodCandidate: boolean; courseName: string; courseFullName: string; - rating: number; - comment: string; - english: EnglishLevel | number; + score: number; + maxScore: number; date: string; - programmingTask: { - task: string; - codeWritingLevel: number; - resolved: number; - comment: string; - }; - skills: { - htmlCss: number; - common: number; - dataStructures: number; - }; + version: number; interviewer: { name: string; githubId: string; }; + // This type have to updated to refer to `InterviewFeedbackStepData`, when profile is migrated to nestjs + feedback: + | LegacyFeedback + | { + steps: Record< + string, + { + isCompleted: boolean; + values?: Record; + } + >; + }; } export interface UserInfo { @@ -159,3 +160,19 @@ export interface UserInfo { contacts?: Contacts; discord: Discord | null; } + +export type LegacyFeedback = { + english?: EnglishLevel; + comment: string; + programmingTask: { + task: string; + codeWritingLevel: number; + resolved: number; + comment: string; + }; + skills: { + htmlCss: number; + common: number; + dataStructures: number; + }; +}; diff --git a/nestjs/src/courses/interviews/interviews.service.ts b/nestjs/src/courses/interviews/interviews.service.ts index 391ccf68ef..fc31557e38 100644 --- a/nestjs/src/courses/interviews/interviews.service.ts +++ b/nestjs/src/courses/interviews/interviews.service.ts @@ -43,19 +43,23 @@ export class InterviewsService { }); } - public static getStageInterviewRating = (stageInterviews: StageInterview[]) => { + public static getLastStageInterview = (stageInterviews: StageInterview[]) => { const [lastInterview] = stageInterviews .filter(interview => interview.isCompleted) - .map(({ stageInterviewFeedbacks }) => + .map(({ stageInterviewFeedbacks, score, courseTask }) => stageInterviewFeedbacks.map(feedback => ({ date: feedback.updatedDate, - rating: InterviewsService.getInterviewRatings(JSON.parse(feedback.json) as StageInterviewFeedbackJson).rating, + rating: + score ?? + InterviewsService.getInterviewRatings(JSON.parse(feedback.json) as StageInterviewFeedbackJson).rating, + version: feedback.version, + maxScore: courseTask?.maxScore, })), ) .reduce((acc, cur) => acc.concat(cur), []) .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); - return lastInterview && lastInterview.rating !== undefined ? lastInterview.rating : null; + return lastInterview; }; public async getInterviewRegisteredStudents(courseId: number, courseTaskId: number): Promise { @@ -97,15 +101,19 @@ export class InterviewsService { .innerJoin('student.user', 'user') .leftJoin('student.stageInterviews', 'si') .leftJoin('si.stageInterviewFeedbacks', 'sif') + .leftJoin('si.courseTask', 'courseTask') .addSelect([ ...UsersService.getPrimaryUserFields(), 'si.id', 'si.isGoodCandidate', 'si.isCompleted', 'si.isCanceled', + 'si.score', 'sif.json', 'sif.updatedDate', + 'sif.version', 'sis.createdDate', + 'courseTask.maxScore', ]) .where( [ @@ -131,6 +139,7 @@ export class InterviewsService { .map(student => { const { id, user, totalScore } = student; const stageInterviews: StageInterview[] = student.stageInterviews || []; + const lastStageInterview = InterviewsService.getLastStageInterview(stageInterviews); return { id, totalScore, @@ -139,13 +148,19 @@ export class InterviewsService { cityName: user.cityName, countryName: user.countryName, isGoodCandidate: this.isGoodCandidate(stageInterviews), - rating: InterviewsService.getStageInterviewRating(stageInterviews), + rating: lastStageInterview?.rating, + maxScore: lastStageInterview?.maxScore, + feedbackVersion: lastStageInterview?.version, registeredDate: raw.find(item => item.student_id === student.id)?.sis_createdDate, }; }); + return result; } + /** + * @deprecated - should be removed once Artsiom A. makes migration of the legacy feedback format + */ private static getInterviewRatings({ skills, programmingTask, resume }: StageInterviewFeedbackJson) { const commonSkills = Object.values(skills.common).filter(Boolean) as number[]; const dataStructuresSkills = Object.values(skills.dataStructures).filter(Boolean) as number[]; @@ -155,7 +170,7 @@ export class InterviewsService { const dataStructures = dataStructuresSkills.reduce((acc, cur) => acc + cur, 0) / dataStructuresSkills.length; if (resume?.score !== undefined) { - const rating = resume.score / 10; + const rating = resume.score; return { rating, htmlCss, common, dataStructures }; } diff --git a/nestjs/src/courses/score/score.service.ts b/nestjs/src/courses/score/score.service.ts index 00438e4996..9dadc975fe 100644 --- a/nestjs/src/courses/score/score.service.ts +++ b/nestjs/src/courses/score/score.service.ts @@ -58,7 +58,7 @@ export class ScoreService { const [preScreeningInterview] = student.stageInterviews ?? []; const preScreeningScore = Math.floor( - (InterviewsService.getStageInterviewRating(student.stageInterviews ?? []) ?? 0) * 10, + InterviewsService.getLastStageInterview(student.stageInterviews ?? [])?.rating ?? 0, ); const preScreeningInterviewWithScore = preScreeningInterview ? { score: preScreeningScore, courseTaskId: preScreeningInterview.courseTaskId } @@ -115,7 +115,15 @@ export class ScoreService { .addSelect(UsersService.getPrimaryUserFields('mu')) .leftJoin('student.stageInterviews', 'si') .leftJoin('si.stageInterviewFeedbacks', 'sif') - .addSelect(['sif.stageInterviewId', 'sif.json', 'sif.updatedDate', 'si.isCompleted', 'si.id', 'si.courseTaskId']) + .addSelect([ + 'sif.stageInterviewId', + 'sif.json', + 'sif.updatedDate', + 'si.isCompleted', + 'si.id', + 'si.courseTaskId', + 'si.score', + ]) .where('student."courseId" = :courseId', { courseId }); if (filter.activeOnly === 'true') { diff --git a/server/src/routes/profile/stage-interview-feedback.ts b/server/src/routes/profile/stage-interview-feedback.ts index a4f7bd43cb..00e309dd90 100644 --- a/server/src/routes/profile/stage-interview-feedback.ts +++ b/server/src/routes/profile/stage-interview-feedback.ts @@ -1,8 +1,24 @@ import { getRepository } from 'typeorm'; import { StageInterviewDetailedFeedback } from '../../../../common/models/profile'; import { getFullName } from '../../rules'; -import { User, Mentor, Student, Course, StageInterview, StageInterviewFeedback } from '../../models'; +import { User, Mentor, Student, Course, StageInterview, StageInterviewFeedback, CourseTask } from '../../models'; import { stageInterviewService } from '../../services'; +import { StageInterviewFeedbackJson } from '../../../../common/models'; + +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; +}; export const getStageInterviewFeedback = async (githubId: string): Promise => ( @@ -10,13 +26,16 @@ export const getStageInterviewFeedback = async (githubId: string): Promise { - const interviewResult = JSON.parse(interviewResultJson); - const { english, programmingTask, resume } = interviewResult; - const { rating, htmlCss, common, dataStructures } = stageInterviewService.getInterviewRatings(interviewResult); + ) + .map((data: FeedbackData) => { + const { + feedbackVersion, + decision, + interviewFeedbackDate, + interviewerFirstName, + courseFullName, + courseName, + interviewerLastName, + interviewerGithubId, + isGoodCandidate, + interviewScore, + interviewResultJson, + maxScore, + } = data; + const feedbackTemplate = JSON.parse(interviewResultJson); + const { score, feedback } = !feedbackVersion + ? parseLegacyFeedback(feedbackTemplate) + : { + feedback: feedbackTemplate, + score: interviewScore ?? 0, + }; + return { + version: feedbackVersion ?? 0, + date: interviewFeedbackDate, decision, isGoodCandidate, courseName, courseFullName, - programmingTask, - rating, - comment: resume.comment, - english: english.levelMentorOpinion ? english.levelMentorOpinion : english.levelStudentOpinion, - date: interviewFeedbackDate, - skills: { - htmlCss, - common, - dataStructures, - }, + feedback, + score, interviewer: { name: getFullName(interviewerFirstName, interviewerLastName, interviewerGithubId), githubId: interviewerGithubId, }, - } as StageInterviewDetailedFeedback; + maxScore, + }; + }) + .filter(Boolean); + +// this is legacy form +function parseLegacyFeedback(interviewResult: StageInterviewFeedbackJson) { + const { english, programmingTask, resume } = interviewResult; + const { rating, htmlCss, common, dataStructures } = stageInterviewService.getInterviewRatings(interviewResult); + + return { + score: rating, + feedback: { + english: english.levelMentorOpinion ? english.levelMentorOpinion : english.levelStudentOpinion, + programmingTask, + comment: resume.comment, + skills: { + htmlCss, + common, + dataStructures, + }, }, - ); + }; +} diff --git a/server/src/services/course.service.ts b/server/src/services/course.service.ts index 934180c601..0e15ccedb5 100644 --- a/server/src/services/course.service.ts +++ b/server/src/services/course.service.ts @@ -463,7 +463,7 @@ export async function getStudentScore(studentId: number) { // we have a case when technical screening score are set as task result. if (stageInterviews?.length && !results.find(tr => tr.courseTaskId === stageInterviews[0].courseTaskId)) { results.push({ - score: Math.floor((getStageInterviewRating(stageInterviews) ?? 0) * 10), + score: Math.floor(getStageInterviewRating(stageInterviews) ?? 0), courseTaskId: stageInterviews[0].courseTaskId, }); } diff --git a/server/src/services/score/score.service.ts b/server/src/services/score/score.service.ts index 7b6408171d..99f6c1de32 100644 --- a/server/src/services/score/score.service.ts +++ b/server/src/services/score/score.service.ts @@ -136,7 +136,15 @@ export class ScoreService { .addSelect(getPrimaryUserFields('mu')) .leftJoin('student.stageInterviews', 'si') .leftJoin('si.stageInterviewFeedbacks', 'sif') - .addSelect(['sif.stageInterviewId', 'sif.json', 'sif.updatedDate', 'si.isCompleted', 'si.id', 'si.courseTaskId']) + .addSelect([ + 'sif.stageInterviewId', + 'sif.json', + 'sif.updatedDate', + 'si.isCompleted', + 'si.id', + 'si.courseTaskId', + 'si.score', + ]) .where('student."courseId" = :courseId', { courseId: this.courseId }); if (this.options.includeCertificate) { @@ -173,7 +181,7 @@ export class ScoreService { const content = await query.orderBy(orderByFieldMapping[orderBy.field], orderBy.direction).getMany(); const students = content.map(student => { - const preScreeningScore = Math.floor((getStageInterviewRating(student.stageInterviews ?? []) ?? 0) * 10); + const preScreeningScore = Math.floor(getStageInterviewRating(student.stageInterviews ?? []) ?? 0); const preScreningInterviews = student.stageInterviews?.length ? [{ score: preScreeningScore, courseTaskId: student.stageInterviews[0].courseTaskId }] : []; diff --git a/server/src/services/stageInterview.service.ts b/server/src/services/stageInterview.service.ts index 3a6c462d9f..50c3fb459f 100644 --- a/server/src/services/stageInterview.service.ts +++ b/server/src/services/stageInterview.service.ts @@ -1,60 +1,5 @@ -import { StageInterview, StageInterviewFeedback, StageInterviewStudent, Student } from '../models'; -import { getRepository } from 'typeorm'; +import { StageInterview, StageInterviewFeedback } from '../models'; import { StageInterviewFeedbackJson } from '../../../common/models'; -import * as courseService from './course.service'; - -export async function getAvailableStudents(courseId: number) { - const students = await getRepository(Student) - .createQueryBuilder('student') - .innerJoin(StageInterviewStudent, 'sis', 'sis.studentId = student.id') - .innerJoin('student.user', 'user') - .leftJoin('student.stageInterviews', 'si') - .leftJoin('si.stageInterviewFeedbacks', 'sif') - .addSelect([ - ...courseService.getPrimaryUserFields('user'), - 'si.id', - 'si.isGoodCandidate', - 'si.isCompleted', - 'si.isCanceled', - 'sif.json', - 'sif.updatedDate', - ]) - .where( - [ - `student.courseId = :courseId`, - `student.isFailed = false`, - `student.isExpelled = false`, - `student.mentorId IS NULL`, - `student.mentoring <> false`, - ].join(' AND '), - { courseId }, - ) - .orderBy('student.totalScore', 'DESC') - .getMany(); - - const result = students - .filter(s => { - return ( - !s.stageInterviews || - s.stageInterviews.length === 0 || - s.stageInterviews.every(i => i.isCompleted || i.isCanceled) - ); - }) - .map(student => { - const { id, user, totalScore } = student; - const stageInterviews: StageInterview[] = student.stageInterviews || []; - return { - id, - totalScore, - githubId: user.githubId, - name: `${user.firstName} ${user.lastName}`.trim(), - cityName: user.cityName, - isGoodCandidate: isGoodCandidate(stageInterviews), - rating: getStageInterviewRating(stageInterviews), - }; - }); - return result; -} export function getInterviewRatings({ skills, programmingTask, resume }: StageInterviewFeedbackJson) { const commonSkills = Object.values(skills.common).filter(Boolean) as number[]; @@ -65,7 +10,7 @@ export function getInterviewRatings({ skills, programmingTask, resume }: StageIn const dataStructures = dataStructuresSkills.reduce((acc, cur) => acc + cur, 0) / dataStructuresSkills.length; if (resume?.score !== undefined) { - const rating = resume.score / 10; + const rating = resume.score; return { rating, htmlCss, common, dataStructures }; } @@ -76,16 +21,14 @@ export function getInterviewRatings({ skills, programmingTask, resume }: StageIn return { rating, htmlCss, common, dataStructures }; } -const isGoodCandidate = (stageInterviews: StageInterview[]) => - stageInterviews.some(i => i.isCompleted && i.isGoodCandidate); - export const getStageInterviewRating = (stageInterviews: StageInterview[]) => { const [lastInterview] = stageInterviews .filter((interview: StageInterview) => interview.isCompleted) - .map(({ stageInterviewFeedbacks }: StageInterview) => + .map(({ stageInterviewFeedbacks, score }: StageInterview) => stageInterviewFeedbacks.map((feedback: StageInterviewFeedback) => ({ date: feedback.updatedDate, - rating: getInterviewRatings(JSON.parse(feedback.json)).rating, + // interviews in new template should have score precalculated + rating: score ?? getInterviewRatings(JSON.parse(feedback.json)).rating, })), ) .reduce((acc, cur) => acc.concat(cur), [])