diff --git a/client/src/api/api.ts b/client/src/api/api.ts index ed9b222133..c5806faebf 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -379,6 +379,18 @@ export interface AvailableStudentDto { * @memberof AvailableStudentDto */ 'registeredDate': string; + /** + * + * @type {number} + * @memberof AvailableStudentDto + */ + 'maxScore': number; + /** + * + * @type {number} + * @memberof AvailableStudentDto + */ + 'feedbackVersion': number; } /** * @@ -3079,6 +3091,37 @@ export interface InterviewDto { */ 'attributes': Attributes; } +/** + * + * @export + * @interface InterviewFeedbackDto + */ +export interface InterviewFeedbackDto { + /** + * + * @type {number} + * @memberof InterviewFeedbackDto + */ + 'version'?: number; + /** + * + * @type {object} + * @memberof InterviewFeedbackDto + */ + 'json'?: object; + /** + * + * @type {boolean} + * @memberof InterviewFeedbackDto + */ + 'isCompleted': boolean; + /** + * + * @type {number} + * @memberof InterviewFeedbackDto + */ + 'maxScore': number; +} /** * * @export @@ -3996,6 +4039,49 @@ export interface PublicVisibilitySettings { */ 'all': boolean; } +/** + * + * @export + * @interface PutInterviewFeedbackDto + */ +export interface PutInterviewFeedbackDto { + /** + * + * @type {number} + * @memberof PutInterviewFeedbackDto + */ + 'version': number; + /** + * + * @type {object} + * @memberof PutInterviewFeedbackDto + */ + 'json': object; + /** + * + * @type {string} + * @memberof PutInterviewFeedbackDto + */ + 'decision'?: string; + /** + * + * @type {boolean} + * @memberof PutInterviewFeedbackDto + */ + 'isGoodCandidate'?: boolean; + /** + * + * @type {boolean} + * @memberof PutInterviewFeedbackDto + */ + 'isCompleted': boolean; + /** + * + * @type {number} + * @memberof PutInterviewFeedbackDto + */ + 'score'?: number; +} /** * * @export @@ -8614,6 +8700,53 @@ export class CoursesEventsApi extends BaseAPI { */ export const CoursesInterviewsApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @param {number} courseId + * @param {number} interviewId + * @param {string} type + * @param {PutInterviewFeedbackDto} putInterviewFeedbackDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createInterviewFeedback: async (courseId: number, interviewId: number, type: string, putInterviewFeedbackDto: PutInterviewFeedbackDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'courseId' is not null or undefined + assertParamExists('createInterviewFeedback', 'courseId', courseId) + // verify required parameter 'interviewId' is not null or undefined + assertParamExists('createInterviewFeedback', 'interviewId', interviewId) + // verify required parameter 'type' is not null or undefined + assertParamExists('createInterviewFeedback', 'type', type) + // verify required parameter 'putInterviewFeedbackDto' is not null or undefined + assertParamExists('createInterviewFeedback', 'putInterviewFeedbackDto', putInterviewFeedbackDto) + const localVarPath = `/courses/{courseId}/interviews/{interviewId}/{type}/feedback` + .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))) + .replace(`{${"interviewId"}}`, encodeURIComponent(String(interviewId))) + .replace(`{${"type"}}`, encodeURIComponent(String(type))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(putInterviewFeedbackDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {number} courseId @@ -8679,6 +8812,47 @@ export const CoursesInterviewsApiAxiosParamCreator = function (configuration?: C + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {number} courseId + * @param {number} interviewId + * @param {string} type + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getInterviewFeedback: async (courseId: number, interviewId: number, type: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'courseId' is not null or undefined + assertParamExists('getInterviewFeedback', 'courseId', courseId) + // verify required parameter 'interviewId' is not null or undefined + assertParamExists('getInterviewFeedback', 'interviewId', interviewId) + // verify required parameter 'type' is not null or undefined + assertParamExists('getInterviewFeedback', 'type', type) + const localVarPath = `/courses/{courseId}/interviews/{interviewId}/{type}/feedback` + .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))) + .replace(`{${"interviewId"}}`, encodeURIComponent(String(interviewId))) + .replace(`{${"type"}}`, encodeURIComponent(String(type))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -8741,6 +8915,19 @@ export const CoursesInterviewsApiAxiosParamCreator = function (configuration?: C export const CoursesInterviewsApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = CoursesInterviewsApiAxiosParamCreator(configuration) return { + /** + * + * @param {number} courseId + * @param {number} interviewId + * @param {string} type + * @param {PutInterviewFeedbackDto} putInterviewFeedbackDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createInterviewFeedback(courseId: number, interviewId: number, type: string, putInterviewFeedbackDto: PutInterviewFeedbackDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createInterviewFeedback(courseId, interviewId, type, putInterviewFeedbackDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {number} courseId @@ -8763,6 +8950,18 @@ export const CoursesInterviewsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getInterview(interviewId, courseId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {number} courseId + * @param {number} interviewId + * @param {string} type + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getInterviewFeedback(courseId: number, interviewId: number, type: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getInterviewFeedback(courseId, interviewId, type, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {number} courseId @@ -8785,6 +8984,18 @@ export const CoursesInterviewsApiFp = function(configuration?: Configuration) { export const CoursesInterviewsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = CoursesInterviewsApiFp(configuration) return { + /** + * + * @param {number} courseId + * @param {number} interviewId + * @param {string} type + * @param {PutInterviewFeedbackDto} putInterviewFeedbackDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createInterviewFeedback(courseId: number, interviewId: number, type: string, putInterviewFeedbackDto: PutInterviewFeedbackDto, options?: any): AxiosPromise { + return localVarFp.createInterviewFeedback(courseId, interviewId, type, putInterviewFeedbackDto, options).then((request) => request(axios, basePath)); + }, /** * * @param {number} courseId @@ -8805,6 +9016,17 @@ export const CoursesInterviewsApiFactory = function (configuration?: Configurati getInterview(interviewId: number, courseId: number, options?: any): AxiosPromise { return localVarFp.getInterview(interviewId, courseId, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {number} courseId + * @param {number} interviewId + * @param {string} type + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getInterviewFeedback(courseId: number, interviewId: number, type: string, options?: any): AxiosPromise { + return localVarFp.getInterviewFeedback(courseId, interviewId, type, options).then((request) => request(axios, basePath)); + }, /** * * @param {number} courseId @@ -8826,6 +9048,20 @@ export const CoursesInterviewsApiFactory = function (configuration?: Configurati * @extends {BaseAPI} */ export class CoursesInterviewsApi extends BaseAPI { + /** + * + * @param {number} courseId + * @param {number} interviewId + * @param {string} type + * @param {PutInterviewFeedbackDto} putInterviewFeedbackDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CoursesInterviewsApi + */ + public createInterviewFeedback(courseId: number, interviewId: number, type: string, putInterviewFeedbackDto: PutInterviewFeedbackDto, options?: AxiosRequestConfig) { + return CoursesInterviewsApiFp(this.configuration).createInterviewFeedback(courseId, interviewId, type, putInterviewFeedbackDto, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {number} courseId @@ -8850,6 +9086,19 @@ export class CoursesInterviewsApi extends BaseAPI { return CoursesInterviewsApiFp(this.configuration).getInterview(interviewId, courseId, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {number} courseId + * @param {number} interviewId + * @param {string} type + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CoursesInterviewsApi + */ + public getInterviewFeedback(courseId: number, interviewId: number, type: string, options?: AxiosRequestConfig) { + return CoursesInterviewsApiFp(this.configuration).getInterviewFeedback(courseId, interviewId, type, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {number} courseId diff --git a/client/src/services/course.ts b/client/src/services/course.ts index f0b648d37d..c3797d1103 100644 --- a/client/src/services/course.ts +++ b/client/src/services/course.ts @@ -443,16 +443,17 @@ export class CourseService { return result.data.data; } + /** + * @deprecated. should be removed after feedbacks are migrated to new template + */ async getInterviewerStageInterviews(githubId: string) { const result = await this.axios.get(`/interview/stage/interviewer/${githubId}/students`); return result.data.data as { id: number; completed: boolean; student: StudentBasic }[]; } - async postStageInterviews(stageId: number) { - const result = await this.axios.post(`/stage/${stageId}/interviews`); - return result.data.data; - } - + /** + * @deprecated. should be removed after feedbacks are migrated to new template + */ async postStageInterviewFeedback( interviewId: number, data: { json: unknown; githubId: string; isGoodCandidate: boolean; isCompleted: boolean; decision: string }, @@ -461,13 +462,12 @@ export class CourseService { return result.data.data; } + /** + * @deprecated. should be removed after feedbacks are migrated to new template + */ async getStageInterviewFeedback(interviewId: number) { const result = await this.axios.get(`/interview/stage/${interviewId}/feedback`); - return result.data.data; - } - async getStageInterviewsByStudent(githubId: string) { - const result = await this.axios.get(`/student/${githubId}/interviews`); return result.data.data; } diff --git a/nestjs/src/courses/courses.module.ts b/nestjs/src/courses/courses.module.ts index 992938b78c..d46d485d35 100644 --- a/nestjs/src/courses/courses.module.ts +++ b/nestjs/src/courses/courses.module.ts @@ -36,7 +36,7 @@ import { StudentsService, StudentsController } from './students'; import { MentorsService, MentorsController } from './mentors'; import { CourseAccessService } from './course-access.service'; import { CourseTasksController, CourseTasksService } from './course-tasks'; -import { InterviewsController, InterviewsService } from './interviews'; +import { InterviewsController, InterviewsService, InterviewFeedbackService } from './interviews'; import { TasksController } from './tasks/tasks.controller'; import { TasksService } from './tasks/tasks.service'; import { CourseStatsController, CourseStatsService } from './stats'; @@ -126,6 +126,7 @@ import { SelfEducationService } from './task-verifications/self-education.servic MentorsService, CourseAccessService, InterviewsService, + InterviewFeedbackService, TasksService, CourseStatsService, CourseCrossCheckService, diff --git a/nestjs/src/courses/interviews/dto/available-student.dto.ts b/nestjs/src/courses/interviews/dto/available-student.dto.ts index 069ed09e4d..88bca7077e 100644 --- a/nestjs/src/courses/interviews/dto/available-student.dto.ts +++ b/nestjs/src/courses/interviews/dto/available-student.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsNotEmpty, IsNumber, IsString } from 'class-validator'; +import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; export class AvailableStudentDto { @IsNotEmpty() @@ -42,4 +42,12 @@ export class AvailableStudentDto { @IsString() @ApiProperty() registeredDate: string; + + @IsOptional() + @ApiProperty() + maxScore?: number; + + @IsOptional() + @ApiProperty() + feedbackVersion?: number; } diff --git a/nestjs/src/courses/interviews/dto/get-interview-feedback.dto.ts b/nestjs/src/courses/interviews/dto/get-interview-feedback.dto.ts new file mode 100644 index 0000000000..47506c3586 --- /dev/null +++ b/nestjs/src/courses/interviews/dto/get-interview-feedback.dto.ts @@ -0,0 +1,34 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber } from 'class-validator'; + +export class InterviewFeedbackDto { + constructor(data: { + feedback: { + json: Record; + version: number; + } | null; + isCompleted: boolean; + maxScore: number; + }) { + this.version = data.feedback?.version; + this.json = data.feedback?.json; + this.maxScore = data.maxScore; + this.isCompleted = data.isCompleted ?? false; + } + + @IsNumber() + @ApiProperty({ required: false }) + version?: number; + + @ApiProperty({ required: false }) + @IsNotEmpty() + json?: Record; + + @ApiProperty() + @IsNotEmpty() + isCompleted: boolean; + + @ApiProperty() + @IsNotEmpty() + maxScore: number; +} diff --git a/nestjs/src/courses/interviews/dto/put-interview-feedback.dto.ts b/nestjs/src/courses/interviews/dto/put-interview-feedback.dto.ts new file mode 100644 index 0000000000..5a4a0359a1 --- /dev/null +++ b/nestjs/src/courses/interviews/dto/put-interview-feedback.dto.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; + +export class PutInterviewFeedbackDto { + @IsNotEmpty() + @IsNumber() + @ApiProperty() + version: number; + + @ApiProperty() + @IsNotEmpty() + json: Record; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + decision?: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsBoolean() + isGoodCandidate?: boolean; + + @ApiProperty() + @IsNotEmpty() + isCompleted: boolean; + + @ApiProperty({ required: false }) + @IsOptional() + score: number; +} diff --git a/nestjs/src/courses/interviews/index.ts b/nestjs/src/courses/interviews/index.ts index a9e290346e..50414e54e1 100644 --- a/nestjs/src/courses/interviews/index.ts +++ b/nestjs/src/courses/interviews/index.ts @@ -1,2 +1,3 @@ export * from './interviews.service'; export * from './interviews.controller'; +export * from './interviewFeedback.service'; diff --git a/nestjs/src/courses/interviews/interviewFeedback.service.ts b/nestjs/src/courses/interviews/interviewFeedback.service.ts new file mode 100644 index 0000000000..a951c03530 --- /dev/null +++ b/nestjs/src/courses/interviews/interviewFeedback.service.ts @@ -0,0 +1,98 @@ +import { Repository } from 'typeorm'; +import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { StageInterview, StageInterviewFeedback } from '@entities/index'; +import { StudentsService } from '../students'; +import { PutInterviewFeedbackDto } from './dto/put-interview-feedback.dto'; + +@Injectable() +export class InterviewFeedbackService { + constructor( + @InjectRepository(StageInterview) + readonly stageInterviewRepository: Repository, + @InjectRepository(StageInterviewFeedback) + readonly stageInterviewFeedbackRepository: Repository, + readonly studentsService: StudentsService, + ) {} + + public async getStageInterviewFeedback(interviewId: number, interviewerGithubId: string) { + const interview = await this.stageInterviewRepository.findOne({ + where: { + id: interviewId, + mentor: { + user: { + githubId: interviewerGithubId, + }, + }, + }, + relations: ['stageInterviewFeedbacks', 'mentor', 'mentor.user', 'courseTask'], + }); + + if (!interview) { + throw new NotFoundException(`Interview not found ${interviewId}`); + } + + const { courseTask, stageInterviewFeedbacks } = interview; + const feedback = stageInterviewFeedbacks.pop(); + + return { + feedback: feedback ? this.parseFeedback(feedback) : null, + isCompleted: interview.isCompleted, + maxScore: courseTask.maxScore, + }; + } + + public async upsertInterviewFeedback({ + interviewId, + dto, + interviewerId, + }: { + interviewId: number; + dto: PutInterviewFeedbackDto; + interviewerId: number; + }) { + const interview = await this.stageInterviewRepository.findOneBy({ id: interviewId, mentorId: interviewerId }); + + if (!interview) { + throw new ForbiddenException(); + } + + const { studentId } = interview; + const { decision, isGoodCandidate, isCompleted, score } = dto; + + await Promise.all([ + this.saveFeedback(interview.id, dto), + this.stageInterviewRepository.update(interviewId, { + isCompleted, + decision, + isGoodCandidate, + score, + }), + decision === 'yes' ? this.studentsService.setMentor(studentId, interviewerId) : Promise.resolve(), + ]); + } + + private async saveFeedback(stageInterviewId: number, data: PutInterviewFeedbackDto) { + const feedback = await this.stageInterviewFeedbackRepository.findOne({ where: { stageInterviewId } }); + + const newFeedback = { stageInterviewId, json: JSON.stringify(data.json), version: data.version }; + + if (feedback) { + await this.stageInterviewFeedbackRepository.update(feedback.id, newFeedback); + } else { + await this.stageInterviewFeedbackRepository.insert(newFeedback); + } + } + + /** + * `json` stores the feedback in the form, which depends on the version. + */ + private parseFeedback(feedback: StageInterviewFeedback) { + const { json, version } = feedback; + + return { + json: json ? (JSON.parse(json) as Record) : {}, + version: version ?? 0, + }; + } +} diff --git a/nestjs/src/courses/interviews/interviews.controller.ts b/nestjs/src/courses/interviews/interviews.controller.ts index ef6afa0b95..d9a51fff9e 100644 --- a/nestjs/src/courses/interviews/interviews.controller.ts +++ b/nestjs/src/courses/interviews/interviews.controller.ts @@ -1,14 +1,18 @@ import { BadRequestException, + Body, CacheInterceptor, CacheTTL, Controller, + ForbiddenException, Get, NotFoundException, Param, ParseArrayPipe, ParseIntPipe, + Post, Query, + Req, UseGuards, UseInterceptors, } from '@nestjs/common'; @@ -21,18 +25,24 @@ import { ApiQuery, ApiTags, } from '@nestjs/swagger'; -import { CourseGuard, CourseRole, DefaultGuard, RequiredRoles, RoleGuard } from '../../auth'; +import { CourseGuard, CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, RoleGuard } from '../../auth'; import { DEFAULT_CACHE_TTL } from '../../constants'; import { InterviewDto } from './dto'; import { AvailableStudentDto } from './dto/available-student.dto'; import { InterviewsService } from './interviews.service'; import { TaskType } from '@entities/task'; +import { InterviewFeedbackService } from './interviewFeedback.service'; +import { InterviewFeedbackDto } from './dto/get-interview-feedback.dto'; +import { PutInterviewFeedbackDto } from './dto/put-interview-feedback.dto'; @Controller('courses/:courseId/interviews') @ApiTags('courses interviews') @UseGuards(DefaultGuard, CourseGuard, RoleGuard) export class InterviewsController { - constructor(private courseTasksService: InterviewsService) {} + constructor( + private courseTasksService: InterviewsService, + private interviewFeedbackService: InterviewFeedbackService, + ) {} @Get() @CacheTTL(DEFAULT_CACHE_TTL) @@ -96,4 +106,59 @@ export class InterviewsController { throw new BadRequestException('Invalid interview id'); } + + // use `type` as a way to differentiate between stage-interview and interview. + @Get('/:interviewId/:type/feedback') + @ApiOkResponse({ type: InterviewFeedbackDto }) + @ApiForbiddenResponse() + @ApiBadRequestResponse() + @ApiOperation({ operationId: 'getInterviewFeedback' }) + @RequiredRoles([CourseRole.Mentor, CourseRole.Supervisor, CourseRole.Manager]) + public async getInterviewFeedback( + @Param('courseId', ParseIntPipe) _: number, + @Param('interviewId', ParseIntPipe) interviewId: number, + @Param('type') type: 'stage-interview' | 'interview', + @Req() req: CurrentRequest, + ) { + const { user } = req; + + if (type !== 'stage-interview') { + throw new BadRequestException('Only stage interviews are supported now.'); + } + + const interview = await this.interviewFeedbackService.getStageInterviewFeedback(interviewId, user.githubId); + + return new InterviewFeedbackDto(interview); + } + + @Post('/:interviewId/:type/feedback') + @ApiOkResponse() + @ApiForbiddenResponse() + @ApiBadRequestResponse() + @ApiOperation({ operationId: 'createInterviewFeedback' }) + @RequiredRoles([CourseRole.Mentor, CourseRole.Supervisor, CourseRole.Manager]) + public async createInterviewFeedback( + @Param('courseId', ParseIntPipe) courseId: number, + @Param('interviewId', ParseIntPipe) interviewId: number, + @Param('type') type: 'stage-interview' | 'interview', + @Body() dto: PutInterviewFeedbackDto, + @Req() req: CurrentRequest, + ) { + const { user } = req; + + const interviewerId = user.courses[courseId]?.mentorId; + if (!interviewerId) { + throw new ForbiddenException(`You are not a mentor of course ${courseId}`); + } + + if (type !== 'stage-interview') { + throw new BadRequestException('Only stage interviews are supported now.'); + } + + await this.interviewFeedbackService.upsertInterviewFeedback({ + interviewId, + dto, + interviewerId, + }); + } } diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json index 6867d4ec17..881b012c2f 100644 --- a/nestjs/src/spec.json +++ b/nestjs/src/spec.json @@ -602,6 +602,41 @@ "tags": ["courses interviews"] } }, + "/courses/{courseId}/interviews/{interviewId}/{type}/feedback": { + "get": { + "operationId": "getInterviewFeedback", + "summary": "", + "parameters": [ + { "name": "courseId", "required": true, "in": "path", "schema": { "type": "number" } }, + { "name": "interviewId", "required": true, "in": "path", "schema": { "type": "number" } }, + { "name": "type", "required": true, "in": "path", "schema": { "type": "string" } } + ], + "responses": { + "200": { + "description": "", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InterviewFeedbackDto" } } } + }, + "400": { "description": "" }, + "403": { "description": "" } + }, + "tags": ["courses interviews"] + }, + "post": { + "operationId": "createInterviewFeedback", + "summary": "", + "parameters": [ + { "name": "courseId", "required": true, "in": "path", "schema": { "type": "number" } }, + { "name": "interviewId", "required": true, "in": "path", "schema": { "type": "number" } }, + { "name": "type", "required": true, "in": "path", "schema": { "type": "string" } } + ], + "requestBody": { + "required": true, + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PutInterviewFeedbackDto" } } } + }, + "responses": { "200": { "description": "" }, "400": { "description": "" }, "403": { "description": "" } }, + "tags": ["courses interviews"] + } + }, "/tasks/notify/changes": { "post": { "operationId": "notifyTasksDeadlines", @@ -2791,7 +2826,9 @@ "isGoodCandidate": { "type": "boolean" }, "rating": { "type": "string", "nullable": true }, "totalScore": { "type": "number" }, - "registeredDate": { "type": "string" } + "registeredDate": { "type": "string" }, + "maxScore": { "type": "number" }, + "feedbackVersion": { "type": "number" } }, "required": [ "id", @@ -2802,9 +2839,33 @@ "isGoodCandidate", "rating", "totalScore", - "registeredDate" + "registeredDate", + "maxScore", + "feedbackVersion" ] }, + "InterviewFeedbackDto": { + "type": "object", + "properties": { + "version": { "type": "number" }, + "json": { "type": "object" }, + "isCompleted": { "type": "boolean" }, + "maxScore": { "type": "number" } + }, + "required": ["isCompleted", "maxScore"] + }, + "PutInterviewFeedbackDto": { + "type": "object", + "properties": { + "version": { "type": "number" }, + "json": { "type": "object" }, + "decision": { "type": "string" }, + "isGoodCandidate": { "type": "boolean" }, + "isCompleted": { "type": "boolean" }, + "score": { "type": "number" } + }, + "required": ["version", "json", "isCompleted"] + }, "CheckTasksDeadlineDto": { "type": "object", "properties": { "deadlineInHours": { "type": "number" } }, diff --git a/server/src/repositories/stageInterviewFeedback.repository.ts b/server/src/repositories/stageInterviewFeedback.repository.ts index 7e663627b4..40675ba273 100644 --- a/server/src/repositories/stageInterviewFeedback.repository.ts +++ b/server/src/repositories/stageInterviewFeedback.repository.ts @@ -28,6 +28,9 @@ export class StageInterviewFeedbackRepository extends AbstractRepository, logger: ILogger) { stageInterview.getInterviewerStudents(logger), ); + /** + * @deprecated. should be removed after feedbacks are migrated to new template + */ router.get('/interview/stage/:interviewId/feedback', courseMentorGuard, stageInterview.getFeedback(logger)); router.post('/interview/stage/:interviewId/feedback', courseMentorGuard, stageInterview.createFeedback(logger)); diff --git a/server/src/routes/course/stageInterview/createFeedback.ts b/server/src/routes/course/stageInterview/createFeedback.ts index 2ebb2f9179..8132f30f75 100644 --- a/server/src/routes/course/stageInterview/createFeedback.ts +++ b/server/src/routes/course/stageInterview/createFeedback.ts @@ -16,6 +16,9 @@ type BodyParams = { isGoodCandidate: boolean | null; }; +/** + * @deprecated. should be removed after feedbacks are migrated to new template + */ export const createFeedback = (_: ILogger) => async (ctx: Router.RouterContext) => { const data: BodyParams = ctx.request.body; const { courseId } = ctx.params; diff --git a/server/src/routes/course/stageInterview/getFeedback.ts b/server/src/routes/course/stageInterview/getFeedback.ts index ebefa5abe3..d17c71fcf7 100644 --- a/server/src/routes/course/stageInterview/getFeedback.ts +++ b/server/src/routes/course/stageInterview/getFeedback.ts @@ -6,6 +6,9 @@ import { IUserSession } from '../../../models'; import { StageInterviewFeedbackRepository } from '../../../repositories/stageInterviewFeedback.repository'; import { setResponse } from '../../utils'; +/** + * @deprecated. should be removed after feedbacks are migrated to new template + */ export const getFeedback = (_: ILogger) => async (ctx: Router.RouterContext) => { const { interviewId } = ctx.params; const mentorGithubId = (ctx.state!.user as IUserSession).githubId;