From c7078bfdb37bc3357f5fdfc4fa2df8f4c7cef476 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Mon, 22 Apr 2024 21:09:05 +0300 Subject: [PATCH 01/26] feat: implement nestjs route returning mock data --- client/src/api/api.ts | 70 +++++++++++++++++++ .../src/modules/Home/pages/HomePage/index.tsx | 1 + .../course-students.controller.ts | 57 +++++++++++++++ .../course-students.service.ts | 17 +++++ .../dto/mentor-student-summary.dto.ts | 68 ++++++++++++++++++ .../dto/student-summary.dto.ts | 45 ++++++++++++ nestjs/src/courses/courses.module.ts | 4 ++ nestjs/src/spec.json | 12 ++++ server/src/routes/course/student.ts | 1 + 9 files changed, 275 insertions(+) create mode 100644 nestjs/src/courses/course-students/course-students.controller.ts create mode 100644 nestjs/src/courses/course-students/course-students.service.ts create mode 100644 nestjs/src/courses/course-students/dto/mentor-student-summary.dto.ts create mode 100644 nestjs/src/courses/course-students/dto/student-summary.dto.ts diff --git a/client/src/api/api.ts b/client/src/api/api.ts index d8bb953fdf..1c03b56f10 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -15248,6 +15248,43 @@ export const StudentsApiAxiosParamCreator = function (configuration?: Configurat + 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 {string} studentId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getStudentSummary: async (courseId: number, studentId: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'courseId' is not null or undefined + assertParamExists('getStudentSummary', 'courseId', courseId) + // verify required parameter 'studentId' is not null or undefined + assertParamExists('getStudentSummary', 'studentId', studentId) + const localVarPath = `/courses/{courseId}/students/{studentId}/summary` + .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))) + .replace(`{${"studentId"}}`, encodeURIComponent(String(studentId))); + // 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}; @@ -15277,6 +15314,17 @@ export const StudentsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getStudent(studentId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {number} courseId + * @param {string} studentId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getStudentSummary(courseId: number, studentId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getStudentSummary(courseId, studentId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -15296,6 +15344,16 @@ export const StudentsApiFactory = function (configuration?: Configuration, baseP getStudent(studentId: number, options?: any): AxiosPromise { return localVarFp.getStudent(studentId, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {number} courseId + * @param {string} studentId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getStudentSummary(courseId: number, studentId: string, options?: any): AxiosPromise { + return localVarFp.getStudentSummary(courseId, studentId, options).then((request) => request(axios, basePath)); + }, }; }; @@ -15316,6 +15374,18 @@ export class StudentsApi extends BaseAPI { public getStudent(studentId: number, options?: AxiosRequestConfig) { return StudentsApiFp(this.configuration).getStudent(studentId, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @param {number} courseId + * @param {string} studentId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof StudentsApi + */ + public getStudentSummary(courseId: number, studentId: string, options?: AxiosRequestConfig) { + return StudentsApiFp(this.configuration).getStudentSummary(courseId, studentId, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/client/src/modules/Home/pages/HomePage/index.tsx b/client/src/modules/Home/pages/HomePage/index.tsx index 8ddff9fedf..d862640007 100644 --- a/client/src/modules/Home/pages/HomePage/index.tsx +++ b/client/src/modules/Home/pages/HomePage/index.tsx @@ -69,6 +69,7 @@ export function HomePage() { }; const { courseTasks, studentSummary } = useStudentSummary(session, course); + console.log(studentSummary) return ( diff --git a/nestjs/src/courses/course-students/course-students.controller.ts b/nestjs/src/courses/course-students/course-students.controller.ts new file mode 100644 index 0000000000..5efe56a4a8 --- /dev/null +++ b/nestjs/src/courses/course-students/course-students.controller.ts @@ -0,0 +1,57 @@ +import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; +import { ApiBadRequestResponse, ApiForbiddenResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { CurrentRequest, DefaultGuard } from '../../auth'; +// import { StudentsService } from '../students'; +import { StudentSummaryDto } from './dto/student-summary.dto'; + +@Controller('courses/:courseId/students') +@ApiTags('students') +@UseGuards(DefaultGuard) +export class CourseStudentsController { +// constructor(private studentsService: StudentsService) {} + + @Get(':studentId/summary') + @ApiForbiddenResponse() + @ApiBadRequestResponse() + @ApiOperation({ operationId: 'getStudentSummary' }) + public async getStudentSummary( + @Param('courseId') courseId: number, + @Param('studentId') githubId: string, + @Req() req: CurrentRequest, + ) { + let studentGithubId; + if (githubId === 'me') { + studentGithubId = req.user.githubId; + } else { + studentGithubId = githubId; + } + return new StudentSummaryDto({ + totalScore: 0, + rank: 5, + results: [ + { + score: 50, + courseTaskId: 719, + }, + ], + isActive: true, + mentor: { + isActive: true, + name: 'dmitry romaniuk', + id: 1273, + githubId: studentGithubId, + students: [], + cityName: 'Minsk', + countryName: 'Belarus', + contactsEmail: 'hello@example.com', + contactsSkype: null, + contactsWhatsApp: null, + contactsTelegram: 'pavel_durov', + contactsNotes: 'do not call me', + contactsPhone: null, + }, + repository: null, + discord: 'eleven' + }); + } +} diff --git a/nestjs/src/courses/course-students/course-students.service.ts b/nestjs/src/courses/course-students/course-students.service.ts new file mode 100644 index 0000000000..0e464d84bd --- /dev/null +++ b/nestjs/src/courses/course-students/course-students.service.ts @@ -0,0 +1,17 @@ +import { Student } from '@entities/student'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +@Injectable() +export class CourseStudentsService { + constructor( + @InjectRepository(Student) + readonly studentRepository: Repository, + ) {} + + + public async getStudentSummary(courseId: number, studentId: number) { + console.log('summary', { studentId, courseId }); + } +} diff --git a/nestjs/src/courses/course-students/dto/mentor-student-summary.dto.ts b/nestjs/src/courses/course-students/dto/mentor-student-summary.dto.ts new file mode 100644 index 0000000000..4c19d089b1 --- /dev/null +++ b/nestjs/src/courses/course-students/dto/mentor-student-summary.dto.ts @@ -0,0 +1,68 @@ +import { MentorBasic, StudentBasic } from '@common/models'; +import { ApiProperty } from '@nestjs/swagger'; + +export type Mentor = MentorBasic & { + contactsEmail: string | null; + contactsPhone: string | null; + contactsSkype: string | null; + contactsTelegram: string | null; + contactsNotes: string | null; + contactsWhatsApp: string | null; +}; + +export class MentorStudentSummaryDto { + constructor(mentor: Mentor) { + this.id = mentor.id; + this.githubId = mentor.githubId; + this.name = mentor.name; + this.isActive = mentor.isActive; + this.cityName = mentor.cityName; + this.countryName = mentor.countryName; + this.students = mentor.students; + this.contactsEmail = mentor.contactsEmail; + this.contactsPhone = mentor.contactsPhone; + this.contactsSkype = mentor.contactsSkype; + this.contactsTelegram = mentor.contactsTelegram; + this.contactsNotes = mentor.contactsNotes; + this.contactsWhatsApp = mentor.contactsWhatsApp; + } + + @ApiProperty({ type: Number }) + id: number; + + @ApiProperty({ type: String }) + githubId: string; + + @ApiProperty({ type: String }) + name: string; + + @ApiProperty({ type: Boolean }) + isActive: boolean; + + @ApiProperty({ type: String }) + cityName: string; + + @ApiProperty({ type: String }) + countryName: string; + + @ApiProperty({ type: Array }) + students: (StudentBasic | { id: number })[]; + + @ApiProperty({ type: String, nullable: true }) + contactsEmail: string | null; + + @ApiProperty({ type: String, nullable: true }) + contactsPhone: string | null; + + @ApiProperty({ type: String, nullable: true }) + contactsSkype: string | null; + + @ApiProperty({ type: String, nullable: true }) + contactsTelegram: string | null; + + @ApiProperty({ type: String, nullable: true }) + contactsNotes: string | null; + + @ApiProperty({ type: String, nullable: true }) + contactsWhatsApp: string | null; +} diff --git a/nestjs/src/courses/course-students/dto/student-summary.dto.ts b/nestjs/src/courses/course-students/dto/student-summary.dto.ts new file mode 100644 index 0000000000..b231c3571f --- /dev/null +++ b/nestjs/src/courses/course-students/dto/student-summary.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Mentor, MentorStudentSummaryDto } from './mentor-student-summary.dto'; + +export interface StudentSummary { + totalScore: number; + results: { score: number; courseTaskId: number }[]; + isActive: boolean; + discord: string; + mentor: Mentor | null; + rank: number; + repository: string | null; +} + +export class StudentSummaryDto { + constructor(studentSummary: StudentSummary) { + this.totalScore = studentSummary.totalScore; + this.results = studentSummary.results; + this.isActive = studentSummary.isActive; + this.discord = studentSummary.discord; + this.mentor = studentSummary.mentor; + this.rank = studentSummary.rank; + this.repository = studentSummary.repository; + } + + @ApiProperty() + totalScore: number; + + @ApiProperty({ type: Array<{ score: number; courseTaskId: number }> }) + results: { score: number; courseTaskId: number }[]; + + @ApiProperty() + isActive: boolean; + + @ApiProperty() + discord: string; + + @ApiProperty({ type: MentorStudentSummaryDto, nullable: true }) + mentor: MentorStudentSummaryDto | null; + + @ApiProperty({ type: Number }) + rank: number; + + @ApiProperty({type: String, nullable: true}) + repository: string | null; +} diff --git a/nestjs/src/courses/courses.module.ts b/nestjs/src/courses/courses.module.ts index 7dcff84da5..f28150007f 100644 --- a/nestjs/src/courses/courses.module.ts +++ b/nestjs/src/courses/courses.module.ts @@ -65,6 +65,8 @@ import { CourseUsersService } from './course-users/course-users.service'; import { CloudApiModule } from 'src/cloud-api/cloud-api.module'; import { SelfEducationService } from './task-verifications/self-education.service'; import { CourseMentorsController, CourseMentorsService } from './course-mentors'; +import { CourseStudentsController } from './course-students/course-students.controller'; +import { CourseStudentsService } from './course-students/course-students.service'; @Module({ imports: [ @@ -118,6 +120,7 @@ import { CourseMentorsController, CourseMentorsService } from './course-mentors' TaskVerificationsController, CourseUsersController, CourseMentorsController, + CourseStudentsController, ], providers: [ CourseTasksService, @@ -145,6 +148,7 @@ import { CourseMentorsController, CourseMentorsService } from './course-mentors' SelfEducationService, TaskVerificationsService, CourseMentorsService, + CourseStudentsService, ], exports: [CourseTasksService, CourseUsersService, CoursesService, StudentsService], }) diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json index 972b4d698f..de7daf9efe 100644 --- a/nestjs/src/spec.json +++ b/nestjs/src/spec.json @@ -1400,6 +1400,18 @@ "tags": ["course mentors"] } }, + "/courses/{courseId}/students/{studentId}/summary": { + "get": { + "operationId": "getStudentSummary", + "summary": "", + "parameters": [ + { "name": "courseId", "required": true, "in": "path", "schema": { "type": "number" } }, + { "name": "studentId", "required": true, "in": "path", "schema": { "type": "string" } } + ], + "responses": { "400": { "description": "" }, "403": { "description": "" } }, + "tags": ["students"] + } + }, "/users/notifications": { "get": { "operationId": "getUserNotifications", diff --git a/server/src/routes/course/student.ts b/server/src/routes/course/student.ts index 6999d373ad..aad2dd0c94 100644 --- a/server/src/routes/course/student.ts +++ b/server/src/routes/course/student.ts @@ -79,6 +79,7 @@ export const postFeedback = (_: ILogger) => async (ctx: Router.RouterContext) => export const getStudentSummary = (_: ILogger) => async (ctx: Router.RouterContext) => { const { courseId, githubId } = ctx.params; + console.log(ctx.params) const student = await courseService.getStudentByGithubId(courseId, githubId); if (student == null) { setResponse(ctx, OK, null); From 5925549e9bba2e4919a9fb86655eef7eca669176 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Sun, 28 Apr 2024 14:57:20 +0300 Subject: [PATCH 02/26] feat: implement getting student summary from new api --- client/src/api/api.ts | 158 +++++++++++++++-- client/src/services/course.ts | 6 +- .../course-students.controller.ts | 53 +++--- .../course-students.service.ts | 165 +++++++++++++++++- .../dto/student-summary.dto.ts | 22 +-- nestjs/src/spec.json | 58 +++++- server/src/routes/course/student.ts | 1 - 7 files changed, 397 insertions(+), 66 deletions(-) diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 1c03b56f10..cda44bfffb 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -3792,6 +3792,91 @@ export interface MentorStudentDto { */ 'repoUrl': string | null; } +/** + * + * @export + * @interface MentorStudentSummaryDto + */ +export interface MentorStudentSummaryDto { + /** + * + * @type {number} + * @memberof MentorStudentSummaryDto + */ + 'id': number; + /** + * + * @type {string} + * @memberof MentorStudentSummaryDto + */ + 'githubId': string; + /** + * + * @type {string} + * @memberof MentorStudentSummaryDto + */ + 'name': string; + /** + * + * @type {boolean} + * @memberof MentorStudentSummaryDto + */ + 'isActive': boolean; + /** + * + * @type {string} + * @memberof MentorStudentSummaryDto + */ + 'cityName': string; + /** + * + * @type {string} + * @memberof MentorStudentSummaryDto + */ + 'countryName': string; + /** + * + * @type {Array} + * @memberof MentorStudentSummaryDto + */ + 'students': Array; + /** + * + * @type {string} + * @memberof MentorStudentSummaryDto + */ + 'contactsEmail': string | null; + /** + * + * @type {string} + * @memberof MentorStudentSummaryDto + */ + 'contactsPhone': string | null; + /** + * + * @type {string} + * @memberof MentorStudentSummaryDto + */ + 'contactsSkype': string | null; + /** + * + * @type {string} + * @memberof MentorStudentSummaryDto + */ + 'contactsTelegram': string | null; + /** + * + * @type {string} + * @memberof MentorStudentSummaryDto + */ + 'contactsNotes': string | null; + /** + * + * @type {string} + * @memberof MentorStudentSummaryDto + */ + 'contactsWhatsApp': string | null; +} /** * * @export @@ -5117,6 +5202,49 @@ export interface StudentId { */ 'id': number; } +/** + * + * @export + * @interface StudentSummaryDto + */ +export interface StudentSummaryDto { + /** + * + * @type {number} + * @memberof StudentSummaryDto + */ + 'totalScore': number; + /** + * + * @type {Array} + * @memberof StudentSummaryDto + */ + 'results': Array; + /** + * + * @type {boolean} + * @memberof StudentSummaryDto + */ + 'isActive': boolean; + /** + * + * @type {MentorStudentSummaryDto} + * @memberof StudentSummaryDto + */ + 'mentor': MentorStudentSummaryDto | null; + /** + * + * @type {number} + * @memberof StudentSummaryDto + */ + 'rank': number; + /** + * + * @type {string} + * @memberof StudentSummaryDto + */ + 'repository': string | null; +} /** * * @export @@ -15260,18 +15388,18 @@ export const StudentsApiAxiosParamCreator = function (configuration?: Configurat /** * * @param {number} courseId - * @param {string} studentId + * @param {string} githubId * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getStudentSummary: async (courseId: number, studentId: string, options: AxiosRequestConfig = {}): Promise => { + getStudentSummary: async (courseId: number, githubId: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'courseId' is not null or undefined assertParamExists('getStudentSummary', 'courseId', courseId) - // verify required parameter 'studentId' is not null or undefined - assertParamExists('getStudentSummary', 'studentId', studentId) - const localVarPath = `/courses/{courseId}/students/{studentId}/summary` + // verify required parameter 'githubId' is not null or undefined + assertParamExists('getStudentSummary', 'githubId', githubId) + const localVarPath = `/courses/{courseId}/students/{githubId}/summary` .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))) - .replace(`{${"studentId"}}`, encodeURIComponent(String(studentId))); + .replace(`{${"githubId"}}`, encodeURIComponent(String(githubId))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -15317,12 +15445,12 @@ export const StudentsApiFp = function(configuration?: Configuration) { /** * * @param {number} courseId - * @param {string} studentId + * @param {string} githubId * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getStudentSummary(courseId: number, studentId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getStudentSummary(courseId, studentId, options); + async getStudentSummary(courseId: number, githubId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getStudentSummary(courseId, githubId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } @@ -15347,12 +15475,12 @@ export const StudentsApiFactory = function (configuration?: Configuration, baseP /** * * @param {number} courseId - * @param {string} studentId + * @param {string} githubId * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getStudentSummary(courseId: number, studentId: string, options?: any): AxiosPromise { - return localVarFp.getStudentSummary(courseId, studentId, options).then((request) => request(axios, basePath)); + getStudentSummary(courseId: number, githubId: string, options?: any): AxiosPromise { + return localVarFp.getStudentSummary(courseId, githubId, options).then((request) => request(axios, basePath)); }, }; }; @@ -15378,13 +15506,13 @@ export class StudentsApi extends BaseAPI { /** * * @param {number} courseId - * @param {string} studentId + * @param {string} githubId * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof StudentsApi */ - public getStudentSummary(courseId: number, studentId: string, options?: AxiosRequestConfig) { - return StudentsApiFp(this.configuration).getStudentSummary(courseId, studentId, options).then((request) => request(this.axios, this.basePath)); + public getStudentSummary(courseId: number, githubId: string, options?: AxiosRequestConfig) { + return StudentsApiFp(this.configuration).getStudentSummary(courseId, githubId, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/client/src/services/course.ts b/client/src/services/course.ts index c605a9aae6..63d6421f65 100644 --- a/client/src/services/course.ts +++ b/client/src/services/course.ts @@ -16,6 +16,7 @@ import { CriteriaDto, CrossCheckMessageDto, CrossCheckCriteriaDataDto, + StudentsApi } from 'api'; import { optionalQueryString } from 'utils/optionalQueryString'; import { Decision } from 'data/interviews/technical-screening'; @@ -140,6 +141,7 @@ export type SearchStudent = UserBasic & { mentor: UserBasic | null }; const courseTasksApi = new CoursesTasksApi(); const courseEventsApi = new CoursesEventsApi(); const studentsScoreApi = new StudentsScoreApi(); +const studentsApi = new StudentsApi(); export class CourseService { private axios: AxiosInstance; @@ -527,8 +529,8 @@ export class CourseService { } async getStudentSummary(githubId: string | 'me') { - const result = await this.axios.get(`/student/${githubId}/summary`); - return result.data.data as StudentSummary; + const result = await studentsApi.getStudentSummary(this.courseId, githubId) + return result.data as unknown as StudentSummary; } async getStudentScore(githubId: string) { diff --git a/nestjs/src/courses/course-students/course-students.controller.ts b/nestjs/src/courses/course-students/course-students.controller.ts index 5efe56a4a8..3e6f9d782b 100644 --- a/nestjs/src/courses/course-students/course-students.controller.ts +++ b/nestjs/src/courses/course-students/course-students.controller.ts @@ -1,22 +1,25 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; -import { ApiBadRequestResponse, ApiForbiddenResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiBadRequestResponse, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { CurrentRequest, DefaultGuard } from '../../auth'; -// import { StudentsService } from '../students'; import { StudentSummaryDto } from './dto/student-summary.dto'; +import { CourseStudentsService } from './course-students.service'; @Controller('courses/:courseId/students') @ApiTags('students') @UseGuards(DefaultGuard) export class CourseStudentsController { -// constructor(private studentsService: StudentsService) {} + constructor(private courseStudentService: CourseStudentsService) {} - @Get(':studentId/summary') + @Get(':githubId/summary') @ApiForbiddenResponse() @ApiBadRequestResponse() + @ApiOkResponse({ + type: StudentSummaryDto, + }) @ApiOperation({ operationId: 'getStudentSummary' }) public async getStudentSummary( @Param('courseId') courseId: number, - @Param('studentId') githubId: string, + @Param('githubId') githubId: string, @Req() req: CurrentRequest, ) { let studentGithubId; @@ -25,33 +28,21 @@ export class CourseStudentsController { } else { studentGithubId = githubId; } + + const student = await this.courseStudentService.getStudentByGithubId(courseId, studentGithubId); + + const [score, mentor] = await Promise.all([ + this.courseStudentService.getStudentScore(student?.id || 0), + student?.mentorId ? await this.courseStudentService.getMentorWithContacts(student.mentorId) : null, + ]); + return new StudentSummaryDto({ - totalScore: 0, - rank: 5, - results: [ - { - score: 50, - courseTaskId: 719, - }, - ], - isActive: true, - mentor: { - isActive: true, - name: 'dmitry romaniuk', - id: 1273, - githubId: studentGithubId, - students: [], - cityName: 'Minsk', - countryName: 'Belarus', - contactsEmail: 'hello@example.com', - contactsSkype: null, - contactsWhatsApp: null, - contactsTelegram: 'pavel_durov', - contactsNotes: 'do not call me', - contactsPhone: null, - }, - repository: null, - discord: 'eleven' + totalScore: score?.totalScore, + results: score?.results, + rank: score?.rank, + isActive: !student?.isExpelled && !student?.isFailed, + mentor, + repository: student?.repository || null, }); } } diff --git a/nestjs/src/courses/course-students/course-students.service.ts b/nestjs/src/courses/course-students/course-students.service.ts index 0e464d84bd..431f928577 100644 --- a/nestjs/src/courses/course-students/course-students.service.ts +++ b/nestjs/src/courses/course-students/course-students.service.ts @@ -1,17 +1,180 @@ -import { Student } from '@entities/student'; +import { User } from '@entities/user'; +import { MentorBasic, StageInterviewFeedbackJson } from '@common/models'; +import { StageInterview, StageInterviewFeedback, Mentor, Student } from '@entities/index'; + +import { TaskInterviewResult } from '@entities/taskInterviewResult'; +import { TaskResult } from '@entities/taskResult'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { Mentor as MentorWithContacts } from './dto/mentor-student-summary.dto'; @Injectable() export class CourseStudentsService { constructor( @InjectRepository(Student) readonly studentRepository: Repository, + + @InjectRepository(Mentor) + readonly mentorRepository: Repository, ) {} + studentQuery() { + return this.studentRepository.createQueryBuilder('student'); + } + + mentorQuery() { + return this.mentorRepository.createQueryBuilder('mentor'); + } + + async getStudentByGithubId(courseId: number, githubId: string): Promise { + const record = await this.studentQuery() + .innerJoin('student.user', 'user') + .where('user.githubId = :githubId', { githubId }) + .andWhere('student.courseId = :courseId', { courseId }) + .getOne(); + if (record == null) { + return null; + } + return record; + } public async getStudentSummary(courseId: number, studentId: number) { console.log('summary', { studentId, courseId }); } + public async getStudentScore(studentId: number) { + const student = await this.studentRepository + .createQueryBuilder('student') + .leftJoinAndSelect('student.taskResults', 'taskResults') + .leftJoin('taskResults.courseTask', 'courseTask') + .addSelect(['courseTask.disabled', 'courseTask.id']) + .leftJoinAndSelect('student.taskInterviewResults', 'taskInterviewResults') + .leftJoin('student.stageInterviews', 'si') + .leftJoin('si.stageInterviewFeedbacks', 'sif') + .addSelect([ + 'sif.stageInterviewId', + 'sif.json', + 'si.isCompleted', + 'si.id', + 'si.courseTaskId', + 'si.score', + 'sif.version', + ]) + .where('student.id = :studentId', { studentId }) + .getOne(); + + if (!student) return null; + + const { taskResults, taskInterviewResults, stageInterviews } = student; + + const toTaskScore = ({ courseTaskId, score = 0 }: TaskResult | TaskInterviewResult) => ({ courseTaskId, score }); + + const results = []; + + if (taskResults?.length) { + results.push(...(taskResults.filter(taskResult => !taskResult.courseTask.disabled).map(toTaskScore) ?? [])); + } + + if (taskInterviewResults?.length) { + results.push(...taskInterviewResults.map(toTaskScore)); + } + + // we have a case when technical screening score are set as task result. + if (stageInterviews?.length && !results.find(tr => tr.courseTaskId === stageInterviews[0]?.courseTaskId)) { + const feedbackVersion = stageInterviews[0]?.stageInterviewFeedbacks[0]?.version; + const score = !feedbackVersion + ? Math.floor(getStageInterviewRating(stageInterviews) ?? 0) + : stageInterviews[0]?.score; + + results.push({ + score, + courseTaskId: stageInterviews[0]?.courseTaskId, + }); + } + + return { + totalScore: student.totalScore, + rank: student.rank ?? 999999, + results, + }; + } + + async getMentorWithContacts(mentorId: number): Promise { + const record = (await this.mentorQuery() + .innerJoinAndSelect('mentor.user', 'user') + .where('mentor.id = :mentorId', { mentorId }) + .getOne())!; + const mentor = convertToMentorBasic(record); + const user = record.user as User; + const mentorWithContacts: MentorWithContacts = { + ...mentor, + contactsEmail: user.contactsEmail, + contactsSkype: user.contactsSkype, + contactsWhatsApp: user.contactsWhatsApp, + contactsTelegram: user.contactsTelegram, + contactsNotes: user.contactsNotes, + contactsPhone: null, + }; + return mentorWithContacts; + } +} + +const getStageInterviewRating = (stageInterviews: StageInterview[]) => { + const [lastInterview] = stageInterviews + .filter((interview: StageInterview) => interview.isCompleted) + .map(({ stageInterviewFeedbacks, score }: StageInterview) => + stageInterviewFeedbacks.map((feedback: StageInterviewFeedback) => ({ + date: feedback.updatedDate, + // interviews in new template should have score precalculated + rating: score ?? getInterviewRatings(JSON.parse(feedback.json) as StageInterviewFeedbackJson).rating, + })), + ) + .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; +}; + +export function 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[]; + + const htmlCss = skills?.htmlCss.level; + const common = commonSkills.reduce((acc, cur) => acc + cur, 0) / commonSkills.length; + const dataStructures = dataStructuresSkills.reduce((acc, cur) => acc + cur, 0) / dataStructuresSkills.length; + + if (resume?.score !== undefined) { + const rating = resume.score; + return { rating, htmlCss, common, dataStructures }; + } + + const ratingsCount = 4; + const ratings = [htmlCss, common, dataStructures, programmingTask.codeWritingLevel].filter(Boolean) as number[]; + const rating = (ratings.length === ratingsCount ? ratings.reduce((sum, num) => sum + num) / ratingsCount : 0) * 10; + + return { rating, htmlCss, common, dataStructures }; +} + +export function convertToMentorBasic(mentor: Mentor): MentorBasic { + const user = (mentor.user as User)!; + return { + isActive: !mentor.isExpelled, + name: createName(user), + id: mentor.id, + githubId: user.githubId, + students: mentor.students ? mentor.students.filter(s => !s.isExpelled && !s.isFailed).map(s => ({ id: s.id })) : [], + cityName: user.cityName ?? '', + countryName: user.countryName ?? '', + }; +} + +export function createName({ firstName, lastName }: { firstName: string; lastName: string }) { + const result = []; + if (firstName) { + result.push(firstName.trim()); + } + if (lastName) { + result.push(lastName.trim()); + } + return result.join(' '); } diff --git a/nestjs/src/courses/course-students/dto/student-summary.dto.ts b/nestjs/src/courses/course-students/dto/student-summary.dto.ts index b231c3571f..068f1ad570 100644 --- a/nestjs/src/courses/course-students/dto/student-summary.dto.ts +++ b/nestjs/src/courses/course-students/dto/student-summary.dto.ts @@ -2,37 +2,33 @@ import { ApiProperty } from '@nestjs/swagger'; import { Mentor, MentorStudentSummaryDto } from './mentor-student-summary.dto'; export interface StudentSummary { - totalScore: number; - results: { score: number; courseTaskId: number }[]; + totalScore?: number; + results?: { score?: number; courseTaskId?: number }[]; isActive: boolean; - discord: string; mentor: Mentor | null; - rank: number; + rank?: number; repository: string | null; } export class StudentSummaryDto { constructor(studentSummary: StudentSummary) { - this.totalScore = studentSummary.totalScore; - this.results = studentSummary.results; + this.totalScore = studentSummary.totalScore ?? 0; + this.results = studentSummary.results ?? []; this.isActive = studentSummary.isActive; - this.discord = studentSummary.discord; this.mentor = studentSummary.mentor; - this.rank = studentSummary.rank; + this.rank = studentSummary.rank ?? 999999; this.repository = studentSummary.repository; } @ApiProperty() totalScore: number; - @ApiProperty({ type: Array<{ score: number; courseTaskId: number }> }) - results: { score: number; courseTaskId: number }[]; + @ApiProperty({ type: Array<{ score?: number; courseTaskId?: number }> }) + results: { score?: number; courseTaskId?: number }[]; @ApiProperty() isActive: boolean; - @ApiProperty() - discord: string; @ApiProperty({ type: MentorStudentSummaryDto, nullable: true }) mentor: MentorStudentSummaryDto | null; @@ -40,6 +36,6 @@ export class StudentSummaryDto { @ApiProperty({ type: Number }) rank: number; - @ApiProperty({type: String, nullable: true}) + @ApiProperty({ type: String, nullable: true }) repository: string | null; } diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json index de7daf9efe..a67142ae4b 100644 --- a/nestjs/src/spec.json +++ b/nestjs/src/spec.json @@ -1400,15 +1400,22 @@ "tags": ["course mentors"] } }, - "/courses/{courseId}/students/{studentId}/summary": { + "/courses/{courseId}/students/{githubId}/summary": { "get": { "operationId": "getStudentSummary", "summary": "", "parameters": [ { "name": "courseId", "required": true, "in": "path", "schema": { "type": "number" } }, - { "name": "studentId", "required": true, "in": "path", "schema": { "type": "string" } } + { "name": "githubId", "required": true, "in": "path", "schema": { "type": "string" } } ], - "responses": { "400": { "description": "" }, "403": { "description": "" } }, + "responses": { + "200": { + "description": "", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StudentSummaryDto" } } } + }, + "400": { "description": "" }, + "403": { "description": "" } + }, "tags": ["students"] } }, @@ -3750,6 +3757,51 @@ "properties": { "id": { "type": "number" }, "githubId": { "type": "string" }, "name": { "type": "string" } }, "required": ["id", "githubId", "name"] }, + "MentorStudentSummaryDto": { + "type": "object", + "properties": { + "id": { "type": "number" }, + "githubId": { "type": "string" }, + "name": { "type": "string" }, + "isActive": { "type": "boolean" }, + "cityName": { "type": "string" }, + "countryName": { "type": "string" }, + "students": { "type": "array", "items": { "type": "string" } }, + "contactsEmail": { "type": "string", "nullable": true }, + "contactsPhone": { "type": "string", "nullable": true }, + "contactsSkype": { "type": "string", "nullable": true }, + "contactsTelegram": { "type": "string", "nullable": true }, + "contactsNotes": { "type": "string", "nullable": true }, + "contactsWhatsApp": { "type": "string", "nullable": true } + }, + "required": [ + "id", + "githubId", + "name", + "isActive", + "cityName", + "countryName", + "students", + "contactsEmail", + "contactsPhone", + "contactsSkype", + "contactsTelegram", + "contactsNotes", + "contactsWhatsApp" + ] + }, + "StudentSummaryDto": { + "type": "object", + "properties": { + "totalScore": { "type": "number" }, + "results": { "type": "array", "items": { "type": "string" } }, + "isActive": { "type": "boolean" }, + "mentor": { "nullable": true, "allOf": [{ "$ref": "#/components/schemas/MentorStudentSummaryDto" }] }, + "rank": { "type": "number" }, + "repository": { "type": "string", "nullable": true } + }, + "required": ["totalScore", "results", "isActive", "mentor", "rank", "repository"] + }, "Map": { "type": "object", "properties": {} }, "NotificationUserSettingsDto": { "type": "object", diff --git a/server/src/routes/course/student.ts b/server/src/routes/course/student.ts index aad2dd0c94..6999d373ad 100644 --- a/server/src/routes/course/student.ts +++ b/server/src/routes/course/student.ts @@ -79,7 +79,6 @@ export const postFeedback = (_: ILogger) => async (ctx: Router.RouterContext) => export const getStudentSummary = (_: ILogger) => async (ctx: Router.RouterContext) => { const { courseId, githubId } = ctx.params; - console.log(ctx.params) const student = await courseService.getStudentByGithubId(courseId, githubId); if (student == null) { setResponse(ctx, OK, null); From a749fafacf7d97817058936e6e8cba86edbee630 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Sun, 28 Apr 2024 14:58:50 +0300 Subject: [PATCH 03/26] refactor: fix eslint issue --- nestjs/src/courses/course-students/dto/student-summary.dto.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/nestjs/src/courses/course-students/dto/student-summary.dto.ts b/nestjs/src/courses/course-students/dto/student-summary.dto.ts index 068f1ad570..4f928421f1 100644 --- a/nestjs/src/courses/course-students/dto/student-summary.dto.ts +++ b/nestjs/src/courses/course-students/dto/student-summary.dto.ts @@ -29,7 +29,6 @@ export class StudentSummaryDto { @ApiProperty() isActive: boolean; - @ApiProperty({ type: MentorStudentSummaryDto, nullable: true }) mentor: MentorStudentSummaryDto | null; From 3c9df1c8b2a43a4bc6b04e4c7497f41327e24e85 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Mon, 29 Apr 2024 20:55:52 +0300 Subject: [PATCH 04/26] fix: fixed incorrect display of technical screening results on student dashboard page --- nestjs/src/courses/course-schedule/course-schedule.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nestjs/src/courses/course-schedule/course-schedule.service.ts b/nestjs/src/courses/course-schedule/course-schedule.service.ts index 316d250146..254ee3ae62 100644 --- a/nestjs/src/courses/course-schedule/course-schedule.service.ts +++ b/nestjs/src/courses/course-schedule/course-schedule.service.ts @@ -304,7 +304,7 @@ export class CourseScheduleService { ...(technicalScreeningResults .find(task => task.courseTaskId === courseTaskId) ?.stageInterviewFeedbacks.map(feedback => JSON.parse(feedback.json)) - .map((json: any) => json?.resume?.score ?? 0) ?? []), + .map((json: any) => (json?.resume?.score || json?.steps?.decision?.values?.finalScore) ?? 0) ?? []), ); const currentScore = isFinite(scoreRaw) ? scoreRaw : null; return currentScore; From 75ddc973c9183eb6eaef255194f11da473a100b5 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Tue, 30 Apr 2024 18:51:29 +0300 Subject: [PATCH 05/26] refactor: remove unused code --- server/src/routes/course/index.ts | 2 -- server/src/routes/course/student.ts | 22 ---------------------- 2 files changed, 24 deletions(-) diff --git a/server/src/routes/course/index.ts b/server/src/routes/course/index.ts index c86fed7ba9..5253dce91d 100644 --- a/server/src/routes/course/index.ts +++ b/server/src/routes/course/index.ts @@ -59,7 +59,6 @@ import { createInterviewResult, getCrossMentors, getStudent, - getStudentSummary, postFeedback, selfUpdateStudentStatus, updateMentoringAvailability, @@ -205,7 +204,6 @@ function addStudentApi(router: Router, logger: ILogger) { interviews.createInterviewStudent(logger), ); - router.get('/student/:githubId/summary', courseGuard, ...validators, getStudentSummary(logger)); router.post('/student/:githubId/availability', courseManagerGuard, updateMentoringAvailability(logger)); router.get('/student/:githubId/tasks/cross-mentors', courseGuard, ...validators, getCrossMentors(logger)); router.get('/student/:githubId/tasks/verifications', courseGuard, ...validators, getStudentTaskVerifications(logger)); diff --git a/server/src/routes/course/student.ts b/server/src/routes/course/student.ts index 6999d373ad..f5c0c2d77e 100644 --- a/server/src/routes/course/student.ts +++ b/server/src/routes/course/student.ts @@ -76,28 +76,6 @@ export const postFeedback = (_: ILogger) => async (ctx: Router.RouterContext) => return; }; -export const getStudentSummary = (_: ILogger) => async (ctx: Router.RouterContext) => { - const { courseId, githubId } = ctx.params; - - const student = await courseService.getStudentByGithubId(courseId, githubId); - if (student == null) { - setResponse(ctx, OK, null); - return; - } - - const [score, mentor] = await Promise.all([ - courseService.getStudentScore(student.id), - student.mentorId ? await courseService.getMentorWithContacts(student.mentorId) : null, - ]); - - setResponse(ctx, OK, { - ...score, - isActive: !student.isExpelled && !student.isFailed, - mentor, - repository: student.repository, - }); -}; - export const updateStudent = (_: ILogger) => async (ctx: Router.RouterContext) => { const { courseId, githubId } = ctx.params; const student = await courseService.queryStudentByGithubId(courseId, githubId); From 7646422d88cfb50938db9e226040b11205388e57 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Tue, 30 Apr 2024 20:46:41 +0300 Subject: [PATCH 06/26] refactor: remove redundant code --- client/src/modules/Home/pages/HomePage/index.tsx | 1 - nestjs/src/courses/course-students/course-students.service.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/client/src/modules/Home/pages/HomePage/index.tsx b/client/src/modules/Home/pages/HomePage/index.tsx index d862640007..8ddff9fedf 100644 --- a/client/src/modules/Home/pages/HomePage/index.tsx +++ b/client/src/modules/Home/pages/HomePage/index.tsx @@ -69,7 +69,6 @@ export function HomePage() { }; const { courseTasks, studentSummary } = useStudentSummary(session, course); - console.log(studentSummary) return ( diff --git a/nestjs/src/courses/course-students/course-students.service.ts b/nestjs/src/courses/course-students/course-students.service.ts index 431f928577..9235380174 100644 --- a/nestjs/src/courses/course-students/course-students.service.ts +++ b/nestjs/src/courses/course-students/course-students.service.ts @@ -39,9 +39,6 @@ export class CourseStudentsService { return record; } - public async getStudentSummary(courseId: number, studentId: number) { - console.log('summary', { studentId, courseId }); - } public async getStudentScore(studentId: number) { const student = await this.studentRepository .createQueryBuilder('student') From 3de8682e6fb14e370d26751f593a6762f1745a57 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Tue, 30 Apr 2024 21:02:06 +0300 Subject: [PATCH 07/26] refactor: fix linter issue --- client/src/services/course.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/services/course.ts b/client/src/services/course.ts index 63d6421f65..b2181e8e89 100644 --- a/client/src/services/course.ts +++ b/client/src/services/course.ts @@ -16,7 +16,7 @@ import { CriteriaDto, CrossCheckMessageDto, CrossCheckCriteriaDataDto, - StudentsApi + StudentsApi, } from 'api'; import { optionalQueryString } from 'utils/optionalQueryString'; import { Decision } from 'data/interviews/technical-screening'; @@ -529,7 +529,7 @@ export class CourseService { } async getStudentSummary(githubId: string | 'me') { - const result = await studentsApi.getStudentSummary(this.courseId, githubId) + const result = await studentsApi.getStudentSummary(this.courseId, githubId); return result.data as unknown as StudentSummary; } From ee771488c51d7031d0b847de7aeedb1d963798db Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Wed, 1 May 2024 14:24:14 +0300 Subject: [PATCH 08/26] fix: fix student summary api response type --- client/src/api/api.ts | 23 +++++++++++++++++-- client/src/services/course.ts | 3 +-- .../courses/course-students/dto/result.dto.ts | 9 ++++++++ .../dto/student-summary.dto.ts | 7 +++--- nestjs/src/spec.json | 6 ++++- 5 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 nestjs/src/courses/course-students/dto/result.dto.ts diff --git a/client/src/api/api.ts b/client/src/api/api.ts index f2521f89c4..14af3b57b0 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -4429,6 +4429,25 @@ export interface PutInterviewFeedbackDto { */ 'score'?: number; } +/** + * + * @export + * @interface ResultDto + */ +export interface ResultDto { + /** + * + * @type {number} + * @memberof ResultDto + */ + 'score'?: number; + /** + * + * @type {number} + * @memberof ResultDto + */ + 'courseTaskId'?: number; +} /** * * @export @@ -5224,10 +5243,10 @@ export interface StudentSummaryDto { 'totalScore': number; /** * - * @type {Array} + * @type {Array} * @memberof StudentSummaryDto */ - 'results': Array; + 'results': Array; /** * * @type {boolean} diff --git a/client/src/services/course.ts b/client/src/services/course.ts index b2181e8e89..923b94d2b1 100644 --- a/client/src/services/course.ts +++ b/client/src/services/course.ts @@ -530,7 +530,7 @@ export class CourseService { async getStudentSummary(githubId: string | 'me') { const result = await studentsApi.getStudentSummary(this.courseId, githubId); - return result.data as unknown as StudentSummary; + return result.data as StudentSummary; } async getStudentScore(githubId: string) { @@ -660,7 +660,6 @@ export interface StudentSummary { totalScore: number; results: { score: number; courseTaskId: number }[]; isActive: boolean; - discord: string; mentor: | (MentorBasic & { contactsEmail?: string; diff --git a/nestjs/src/courses/course-students/dto/result.dto.ts b/nestjs/src/courses/course-students/dto/result.dto.ts new file mode 100644 index 0000000000..14754112d4 --- /dev/null +++ b/nestjs/src/courses/course-students/dto/result.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ResultDto { + @ApiProperty({ type: Number, required: false }) + score?: number; + + @ApiProperty({ type: Number, required: false }) + courseTaskId?: number; +} diff --git a/nestjs/src/courses/course-students/dto/student-summary.dto.ts b/nestjs/src/courses/course-students/dto/student-summary.dto.ts index 4f928421f1..63c96e9c9b 100644 --- a/nestjs/src/courses/course-students/dto/student-summary.dto.ts +++ b/nestjs/src/courses/course-students/dto/student-summary.dto.ts @@ -1,9 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; import { Mentor, MentorStudentSummaryDto } from './mentor-student-summary.dto'; +import { ResultDto } from './result.dto'; export interface StudentSummary { totalScore?: number; - results?: { score?: number; courseTaskId?: number }[]; + results?: Array; isActive: boolean; mentor: Mentor | null; rank?: number; @@ -23,8 +24,8 @@ export class StudentSummaryDto { @ApiProperty() totalScore: number; - @ApiProperty({ type: Array<{ score?: number; courseTaskId?: number }> }) - results: { score?: number; courseTaskId?: number }[]; + @ApiProperty({ type: ResultDto, isArray: true }) + results: Array; @ApiProperty() isActive: boolean; diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json index 04e272c92b..3d03b512db 100644 --- a/nestjs/src/spec.json +++ b/nestjs/src/spec.json @@ -3811,6 +3811,10 @@ "properties": { "id": { "type": "number" }, "githubId": { "type": "string" }, "name": { "type": "string" } }, "required": ["id", "githubId", "name"] }, + "ResultDto": { + "type": "object", + "properties": { "score": { "type": "number" }, "courseTaskId": { "type": "number" } } + }, "MentorStudentSummaryDto": { "type": "object", "properties": { @@ -3848,7 +3852,7 @@ "type": "object", "properties": { "totalScore": { "type": "number" }, - "results": { "type": "array", "items": { "type": "string" } }, + "results": { "type": "array", "items": { "$ref": "#/components/schemas/ResultDto" } }, "isActive": { "type": "boolean" }, "mentor": { "nullable": true, "allOf": [{ "$ref": "#/components/schemas/MentorStudentSummaryDto" }] }, "rank": { "type": "number" }, From 8822c46051a63e67cff05b010e46f97199de93cc Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Wed, 1 May 2024 14:26:20 +0300 Subject: [PATCH 09/26] fix: throw not found error when student is not found --- .../courses/course-students/course-students.controller.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nestjs/src/courses/course-students/course-students.controller.ts b/nestjs/src/courses/course-students/course-students.controller.ts index 3e6f9d782b..256d136244 100644 --- a/nestjs/src/courses/course-students/course-students.controller.ts +++ b/nestjs/src/courses/course-students/course-students.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; +import { Controller, Get, NotFoundException, Param, Req, UseGuards } from '@nestjs/common'; import { ApiBadRequestResponse, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { CurrentRequest, DefaultGuard } from '../../auth'; import { StudentSummaryDto } from './dto/student-summary.dto'; @@ -31,8 +31,11 @@ export class CourseStudentsController { const student = await this.courseStudentService.getStudentByGithubId(courseId, studentGithubId); + if (student === null) { + throw new NotFoundException(`Student with GitHub id ${studentGithubId} not found`); + } const [score, mentor] = await Promise.all([ - this.courseStudentService.getStudentScore(student?.id || 0), + this.courseStudentService.getStudentScore(student?.id), student?.mentorId ? await this.courseStudentService.getMentorWithContacts(student.mentorId) : null, ]); From 80b412880bde0f6d1f29381ee0755f8a2212e2d2 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Wed, 1 May 2024 14:30:53 +0300 Subject: [PATCH 10/26] fix: reactor if-else to ternary operation --- .../courses/course-students/course-students.controller.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/nestjs/src/courses/course-students/course-students.controller.ts b/nestjs/src/courses/course-students/course-students.controller.ts index 256d136244..f1024038cc 100644 --- a/nestjs/src/courses/course-students/course-students.controller.ts +++ b/nestjs/src/courses/course-students/course-students.controller.ts @@ -22,12 +22,7 @@ export class CourseStudentsController { @Param('githubId') githubId: string, @Req() req: CurrentRequest, ) { - let studentGithubId; - if (githubId === 'me') { - studentGithubId = req.user.githubId; - } else { - studentGithubId = githubId; - } + const studentGithubId = githubId === 'me' ? req.user.githubId : githubId; const student = await this.courseStudentService.getStudentByGithubId(courseId, studentGithubId); From 5fe595d9659a2cfc5bf8371e1d9ec0fc7bc0d8ab Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Mon, 6 May 2024 20:34:12 +0300 Subject: [PATCH 11/26] refactor: rewrite database queries --- .../course-students.service.ts | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/nestjs/src/courses/course-students/course-students.service.ts b/nestjs/src/courses/course-students/course-students.service.ts index 9235380174..ed2f7a70bc 100644 --- a/nestjs/src/courses/course-students/course-students.service.ts +++ b/nestjs/src/courses/course-students/course-students.service.ts @@ -4,7 +4,7 @@ import { StageInterview, StageInterviewFeedback, Mentor, Student } from '@entiti import { TaskInterviewResult } from '@entities/taskInterviewResult'; import { TaskResult } from '@entities/taskResult'; -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Mentor as MentorWithContacts } from './dto/mentor-student-summary.dto'; @@ -19,20 +19,15 @@ export class CourseStudentsService { readonly mentorRepository: Repository, ) {} - studentQuery() { - return this.studentRepository.createQueryBuilder('student'); - } - - mentorQuery() { - return this.mentorRepository.createQueryBuilder('mentor'); - } - async getStudentByGithubId(courseId: number, githubId: string): Promise { - const record = await this.studentQuery() - .innerJoin('student.user', 'user') - .where('user.githubId = :githubId', { githubId }) - .andWhere('student.courseId = :courseId', { courseId }) - .getOne(); + const record = await this.studentRepository.findOne({ + where: { + courseId, + user: { githubId }, + }, + relations: ['user'], + }); + if (record == null) { return null; } @@ -97,10 +92,17 @@ export class CourseStudentsService { } async getMentorWithContacts(mentorId: number): Promise { - const record = (await this.mentorQuery() - .innerJoinAndSelect('mentor.user', 'user') - .where('mentor.id = :mentorId', { mentorId }) - .getOne())!; + const record = await this.mentorRepository.findOne({ + relations: ['user'], + where: { + id: mentorId, + }, + }); + + if (!record) { + throw new NotFoundException(`Mentor not found ${mentorId}`); + } + const mentor = convertToMentorBasic(record); const user = record.user as User; const mentorWithContacts: MentorWithContacts = { From d39852a0fddf1b3511d0bbe6d2e1b14cb4141839 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Mon, 6 May 2024 20:50:18 +0300 Subject: [PATCH 12/26] refactor: make function getCurrentTaskScore more readable --- .../src/courses/course-schedule/course-schedule.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nestjs/src/courses/course-schedule/course-schedule.service.ts b/nestjs/src/courses/course-schedule/course-schedule.service.ts index 254ee3ae62..bedd106d29 100644 --- a/nestjs/src/courses/course-schedule/course-schedule.service.ts +++ b/nestjs/src/courses/course-schedule/course-schedule.service.ts @@ -304,7 +304,11 @@ export class CourseScheduleService { ...(technicalScreeningResults .find(task => task.courseTaskId === courseTaskId) ?.stageInterviewFeedbacks.map(feedback => JSON.parse(feedback.json)) - .map((json: any) => (json?.resume?.score || json?.steps?.decision?.values?.finalScore) ?? 0) ?? []), + .map((json: any) => { + const resumeScore = json?.resume?.score; + const decisionScore = json?.steps?.decision?.values?.finalScore; + return resumeScore ?? decisionScore ?? 0; + }) ?? []), ); const currentScore = isFinite(scoreRaw) ? scoreRaw : null; return currentScore; From 22e570007d6d2afa0a6b07bfa1874d1f5a7f1326 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Mon, 6 May 2024 21:02:34 +0300 Subject: [PATCH 13/26] refactor:refactor getStudentSummary function return statement --- .../src/courses/course-students/course-students.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nestjs/src/courses/course-students/course-students.controller.ts b/nestjs/src/courses/course-students/course-students.controller.ts index f1024038cc..29188393b6 100644 --- a/nestjs/src/courses/course-students/course-students.controller.ts +++ b/nestjs/src/courses/course-students/course-students.controller.ts @@ -40,7 +40,7 @@ export class CourseStudentsController { rank: score?.rank, isActive: !student?.isExpelled && !student?.isFailed, mentor, - repository: student?.repository || null, + repository: student?.repository ?? null, }); } } From a1f731b785b3cb6a1dbeb32fb43c757c857bde40 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Wed, 8 May 2024 21:21:34 +0300 Subject: [PATCH 14/26] refactor: update course-student.service.ts imports not to use files from commons --- .../course-students.service.ts | 2 +- .../courses/course-students/types/discord.ts | 5 ++ .../courses/course-students/types/index.ts | 6 ++ .../course-students/types/mentor-basic.ts | 9 +++ .../types/stage-interview-feedback-json.ts | 58 +++++++++++++++++++ .../course-students/types/student-basic.ts | 21 +++++++ .../course-students/types/user-basic.ts | 5 ++ 7 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 nestjs/src/courses/course-students/types/discord.ts create mode 100644 nestjs/src/courses/course-students/types/index.ts create mode 100644 nestjs/src/courses/course-students/types/mentor-basic.ts create mode 100644 nestjs/src/courses/course-students/types/stage-interview-feedback-json.ts create mode 100644 nestjs/src/courses/course-students/types/student-basic.ts create mode 100644 nestjs/src/courses/course-students/types/user-basic.ts diff --git a/nestjs/src/courses/course-students/course-students.service.ts b/nestjs/src/courses/course-students/course-students.service.ts index ed2f7a70bc..3cb57a8376 100644 --- a/nestjs/src/courses/course-students/course-students.service.ts +++ b/nestjs/src/courses/course-students/course-students.service.ts @@ -1,5 +1,4 @@ import { User } from '@entities/user'; -import { MentorBasic, StageInterviewFeedbackJson } from '@common/models'; import { StageInterview, StageInterviewFeedback, Mentor, Student } from '@entities/index'; import { TaskInterviewResult } from '@entities/taskInterviewResult'; @@ -8,6 +7,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Mentor as MentorWithContacts } from './dto/mentor-student-summary.dto'; +import { MentorBasic, StageInterviewFeedbackJson } from './types'; @Injectable() export class CourseStudentsService { diff --git a/nestjs/src/courses/course-students/types/discord.ts b/nestjs/src/courses/course-students/types/discord.ts new file mode 100644 index 0000000000..a6f0e6c495 --- /dev/null +++ b/nestjs/src/courses/course-students/types/discord.ts @@ -0,0 +1,5 @@ +export default interface Discord { + id: string; + username: string; + discriminator: string; +} diff --git a/nestjs/src/courses/course-students/types/index.ts b/nestjs/src/courses/course-students/types/index.ts new file mode 100644 index 0000000000..eeeb38c55b --- /dev/null +++ b/nestjs/src/courses/course-students/types/index.ts @@ -0,0 +1,6 @@ +import Discord from './discord'; +import MentorBasic from './mentor-basic'; +import StudentBasic from './student-basic'; +import UserBasic from './user-basic'; +import { StageInterviewFeedbackJson } from './stage-interview-feedback-json'; +export { Discord, MentorBasic, StudentBasic, UserBasic, StageInterviewFeedbackJson }; diff --git a/nestjs/src/courses/course-students/types/mentor-basic.ts b/nestjs/src/courses/course-students/types/mentor-basic.ts new file mode 100644 index 0000000000..d12eef4636 --- /dev/null +++ b/nestjs/src/courses/course-students/types/mentor-basic.ts @@ -0,0 +1,9 @@ +import StudentBasic from './student-basic'; +import UserBasic from './user-basic'; + +export default interface MentorBasic extends UserBasic { + isActive: boolean; + cityName: string; + countryName: string; + students: (StudentBasic | { id: number })[]; +} diff --git a/nestjs/src/courses/course-students/types/stage-interview-feedback-json.ts b/nestjs/src/courses/course-students/types/stage-interview-feedback-json.ts new file mode 100644 index 0000000000..47d2b47dca --- /dev/null +++ b/nestjs/src/courses/course-students/types/stage-interview-feedback-json.ts @@ -0,0 +1,58 @@ +interface StageInterviewFeedback { + common: { + reason: 'haveITEducation' | 'doNotWorkInIT' | 'whatThisCourseAbout' | 'other' | null; + reasonOther: string | null; + whenStartCoding: number | null; + schoolChallengesParticipaton: string | null; + whereStudied: string | null; + workExperience: string | null; + otherAchievements: string | null; + militaryService: 'served' | 'liable' | 'notLiable' | null; + }; + skills?: { + [index: string]: any; + htmlCss: { + level: number | null; + }; + dataStructures: { + array: number | null; + list: number | null; + stack: number | null; + queue: number | null; + tree: number | null; + hashTable: number | null; + heap: number | null; + }; + common: { + binaryNumber: number | null; + oop: number | null; + bigONotation: number | null; + sortingAndSearchAlgorithms: number | null; + }; + comment: string | null; + }; + programmingTask: { + task: string | null; + codeWritingLevel: number | null; + resolved: number | null; + comment: string | null; + }; + resume: { + verdict: StageInterviewFeedbackVerdict; + comment: string | null; + score: number; + }; +} + +export type StageInterviewFeedbackVerdict = 'yes' | 'no' | 'noButGoodCandidate' | 'didNotDecideYet' | null; + +export type EnglishLevel = 'a0' | 'a1' | 'a1+' | 'a2' | 'a2+' | 'b1' | 'b1+' | 'b2' | 'b2+' | 'c1' | 'c1+' | 'c2'; + +export interface StageInterviewFeedbackJson extends StageInterviewFeedback { + english: { + levelStudentOpinion: EnglishLevel | null; + levelMentorOpinion: EnglishLevel | null; + whereAndWhenLearned: string | null; + comment: string | null; + }; +} diff --git a/nestjs/src/courses/course-students/types/student-basic.ts b/nestjs/src/courses/course-students/types/student-basic.ts new file mode 100644 index 0000000000..2089173f5e --- /dev/null +++ b/nestjs/src/courses/course-students/types/student-basic.ts @@ -0,0 +1,21 @@ +import Discord from './discord'; +import MentorBasic from './mentor-basic'; +import UserBasic from './user-basic'; + +export default interface StudentBasic extends UserBasic { + isActive: boolean; + cityName?: string | null; + countryName?: string | null; + mentor: + | MentorBasic + | { id: number } + | { + id: number; + githubId: string; + name: string; + } + | null; + discord: Discord | null; + totalScore: number; + rank?: number; +} diff --git a/nestjs/src/courses/course-students/types/user-basic.ts b/nestjs/src/courses/course-students/types/user-basic.ts new file mode 100644 index 0000000000..84ee15435a --- /dev/null +++ b/nestjs/src/courses/course-students/types/user-basic.ts @@ -0,0 +1,5 @@ +export default interface UserBasic { + id: number; + githubId: string; + name: string; +} From 53f70bbf3d0f21743064640c295a3ea2b1760df6 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Sun, 9 Jun 2024 15:27:50 +0300 Subject: [PATCH 15/26] refactor: use StudentSummaryDto instead of (MentorBasic & MentorContact) --- .../StudentDashboard/components/MentorCard/MentorCard.tsx | 6 +++--- .../StudentDashboard/components/MentorInfo/MentorInfo.tsx | 8 +++----- client/src/pages/course/student/dashboard.tsx | 5 +++-- client/src/services/course.ts | 3 ++- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/client/src/modules/StudentDashboard/components/MentorCard/MentorCard.tsx b/client/src/modules/StudentDashboard/components/MentorCard/MentorCard.tsx index 2f00b09416..614070401d 100644 --- a/client/src/modules/StudentDashboard/components/MentorCard/MentorCard.tsx +++ b/client/src/modules/StudentDashboard/components/MentorCard/MentorCard.tsx @@ -1,11 +1,11 @@ import { Col, Typography, Row } from 'antd'; -import { MentorBasic } from 'common/models'; import CommonCard from '../CommonDashboardCard'; -import { MentorContact, MentorInfo } from '../MentorInfo'; +import { MentorInfo } from '../MentorInfo'; import { SubmitTaskSolution } from '../SubmitTaskSolution'; +import { MentorStudentSummaryDto } from 'api'; export type MentorCardProps = { - mentor?: (MentorBasic & MentorContact) | null; + mentor?: MentorStudentSummaryDto | null; courseId: number; }; diff --git a/client/src/modules/StudentDashboard/components/MentorInfo/MentorInfo.tsx b/client/src/modules/StudentDashboard/components/MentorInfo/MentorInfo.tsx index ec8e218fef..3cd8f93523 100644 --- a/client/src/modules/StudentDashboard/components/MentorInfo/MentorInfo.tsx +++ b/client/src/modules/StudentDashboard/components/MentorInfo/MentorInfo.tsx @@ -1,9 +1,9 @@ import { Col, Row, Space, Typography } from 'antd'; import React from 'react'; -import { MentorBasic } from 'common/models'; import GithubFilled from '@ant-design/icons/GithubFilled'; import EnvironmentFilled from '@ant-design/icons/EnvironmentFilled'; import { GithubAvatar } from 'components/GithubAvatar'; +import { MentorStudentSummaryDto } from 'api'; const { Text, Link } = Typography; @@ -15,10 +15,8 @@ export interface MentorContact { contactsNotes?: string; } -type Contact = { name: string; value: string | undefined }; - interface Props { - mentor: MentorBasic & MentorContact; + mentor: MentorStudentSummaryDto } function MentorInfo({ mentor }: Props) { @@ -42,7 +40,7 @@ function MentorInfo({ mentor }: Props) { { name: 'Notes', value: contactsNotes }, ]; - const filledContacts = contacts.filter(({ value }: Contact) => value); + const filledContacts = contacts.filter(({ value }) => value); return (
diff --git a/client/src/pages/course/student/dashboard.tsx b/client/src/pages/course/student/dashboard.tsx index 1e117f6dd7..6e8867848a 100644 --- a/client/src/pages/course/student/dashboard.tsx +++ b/client/src/pages/course/student/dashboard.tsx @@ -8,7 +8,7 @@ import omitBy from 'lodash/omitBy'; import { LoadingScreen } from 'components/LoadingScreen'; import { PageLayout } from 'components/PageLayout'; -import { CourseService, StudentSummary } from 'services/course'; +import { CourseService } from 'services/course'; import { UserService } from 'services/user'; import { MainStatsCard, @@ -29,6 +29,7 @@ import { CourseScheduleItemDtoStatusEnum, AvailableReviewStatsDto, CourseScheduleItemDtoTypeEnum, + StudentSummaryDto, } from 'api'; import { ActiveCourseProvider, SessionContext, SessionProvider, useActiveCourseContext } from 'modules/Course/contexts'; @@ -44,7 +45,7 @@ function Page() { const courseService = useMemo(() => new CourseService(course.id), [course.id]); const userService = useMemo(() => new UserService(), []); - const [studentSummary, setStudentSummary] = useState({} as StudentSummary); + const [studentSummary, setStudentSummary] = useState(); const [repositoryUrl, setRepositoryUrl] = useState(''); const [courseTasks, setCourseTasks] = useState([]); const [nextEvents, setNextEvent] = useState([] as CourseScheduleItemDto[]); diff --git a/client/src/services/course.ts b/client/src/services/course.ts index 923b94d2b1..7bff948cd1 100644 --- a/client/src/services/course.ts +++ b/client/src/services/course.ts @@ -17,6 +17,7 @@ import { CrossCheckMessageDto, CrossCheckCriteriaDataDto, StudentsApi, + StudentSummaryDto, } from 'api'; import { optionalQueryString } from 'utils/optionalQueryString'; import { Decision } from 'data/interviews/technical-screening'; @@ -530,7 +531,7 @@ export class CourseService { async getStudentSummary(githubId: string | 'me') { const result = await studentsApi.getStudentSummary(this.courseId, githubId); - return result.data as StudentSummary; + return result.data as StudentSummaryDto; } async getStudentScore(githubId: string) { From 58a88ecdbace578ddc7e6ec9aa1e496d8fbd34d5 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Sun, 9 Jun 2024 15:47:42 +0300 Subject: [PATCH 16/26] reafactor: remove unused code --- .../course-students/course-students.controller.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/nestjs/src/courses/course-students/course-students.controller.ts b/nestjs/src/courses/course-students/course-students.controller.ts index 29188393b6..15599d124b 100644 --- a/nestjs/src/courses/course-students/course-students.controller.ts +++ b/nestjs/src/courses/course-students/course-students.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, NotFoundException, Param, Req, UseGuards } from '@nestjs/common'; import { ApiBadRequestResponse, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { CurrentRequest, DefaultGuard } from '../../auth'; +import { DefaultGuard } from '../../auth'; import { StudentSummaryDto } from './dto/student-summary.dto'; import { CourseStudentsService } from './course-students.service'; @@ -17,17 +17,11 @@ export class CourseStudentsController { type: StudentSummaryDto, }) @ApiOperation({ operationId: 'getStudentSummary' }) - public async getStudentSummary( - @Param('courseId') courseId: number, - @Param('githubId') githubId: string, - @Req() req: CurrentRequest, - ) { - const studentGithubId = githubId === 'me' ? req.user.githubId : githubId; - - const student = await this.courseStudentService.getStudentByGithubId(courseId, studentGithubId); + public async getStudentSummary(@Param('courseId') courseId: number, @Param('githubId') githubId: string) { + const student = await this.courseStudentService.getStudentByGithubId(courseId, githubId); if (student === null) { - throw new NotFoundException(`Student with GitHub id ${studentGithubId} not found`); + throw new NotFoundException(`Student with GitHub id ${githubId} not found`); } const [score, mentor] = await Promise.all([ this.courseStudentService.getStudentScore(student?.id), From c03b0ad6d92414f2fb183227d07b7c96b4819521 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Sun, 9 Jun 2024 15:52:35 +0300 Subject: [PATCH 17/26] refactor: avoid using 'me' to load student summary data --- client/src/modules/Home/components/HomeSummary/index.tsx | 6 +++--- client/src/modules/Home/data/loadHomeData.ts | 4 ++-- client/src/modules/Home/hooks/useStudentSummary.tsx | 6 +++--- client/src/services/course.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/src/modules/Home/components/HomeSummary/index.tsx b/client/src/modules/Home/components/HomeSummary/index.tsx index 7c615287ab..c2117ccce7 100644 --- a/client/src/modules/Home/components/HomeSummary/index.tsx +++ b/client/src/modules/Home/components/HomeSummary/index.tsx @@ -1,10 +1,10 @@ import { Card, Col, Row, Statistic, Typography } from 'antd'; +import { StudentSummaryDto } from 'api'; import { GithubUserLink } from 'components/GithubUserLink'; import * as React from 'react'; -import { StudentSummary } from 'services/course'; type Props = { - summary: StudentSummary; + summary: StudentSummaryDto; courseTasks: { id: number }[]; }; @@ -19,7 +19,7 @@ export function HomeSummary({ summary, courseTasks }: Props) { contactsNotes, contactsWhatsApp, } = summary.mentor ?? {}; - const tasksCount = summary.results.filter(r => r.score > 0).length; + const tasksCount = summary.results.filter(r => Number(r.score) > 0).length; const totalTaskCount = courseTasks.length; const contacts = [ diff --git a/client/src/modules/Home/data/loadHomeData.ts b/client/src/modules/Home/data/loadHomeData.ts index 93816df9ec..8dfc8dff16 100644 --- a/client/src/modules/Home/data/loadHomeData.ts +++ b/client/src/modules/Home/data/loadHomeData.ts @@ -1,9 +1,9 @@ import { CoursesTasksApi } from 'api'; import { CourseService } from 'services/course'; -export async function loadHomeData(courseId: number) { +export async function loadHomeData(courseId: number, githubId: string) { const [studentSummary, { data: courseTasks }] = await Promise.all([ - new CourseService(courseId).getStudentSummary('me'), + new CourseService(courseId).getStudentSummary(githubId), new CoursesTasksApi().getCourseTasks(courseId), ]); return { diff --git a/client/src/modules/Home/hooks/useStudentSummary.tsx b/client/src/modules/Home/hooks/useStudentSummary.tsx index 428e58375c..47ab4e909b 100644 --- a/client/src/modules/Home/hooks/useStudentSummary.tsx +++ b/client/src/modules/Home/hooks/useStudentSummary.tsx @@ -1,18 +1,18 @@ +import { StudentSummaryDto } from 'api'; import { Session } from 'components/withSession'; import { isStudent } from 'domain/user'; import { loadHomeData } from 'modules/Home/data/loadHomeData'; import { useState } from 'react'; import { useAsync } from 'react-use'; -import { StudentSummary } from 'services/course'; import { Course } from 'services/models'; export function useStudentSummary(session: Session, course: Course | null) { - const [studentSummary, setStudentSummary] = useState(null); + const [studentSummary, setStudentSummary] = useState(null); const [courseTasks, setCourseTasks] = useState<{ id: number }[]>([]); useAsync(async () => { const showData = course && isStudent(session, course.id); - const data = showData ? await loadHomeData(course.id) : null; + const data = showData ? await loadHomeData(course.id, session.githubId) : null; setStudentSummary(data?.studentSummary ?? null); setCourseTasks(data?.courseTasks ?? []); }, [course]); diff --git a/client/src/services/course.ts b/client/src/services/course.ts index 7bff948cd1..986e1c6fbd 100644 --- a/client/src/services/course.ts +++ b/client/src/services/course.ts @@ -529,7 +529,7 @@ export class CourseService { return result.data; } - async getStudentSummary(githubId: string | 'me') { + async getStudentSummary(githubId: string) { const result = await studentsApi.getStudentSummary(this.courseId, githubId); return result.data as StudentSummaryDto; } From ecb93ee20a4b31d83cd9ff0b123ec32502dbec09 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Sun, 9 Jun 2024 15:54:32 +0300 Subject: [PATCH 18/26] refactor: fix syntax error at comment --- nestjs/src/courses/course-students/course-students.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nestjs/src/courses/course-students/course-students.service.ts b/nestjs/src/courses/course-students/course-students.service.ts index 3cb57a8376..8a772f14d9 100644 --- a/nestjs/src/courses/course-students/course-students.service.ts +++ b/nestjs/src/courses/course-students/course-students.service.ts @@ -124,7 +124,7 @@ const getStageInterviewRating = (stageInterviews: StageInterview[]) => { .map(({ stageInterviewFeedbacks, score }: StageInterview) => stageInterviewFeedbacks.map((feedback: StageInterviewFeedback) => ({ date: feedback.updatedDate, - // interviews in new template should have score precalculated + // interviews in new template should have precalculated score rating: score ?? getInterviewRatings(JSON.parse(feedback.json) as StageInterviewFeedbackJson).rating, })), ) From 2281924ae6b751bcf971c38dc28b7e5de56c6135 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Sun, 9 Jun 2024 15:56:34 +0300 Subject: [PATCH 19/26] refactor: shorten return statement at getStageInterviewRating function --- nestjs/src/courses/course-students/course-students.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nestjs/src/courses/course-students/course-students.service.ts b/nestjs/src/courses/course-students/course-students.service.ts index 8a772f14d9..65ad6dd809 100644 --- a/nestjs/src/courses/course-students/course-students.service.ts +++ b/nestjs/src/courses/course-students/course-students.service.ts @@ -131,7 +131,7 @@ const getStageInterviewRating = (stageInterviews: StageInterview[]) => { .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?.rating ?? null; }; export function getInterviewRatings({ skills, programmingTask, resume }: StageInterviewFeedbackJson) { From 224c2b3d5836624dca8592a36a7eaaf0e4047482 Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Sun, 9 Jun 2024 16:22:36 +0300 Subject: [PATCH 20/26] refactor: remove unused import --- .../src/courses/course-students/course-students.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nestjs/src/courses/course-students/course-students.controller.ts b/nestjs/src/courses/course-students/course-students.controller.ts index 15599d124b..3d117d57f2 100644 --- a/nestjs/src/courses/course-students/course-students.controller.ts +++ b/nestjs/src/courses/course-students/course-students.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, NotFoundException, Param, Req, UseGuards } from '@nestjs/common'; +import { Controller, Get, NotFoundException, Param, UseGuards } from '@nestjs/common'; import { ApiBadRequestResponse, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { DefaultGuard } from '../../auth'; import { StudentSummaryDto } from './dto/student-summary.dto'; From 6fb76a641f0d54ac67915430a6f890137c4e01ae Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Sun, 9 Jun 2024 16:24:12 +0300 Subject: [PATCH 21/26] refactor: avoid using unnecessary typecast --- nestjs/src/courses/course-students/course-students.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nestjs/src/courses/course-students/course-students.service.ts b/nestjs/src/courses/course-students/course-students.service.ts index 65ad6dd809..fdbdef1111 100644 --- a/nestjs/src/courses/course-students/course-students.service.ts +++ b/nestjs/src/courses/course-students/course-students.service.ts @@ -155,7 +155,7 @@ export function getInterviewRatings({ skills, programmingTask, resume }: StageIn } export function convertToMentorBasic(mentor: Mentor): MentorBasic { - const user = (mentor.user as User)!; + const { user } = mentor; return { isActive: !mentor.isExpelled, name: createName(user), From 7cc7edf807daab8ab957f3c52497fa124812322c Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Sun, 9 Jun 2024 16:25:57 +0300 Subject: [PATCH 22/26] refactor: add destructuring to convertToMentorBasic function --- .../courses/course-students/course-students.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nestjs/src/courses/course-students/course-students.service.ts b/nestjs/src/courses/course-students/course-students.service.ts index fdbdef1111..b66ca34d3a 100644 --- a/nestjs/src/courses/course-students/course-students.service.ts +++ b/nestjs/src/courses/course-students/course-students.service.ts @@ -155,13 +155,13 @@ export function getInterviewRatings({ skills, programmingTask, resume }: StageIn } export function convertToMentorBasic(mentor: Mentor): MentorBasic { - const { user } = mentor; + const { user, isExpelled, id, students } = mentor; return { - isActive: !mentor.isExpelled, + isActive: !isExpelled, name: createName(user), - id: mentor.id, + id: id, githubId: user.githubId, - students: mentor.students ? mentor.students.filter(s => !s.isExpelled && !s.isFailed).map(s => ({ id: s.id })) : [], + students: students ? students.filter(s => !s.isExpelled && !s.isFailed).map(s => ({ id: s.id })) : [], cityName: user.cityName ?? '', countryName: user.countryName ?? '', }; From 8d82bae2eaafba1dcdca45959519fb82765e5f4a Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Sun, 9 Jun 2024 16:37:42 +0300 Subject: [PATCH 23/26] refactor: remove types duplication --- .../course-students.service.ts | 2 +- .../courses/course-students/types/discord.ts | 5 -- .../courses/course-students/types/index.ts | 6 -- .../course-students/types/mentor-basic.ts | 9 --- .../types/stage-interview-feedback-json.ts | 58 ------------------- .../course-students/types/student-basic.ts | 21 ------- .../course-students/types/user-basic.ts | 5 -- 7 files changed, 1 insertion(+), 105 deletions(-) delete mode 100644 nestjs/src/courses/course-students/types/discord.ts delete mode 100644 nestjs/src/courses/course-students/types/index.ts delete mode 100644 nestjs/src/courses/course-students/types/mentor-basic.ts delete mode 100644 nestjs/src/courses/course-students/types/stage-interview-feedback-json.ts delete mode 100644 nestjs/src/courses/course-students/types/student-basic.ts delete mode 100644 nestjs/src/courses/course-students/types/user-basic.ts diff --git a/nestjs/src/courses/course-students/course-students.service.ts b/nestjs/src/courses/course-students/course-students.service.ts index b66ca34d3a..fa19361952 100644 --- a/nestjs/src/courses/course-students/course-students.service.ts +++ b/nestjs/src/courses/course-students/course-students.service.ts @@ -7,7 +7,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Mentor as MentorWithContacts } from './dto/mentor-student-summary.dto'; -import { MentorBasic, StageInterviewFeedbackJson } from './types'; +import { MentorBasic, StageInterviewFeedbackJson } from '@common/models'; @Injectable() export class CourseStudentsService { diff --git a/nestjs/src/courses/course-students/types/discord.ts b/nestjs/src/courses/course-students/types/discord.ts deleted file mode 100644 index a6f0e6c495..0000000000 --- a/nestjs/src/courses/course-students/types/discord.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default interface Discord { - id: string; - username: string; - discriminator: string; -} diff --git a/nestjs/src/courses/course-students/types/index.ts b/nestjs/src/courses/course-students/types/index.ts deleted file mode 100644 index eeeb38c55b..0000000000 --- a/nestjs/src/courses/course-students/types/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Discord from './discord'; -import MentorBasic from './mentor-basic'; -import StudentBasic from './student-basic'; -import UserBasic from './user-basic'; -import { StageInterviewFeedbackJson } from './stage-interview-feedback-json'; -export { Discord, MentorBasic, StudentBasic, UserBasic, StageInterviewFeedbackJson }; diff --git a/nestjs/src/courses/course-students/types/mentor-basic.ts b/nestjs/src/courses/course-students/types/mentor-basic.ts deleted file mode 100644 index d12eef4636..0000000000 --- a/nestjs/src/courses/course-students/types/mentor-basic.ts +++ /dev/null @@ -1,9 +0,0 @@ -import StudentBasic from './student-basic'; -import UserBasic from './user-basic'; - -export default interface MentorBasic extends UserBasic { - isActive: boolean; - cityName: string; - countryName: string; - students: (StudentBasic | { id: number })[]; -} diff --git a/nestjs/src/courses/course-students/types/stage-interview-feedback-json.ts b/nestjs/src/courses/course-students/types/stage-interview-feedback-json.ts deleted file mode 100644 index 47d2b47dca..0000000000 --- a/nestjs/src/courses/course-students/types/stage-interview-feedback-json.ts +++ /dev/null @@ -1,58 +0,0 @@ -interface StageInterviewFeedback { - common: { - reason: 'haveITEducation' | 'doNotWorkInIT' | 'whatThisCourseAbout' | 'other' | null; - reasonOther: string | null; - whenStartCoding: number | null; - schoolChallengesParticipaton: string | null; - whereStudied: string | null; - workExperience: string | null; - otherAchievements: string | null; - militaryService: 'served' | 'liable' | 'notLiable' | null; - }; - skills?: { - [index: string]: any; - htmlCss: { - level: number | null; - }; - dataStructures: { - array: number | null; - list: number | null; - stack: number | null; - queue: number | null; - tree: number | null; - hashTable: number | null; - heap: number | null; - }; - common: { - binaryNumber: number | null; - oop: number | null; - bigONotation: number | null; - sortingAndSearchAlgorithms: number | null; - }; - comment: string | null; - }; - programmingTask: { - task: string | null; - codeWritingLevel: number | null; - resolved: number | null; - comment: string | null; - }; - resume: { - verdict: StageInterviewFeedbackVerdict; - comment: string | null; - score: number; - }; -} - -export type StageInterviewFeedbackVerdict = 'yes' | 'no' | 'noButGoodCandidate' | 'didNotDecideYet' | null; - -export type EnglishLevel = 'a0' | 'a1' | 'a1+' | 'a2' | 'a2+' | 'b1' | 'b1+' | 'b2' | 'b2+' | 'c1' | 'c1+' | 'c2'; - -export interface StageInterviewFeedbackJson extends StageInterviewFeedback { - english: { - levelStudentOpinion: EnglishLevel | null; - levelMentorOpinion: EnglishLevel | null; - whereAndWhenLearned: string | null; - comment: string | null; - }; -} diff --git a/nestjs/src/courses/course-students/types/student-basic.ts b/nestjs/src/courses/course-students/types/student-basic.ts deleted file mode 100644 index 2089173f5e..0000000000 --- a/nestjs/src/courses/course-students/types/student-basic.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Discord from './discord'; -import MentorBasic from './mentor-basic'; -import UserBasic from './user-basic'; - -export default interface StudentBasic extends UserBasic { - isActive: boolean; - cityName?: string | null; - countryName?: string | null; - mentor: - | MentorBasic - | { id: number } - | { - id: number; - githubId: string; - name: string; - } - | null; - discord: Discord | null; - totalScore: number; - rank?: number; -} diff --git a/nestjs/src/courses/course-students/types/user-basic.ts b/nestjs/src/courses/course-students/types/user-basic.ts deleted file mode 100644 index 84ee15435a..0000000000 --- a/nestjs/src/courses/course-students/types/user-basic.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default interface UserBasic { - id: number; - githubId: string; - name: string; -} From 8a0f92a1f7b802b41deeeb0300d0e4d29fe53cab Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Sun, 9 Jun 2024 17:56:17 +0300 Subject: [PATCH 24/26] refactor: fix eslint error --- .../StudentDashboard/components/MentorInfo/MentorInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/modules/StudentDashboard/components/MentorInfo/MentorInfo.tsx b/client/src/modules/StudentDashboard/components/MentorInfo/MentorInfo.tsx index 3cd8f93523..88556f29f6 100644 --- a/client/src/modules/StudentDashboard/components/MentorInfo/MentorInfo.tsx +++ b/client/src/modules/StudentDashboard/components/MentorInfo/MentorInfo.tsx @@ -16,7 +16,7 @@ export interface MentorContact { } interface Props { - mentor: MentorStudentSummaryDto + mentor: MentorStudentSummaryDto; } function MentorInfo({ mentor }: Props) { From 0895d6f13d3fc3810c877ce4b9980ef34d6c0f1c Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Thu, 11 Jul 2024 19:22:58 +0300 Subject: [PATCH 25/26] fix: fix merge issues --- client/src/api/api.ts | 81 +++++++++++++++++++++++++++ nestjs/src/courses/courses.module.ts | 3 + nestjs/src/spec.json | 84 ++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+) diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 159bf39202..276729bb32 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -16196,6 +16196,33 @@ export const StudentsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getStudent(studentId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {number} courseId + * @param {string} githubId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getStudentSummary(courseId: number, githubId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getStudentSummary(courseId, githubId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {string} current + * @param {string} pageSize + * @param {string} [student] + * @param {string} [country] + * @param {string} [city] + * @param {string} [ongoingCourses] + * @param {string} [previousCourses] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getUserStudents(current: string, pageSize: string, student?: string, country?: string, city?: string, ongoingCourses?: string, previousCourses?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getUserStudents(current, pageSize, student, country, city, ongoingCourses, previousCourses, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -16215,6 +16242,31 @@ export const StudentsApiFactory = function (configuration?: Configuration, baseP getStudent(studentId: number, options?: any): AxiosPromise { return localVarFp.getStudent(studentId, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {number} courseId + * @param {string} githubId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getStudentSummary(courseId: number, githubId: string, options?: any): AxiosPromise { + return localVarFp.getStudentSummary(courseId, githubId, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {string} current + * @param {string} pageSize + * @param {string} [student] + * @param {string} [country] + * @param {string} [city] + * @param {string} [ongoingCourses] + * @param {string} [previousCourses] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getUserStudents(current: string, pageSize: string, student?: string, country?: string, city?: string, ongoingCourses?: string, previousCourses?: string, options?: any): AxiosPromise { + return localVarFp.getUserStudents(current, pageSize, student, country, city, ongoingCourses, previousCourses, options).then((request) => request(axios, basePath)); + }, }; }; @@ -16235,6 +16287,35 @@ export class StudentsApi extends BaseAPI { public getStudent(studentId: number, options?: AxiosRequestConfig) { return StudentsApiFp(this.configuration).getStudent(studentId, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @param {number} courseId + * @param {string} githubId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof StudentsApi + */ + public getStudentSummary(courseId: number, githubId: string, options?: AxiosRequestConfig) { + return StudentsApiFp(this.configuration).getStudentSummary(courseId, githubId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {string} current + * @param {string} pageSize + * @param {string} [student] + * @param {string} [country] + * @param {string} [city] + * @param {string} [ongoingCourses] + * @param {string} [previousCourses] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof StudentsApi + */ + public getUserStudents(current: string, pageSize: string, student?: string, country?: string, city?: string, ongoingCourses?: string, previousCourses?: string, options?: AxiosRequestConfig) { + return StudentsApiFp(this.configuration).getUserStudents(current, pageSize, student, country, city, ongoingCourses, previousCourses, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/nestjs/src/courses/courses.module.ts b/nestjs/src/courses/courses.module.ts index f28150007f..160e4df8fb 100644 --- a/nestjs/src/courses/courses.module.ts +++ b/nestjs/src/courses/courses.module.ts @@ -67,6 +67,7 @@ import { SelfEducationService } from './task-verifications/self-education.servic import { CourseMentorsController, CourseMentorsService } from './course-mentors'; import { CourseStudentsController } from './course-students/course-students.controller'; import { CourseStudentsService } from './course-students/course-students.service'; +import { MentorReviewsController, MentorReviewsService } from './mentor-reviews'; @Module({ imports: [ @@ -121,6 +122,7 @@ import { CourseStudentsService } from './course-students/course-students.service CourseUsersController, CourseMentorsController, CourseStudentsController, + MentorReviewsController, ], providers: [ CourseTasksService, @@ -149,6 +151,7 @@ import { CourseStudentsService } from './course-students/course-students.service TaskVerificationsService, CourseMentorsService, CourseStudentsService, + MentorReviewsService ], exports: [CourseTasksService, CourseUsersService, CoursesService, StudentsService], }) diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json index 69ebc604e2..0b6434e55f 100644 --- a/nestjs/src/spec.json +++ b/nestjs/src/spec.json @@ -3972,6 +3972,90 @@ "properties": { "id": { "type": "number" }, "githubId": { "type": "string" }, "name": { "type": "string" } }, "required": ["id", "githubId", "name"] }, + "ResultDto": { + "type": "object", + "properties": { "score": { "type": "number" }, "courseTaskId": { "type": "number" } } + }, + "MentorStudentSummaryDto": { + "type": "object", + "properties": { + "id": { "type": "number" }, + "githubId": { "type": "string" }, + "name": { "type": "string" }, + "isActive": { "type": "boolean" }, + "cityName": { "type": "string" }, + "countryName": { "type": "string" }, + "students": { "type": "array", "items": { "type": "string" } }, + "contactsEmail": { "type": "string", "nullable": true }, + "contactsPhone": { "type": "string", "nullable": true }, + "contactsSkype": { "type": "string", "nullable": true }, + "contactsTelegram": { "type": "string", "nullable": true }, + "contactsNotes": { "type": "string", "nullable": true }, + "contactsWhatsApp": { "type": "string", "nullable": true } + }, + "required": [ + "id", + "githubId", + "name", + "isActive", + "cityName", + "countryName", + "students", + "contactsEmail", + "contactsPhone", + "contactsSkype", + "contactsTelegram", + "contactsNotes", + "contactsWhatsApp" + ] + }, + "StudentSummaryDto": { + "type": "object", + "properties": { + "totalScore": { "type": "number" }, + "results": { "type": "array", "items": { "$ref": "#/components/schemas/ResultDto" } }, + "isActive": { "type": "boolean" }, + "mentor": { "nullable": true, "allOf": [{ "$ref": "#/components/schemas/MentorStudentSummaryDto" }] }, + "rank": { "type": "number" }, + "repository": { "type": "string", "nullable": true } + }, + "required": ["totalScore", "results", "isActive", "mentor", "rank", "repository"] + }, + "MentorReviewDto": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "Task solution id" }, + "taskName": { "type": "string", "description": "Task name" }, + "solutionUrl": { "type": "string", "description": "Task solution url" }, + "submittedAt": { "format": "date-time", "type": "string", "description": "Task solution submission date" }, + "checker": { "type": "string", "description": "Checker github id" }, + "score": { "type": "number", "description": "Task solution score" }, + "maxScore": { "type": "number", "description": "Task max score" }, + "student": { "type": "string", "description": "Student github id" }, + "reviewedAt": { "format": "date-time", "type": "string", "description": "Task solution review date" }, + "taskDescriptionUrl": { "type": "string", "description": "Task description url" } + }, + "required": [ + "id", + "taskName", + "solutionUrl", + "submittedAt", + "checker", + "score", + "maxScore", + "student", + "reviewedAt", + "taskDescriptionUrl" + ] + }, + "MentorReviewsDto": { + "type": "object", + "properties": { + "content": { "type": "array", "items": { "$ref": "#/components/schemas/MentorReviewDto" } }, + "pagination": { "$ref": "#/components/schemas/PaginationMetaDto" } + }, + "required": ["content", "pagination"] + }, "Map": { "type": "object", "properties": {} }, "NotificationUserSettingsDto": { "type": "object", From cbd7f24f1586a6780d6b8cfab6a2772bf87928fa Mon Sep 17 00:00:00 2001 From: Dzmitry Chervanenka Date: Thu, 11 Jul 2024 19:46:16 +0300 Subject: [PATCH 26/26] refactor: fix eslint issue --- nestjs/src/courses/courses.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nestjs/src/courses/courses.module.ts b/nestjs/src/courses/courses.module.ts index 160e4df8fb..f7a9152347 100644 --- a/nestjs/src/courses/courses.module.ts +++ b/nestjs/src/courses/courses.module.ts @@ -151,7 +151,7 @@ import { MentorReviewsController, MentorReviewsService } from './mentor-reviews' TaskVerificationsService, CourseMentorsService, CourseStudentsService, - MentorReviewsService + MentorReviewsService, ], exports: [CourseTasksService, CourseUsersService, CoursesService, StudentsService], })