From 48f7e1044cf710dad089d6a78abb5bb462f4aaf9 Mon Sep 17 00:00:00 2001 From: Valery Date: Sat, 5 Aug 2023 19:37:42 +0200 Subject: [PATCH 01/10] feat: migrate get cross check feedback --- client/src/api/api.ts | 190 ++++++++++++++++++ client/src/services/course.ts | 34 ---- .../course-cross-checks.controller.ts | 29 ++- .../course-cross-checks.service.ts | 96 ++++++++- .../cross-check-feedback.guard.ts | 39 ++++ .../dto/cross-check-feedback.dto.ts | 87 ++++++++ nestjs/src/courses/cross-checks/dto/index.ts | 1 + .../courses/interviews/interviews.service.ts | 16 +- nestjs/src/courses/score/score.service.ts | 15 +- nestjs/src/spec.json | 50 +++++ nestjs/src/users/users.service.ts | 12 ++ server/src/models/taskSolutionResult.ts | 4 +- .../routes/course/crossCheck/getFeedback.ts | 27 --- server/src/routes/course/crossCheck/index.ts | 1 - server/src/routes/course/index.ts | 1 - server/src/services/taskResults.service.ts | 90 ++++----- 16 files changed, 548 insertions(+), 144 deletions(-) create mode 100644 nestjs/src/courses/cross-checks/cross-check-feedback.guard.ts create mode 100644 nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts delete mode 100644 server/src/routes/course/crossCheck/getFeedback.ts diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 3be31c50c7..42d03796f4 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -2068,6 +2068,77 @@ export const CriteriaDtoTypeEnum = { export type CriteriaDtoTypeEnum = typeof CriteriaDtoTypeEnum[keyof typeof CriteriaDtoTypeEnum]; +/** + * + * @export + * @interface CrossCheckCriteriaDataDto + */ +export interface CrossCheckCriteriaDataDto { + /** + * + * @type {string} + * @memberof CrossCheckCriteriaDataDto + */ + 'key': string; + /** + * + * @type {number} + * @memberof CrossCheckCriteriaDataDto + */ + 'max'?: number; + /** + * + * @type {string} + * @memberof CrossCheckCriteriaDataDto + */ + 'text': string; + /** + * + * @type {string} + * @memberof CrossCheckCriteriaDataDto + */ + 'type': CrossCheckCriteriaDataDtoTypeEnum; + /** + * + * @type {number} + * @memberof CrossCheckCriteriaDataDto + */ + 'point'?: number; + /** + * + * @type {string} + * @memberof CrossCheckCriteriaDataDto + */ + 'textComment'?: string; +} + +export const CrossCheckCriteriaDataDtoTypeEnum = { + Title: 'title', + Subtask: 'subtask', + Penalty: 'penalty' +} as const; + +export type CrossCheckCriteriaDataDtoTypeEnum = typeof CrossCheckCriteriaDataDtoTypeEnum[keyof typeof CrossCheckCriteriaDataDtoTypeEnum]; + +/** + * + * @export + * @interface CrossCheckFeedbackDto + */ +export interface CrossCheckFeedbackDto { + /** + * + * @type {string} + * @memberof CrossCheckFeedbackDto + */ + 'url'?: string; + /** + * + * @type {Array} + * @memberof CrossCheckFeedbackDto + */ + 'reviews'?: Array; +} /** * * @export @@ -2230,6 +2301,55 @@ export interface CrossCheckPairResponseDto { */ 'pagination': PaginationDto; } +/** + * + * @export + * @interface CrossCheckSolutionReviewDto + */ +export interface CrossCheckSolutionReviewDto { + /** + * + * @type {number} + * @memberof CrossCheckSolutionReviewDto + */ + 'id': number; + /** + * + * @type {number} + * @memberof CrossCheckSolutionReviewDto + */ + 'dateTime': number; + /** + * + * @type {string} + * @memberof CrossCheckSolutionReviewDto + */ + 'comment': string; + /** + * + * @type {Array} + * @memberof CrossCheckSolutionReviewDto + */ + 'criteria'?: Array; + /** + * + * @type {object} + * @memberof CrossCheckSolutionReviewDto + */ + 'author': object; + /** + * + * @type {number} + * @memberof CrossCheckSolutionReviewDto + */ + 'score': number; + /** + * + * @type {Array} + * @memberof CrossCheckSolutionReviewDto + */ + 'messages': Array; +} /** * * @export @@ -9368,6 +9488,43 @@ export const CoursesTasksApiAxiosParamCreator = function (configuration?: Config + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {number} courseId + * @param {number} courseTaskId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCrossCheckFeedback: async (courseId: number, courseTaskId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'courseId' is not null or undefined + assertParamExists('getCrossCheckFeedback', 'courseId', courseId) + // verify required parameter 'courseTaskId' is not null or undefined + assertParamExists('getCrossCheckFeedback', 'courseTaskId', courseTaskId) + const localVarPath = `/courses/{courseId}/cross-checks/{courseTaskId}/student/feedback` + .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))) + .replace(`{${"courseTaskId"}}`, encodeURIComponent(String(courseTaskId))); + // 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}; @@ -9628,6 +9785,17 @@ export const CoursesTasksApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getCrossCheckCsv(courseId, courseTaskId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {number} courseId + * @param {number} courseTaskId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getCrossCheckFeedback(courseId: number, courseTaskId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getCrossCheckFeedback(courseId, courseTaskId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {number} courseId @@ -9756,6 +9924,16 @@ export const CoursesTasksApiFactory = function (configuration?: Configuration, b getCrossCheckCsv(courseId: number, courseTaskId: number, options?: any): AxiosPromise { return localVarFp.getCrossCheckCsv(courseId, courseTaskId, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {number} courseId + * @param {number} courseTaskId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCrossCheckFeedback(courseId: number, courseTaskId: number, options?: any): AxiosPromise { + return localVarFp.getCrossCheckFeedback(courseId, courseTaskId, options).then((request) => request(axios, basePath)); + }, /** * * @param {number} courseId @@ -9897,6 +10075,18 @@ export class CoursesTasksApi extends BaseAPI { return CoursesTasksApiFp(this.configuration).getCrossCheckCsv(courseId, courseTaskId, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {number} courseId + * @param {number} courseTaskId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CoursesTasksApi + */ + public getCrossCheckFeedback(courseId: number, courseTaskId: number, options?: AxiosRequestConfig) { + return CoursesTasksApiFp(this.configuration).getCrossCheckFeedback(courseId, courseTaskId, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {number} courseId diff --git a/client/src/services/course.ts b/client/src/services/course.ts index 9ac562d768..cd22622c12 100644 --- a/client/src/services/course.ts +++ b/client/src/services/course.ts @@ -25,39 +25,12 @@ export enum CrossCheckStatus { } export type CrossCheckCriteriaType = 'title' | 'subtask' | 'penalty'; -export interface CrossCheckCriteriaData { - key: string; - max?: number; - text: string; - type: CrossCheckCriteriaType; - point?: number; - textComment?: string; -} export interface CrossCheckMessageAuthor { id: number; githubId: string; } -export type SolutionReviewType = { - id: number; - dateTime: number; - comment: string; - criteria?: CrossCheckCriteriaData[]; - author: { - id: number; - name: string; - githubId: string; - discord: Discord | null; - } | null; - score: number; - messages: CrossCheckMessageDto[]; -}; - -export type Feedback = { - url?: string; - reviews?: SolutionReviewType[]; -}; export interface Verification { id: number; @@ -531,13 +504,6 @@ export class CourseService { return result.data.data; } - async getCrossCheckFeedback(githubId: string, courseTaskId: number) { - const result = await this.axios.get<{ data: Feedback }>( - `/student/${githubId}/task/${courseTaskId}/cross-check/feedback`, - ); - return result.data.data; - } - async createCrossCheckDistribution(courseTaskId: number) { const result = await this.axios.post(`/task/${courseTaskId}/cross-check/distribution`); return result.data; diff --git a/nestjs/src/courses/cross-checks/course-cross-checks.controller.ts b/nestjs/src/courses/cross-checks/course-cross-checks.controller.ts index 8be436dfec..8658b3d81e 100644 --- a/nestjs/src/courses/cross-checks/course-cross-checks.controller.ts +++ b/nestjs/src/courses/cross-checks/course-cross-checks.controller.ts @@ -15,17 +15,18 @@ import { ApiForbiddenResponse, ApiOperation, ApiResponse, ApiTags, ApiQuery } fr import { CourseGuard, CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../../auth'; import { CourseTasksService } from '../course-tasks'; import { OrderField, OrderDirection, CourseCrossCheckService } from './course-cross-checks.service'; -import { CrossCheckPairResponseDto } from './dto'; +import { CrossCheckFeedbackDto, CrossCheckPairResponseDto } from './dto'; import { AvailableReviewStatsDto } from './dto/available-review-stats.dto'; import { parseAsync } from 'json2csv'; import { Response } from 'express'; +import { StudentId } from 'src/core/decorators'; @Controller('courses/:courseId/cross-checks') @ApiTags('courses tasks') @UseGuards(DefaultGuard, CourseGuard) export class CourseCrossCheckController { constructor( - private crossCheckPairsService: CourseCrossCheckService, + private courseCrossCheckService: CourseCrossCheckService, private courseTasksService: CourseTasksService, ) {} @@ -53,7 +54,7 @@ export class CourseCrossCheckController { @Query('url') url: string, @Query('task') task: string, ) { - const { items, pagination } = await this.crossCheckPairsService.findPairs( + const { items, pagination } = await this.courseCrossCheckService.findPairs( courseId, { pageSize, current }, { checker, student, url, task }, @@ -76,7 +77,7 @@ export class CourseCrossCheckController { if (!studentId) throw new BadRequestException(); const crossChecks = await this.courseTasksService.getAvailableCrossChecks(courseId); if (crossChecks.length === 0) return []; - const res = await this.crossCheckPairsService.getAvailableCrossChecksStats(crossChecks, studentId); + const res = await this.courseCrossCheckService.getAvailableCrossChecksStats(crossChecks, studentId); return res.map(e => new AvailableReviewStatsDto(e)); } @@ -92,7 +93,7 @@ export class CourseCrossCheckController { ) { const [courseTask, solutionUrls] = await Promise.all([ this.courseTasksService.getById(courseTaskId), - this.crossCheckPairsService.getSolutionsUrls(courseId, courseTaskId), + this.courseCrossCheckService.getSolutionsUrls(courseId, courseTaskId), ]); const parsedData = await parseAsync(solutionUrls, { fields: ['githubId', 'solutionUrl'] }); @@ -102,4 +103,22 @@ export class CourseCrossCheckController { res.end(parsedData); } + + @Get(':courseTaskId/student/feedback') + @ApiOperation({ operationId: 'getCrossCheckFeedback' }) + @ApiForbiddenResponse() + @ApiResponse({ type: CrossCheckFeedbackDto }) + @RequiredRoles([CourseRole.Manager, Role.Admin, CourseRole.Student], true) + @UseGuards(DefaultGuard, RoleGuard) + public async getCrossCheckFeedback( + @StudentId() studentId: number, + @Param('courseId', ParseIntPipe) _courseId: number, + @Param('courseTaskId', ParseIntPipe) courseTaskId: number, + ) { + const [crossCheckSolutionReviews, taskSolution] = await Promise.all([ + this.courseCrossCheckService.getCrossCheckSolutionReviews(studentId, courseTaskId), + this.courseCrossCheckService.getTaskSolution(studentId, courseTaskId), + ]); + return new CrossCheckFeedbackDto(crossCheckSolutionReviews, taskSolution); + } } diff --git a/nestjs/src/courses/cross-checks/course-cross-checks.service.ts b/nestjs/src/courses/cross-checks/course-cross-checks.service.ts index 9ef9969ce0..8537953616 100644 --- a/nestjs/src/courses/cross-checks/course-cross-checks.service.ts +++ b/nestjs/src/courses/cross-checks/course-cross-checks.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { Task } from '@entities/task'; import { User } from '@entities/user'; import { InjectRepository } from '@nestjs/typeorm'; @@ -6,8 +6,17 @@ import { TaskSolutionChecker } from '@entities/taskSolutionChecker'; import { Repository } from 'typeorm'; import { Student } from '@entities/student'; import { TaskSolution } from '@entities/taskSolution'; -import { CrossCheckMessage, ScoreRecord, TaskSolutionResult } from '@entities/taskSolutionResult'; +import { + CrossCheckMessage, + CrossCheckMessageAuthorRole, + ScoreRecord, + TaskSolutionResult, +} from '@entities/taskSolutionResult'; import { CourseTask } from '@entities/courseTask'; +import { UsersService } from 'src/users/users.service'; +import { PersonDto } from 'src/core/dto'; +import { CrossCheckCriteriaDataDto, CrossCheckMessageDto } from './dto'; +import { Discord } from 'src/profile/dto'; export type CrossCheckPair = { id: number; @@ -69,6 +78,21 @@ const orderFieldMapping: Record = { reviewedDate: 'tsr.updatedDate', }; +export type CrossCheckSolutionReview = { + id: number; + dateTime: number; + comment: string; + criteria?: CrossCheckCriteriaDataDto[]; + author: { + id: number; + name: string; + githubId: string; + discord: Discord | null; + } | null; + score: number; + messages: CrossCheckMessageDto[]; +}; + @Injectable() export class CourseCrossCheckService { constructor( @@ -76,6 +100,8 @@ export class CourseCrossCheckService { private readonly taskSolutionCheckerRepository: Repository, @InjectRepository(TaskSolution) private readonly taskSolutionRepository: Repository, + @InjectRepository(TaskSolutionResult) + private readonly TaskSolutionResultRepository: Repository, ) {} public async findPairs( @@ -222,4 +248,70 @@ export class CourseCrossCheckService { }) .filter(el => el.checksCount !== 0); } + + public isCrossCheckTask(courseTask: Partial) { + return courseTask.checker === 'crossCheck'; + } + + public async getCrossCheckSolutionReviews( + studentId: number, + courseTaskId: number, + ): Promise { + const comments = ( + await this.TaskSolutionResultRepository.createQueryBuilder('tsr') + .select(['tsr.id', 'tsr.comment', 'tsr.anonymous', 'tsr.score', 'tsr.messages', 'tsr.historicalScores']) + .innerJoin('tsr.checker', 'checker') + .innerJoin('checker.user', 'user') + .addSelect(['checker.id', ...UsersService.getPrimaryUserFields('user')]) + .where('"tsr"."studentId" = :studentId', { studentId }) + .andWhere('"tsr"."courseTaskId" = :courseTaskId', { courseTaskId }) + .getMany() + ).map(taskSolutionResult => { + const author = !taskSolutionResult.anonymous + ? { + id: taskSolutionResult.checker.user.id, + name: PersonDto.getName(taskSolutionResult.checker.user), + githubId: taskSolutionResult.checker.user.githubId, + discord: taskSolutionResult.checker.user.discord, + } + : null; + + const [lastCheck] = taskSolutionResult.historicalScores.sort((a, b) => b.dateTime - a.dateTime); + + if (!lastCheck) { + throw new BadRequestException('No historical scores found'); + } + + const { dateTime, criteria } = lastCheck; + + const messages = !taskSolutionResult.anonymous + ? taskSolutionResult.messages + : taskSolutionResult.messages.map(message => ({ + ...message, + author: message.role === CrossCheckMessageAuthorRole.Reviewer ? null : message.author, + })); + + return { + dateTime, + author, + messages, + id: taskSolutionResult.id, + comment: taskSolutionResult.comment ?? '', + score: taskSolutionResult.score, + criteria, + }; + }); + + return comments; + } + + public async getTaskSolution(studentId: number, courseTaskId: number) { + const taskSolution = await this.taskSolutionRepository + .createQueryBuilder('ts') + .where('"ts"."studentId" = :studentId', { studentId }) + .andWhere('"ts"."courseTaskId" = :courseTaskId', { courseTaskId }) + .getOneOrFail(); + + return taskSolution; + } } diff --git a/nestjs/src/courses/cross-checks/cross-check-feedback.guard.ts b/nestjs/src/courses/cross-checks/cross-check-feedback.guard.ts new file mode 100644 index 0000000000..7f985dc724 --- /dev/null +++ b/nestjs/src/courses/cross-checks/cross-check-feedback.guard.ts @@ -0,0 +1,39 @@ +import { Injectable, CanActivate, ExecutionContext, BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { CourseCrossCheckService } from './course-cross-checks.service'; +import { CourseRole } from '@entities/session'; +import { CourseTasksService } from '../course-tasks'; + +@Injectable() +export class FeedbackGuard implements CanActivate { + constructor( + private readonly courseCrossCheckService: CourseCrossCheckService, + private readonly courseTasksService: CourseTasksService, + ) {} + + canActivate(context: ExecutionContext): boolean | Promise { + const request = context.switchToHttp().getRequest(); + const { courseId, courseTaskId } = request.params; + const studentId = request.user.courses[courseId]?.studentId; + const isManager = request.user.isAdmin || request.user.courses[courseId]?.roles.includes(CourseRole.Manager); + + if (!studentId && !isManager) { + throw new UnauthorizedException('Not a valid student for this course'); + } + + return this.validateTask(courseTaskId); + } + + async validateTask(courseTaskId: number): Promise { + const courseTask = await this.courseTasksService.getById(courseTaskId); + + if (courseTask == null) { + throw new BadRequestException('not valid student or course task'); + } + + if (!this.courseCrossCheckService.isCrossCheckTask(courseTask)) { + throw new BadRequestException('not supported task'); + } + + return true; + } +} diff --git a/nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts b/nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts new file mode 100644 index 0000000000..6024ab7659 --- /dev/null +++ b/nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts @@ -0,0 +1,87 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CrossCheckMessageDto } from './check-tasks-pairs.dto'; +import { Discord } from 'src/profile/dto'; +import { IsIn, IsNumber, IsOptional, IsString } from 'class-validator'; +import { CrossCheckSolutionReview } from '../course-cross-checks.service'; +import { TaskSolution } from '@entities/taskSolution'; + +export type CrossCheckCriteriaType = 'title' | 'subtask' | 'penalty'; + +export class CrossCheckCriteriaDataDto { + @ApiProperty() + @IsString() + key: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsNumber() + max?: number; + + @ApiProperty() + @IsString() + text: string; + + @ApiProperty({ enum: ['title', 'subtask', 'penalty'] }) + @IsIn(['title', 'subtask', 'penalty']) + type: CrossCheckCriteriaType; + + @ApiProperty({ required: false }) + @IsOptional() + @IsNumber() + point?: number; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + textComment?: string; +} + +export class CrossCheckAuthorDto { + @ApiProperty({ required: true }) + public id: number; + + @ApiProperty({ required: true }) + public name: string; + + @ApiProperty({ required: true }) + public githubId: string; + + @ApiProperty({ nullable: true, type: Discord }) + discord: Discord | null; +} + +export class CrossCheckSolutionReviewDto { + @ApiProperty({ required: true }) + public id: number; + + @ApiProperty({ required: true }) + public dateTime: number; + + @ApiProperty({ required: true }) + public comment: string; + + @ApiProperty({ required: false, type: [CrossCheckCriteriaDataDto] }) + public criteria?: CrossCheckCriteriaDataDto[]; + + @ApiProperty({ required: true }) + public author: CrossCheckAuthorDto | null; + + @ApiProperty({ required: true }) + public score: number; + + @ApiProperty({ required: true, type: [CrossCheckMessageDto] }) + public messages: CrossCheckMessageDto[]; +} + +export class CrossCheckFeedbackDto { + constructor(crossCheckSolutionReviews: CrossCheckSolutionReview[], taskSolution: TaskSolution) { + this.reviews = crossCheckSolutionReviews; + this.url = taskSolution.url; + } + + @ApiProperty({ required: false }) + public url?: string; + + @ApiProperty({ required: false, type: [CrossCheckSolutionReviewDto] }) + public reviews?: CrossCheckSolutionReviewDto[]; +} diff --git a/nestjs/src/courses/cross-checks/dto/index.ts b/nestjs/src/courses/cross-checks/dto/index.ts index 0faefe7e69..b287de3fdb 100644 --- a/nestjs/src/courses/cross-checks/dto/index.ts +++ b/nestjs/src/courses/cross-checks/dto/index.ts @@ -1 +1,2 @@ export * from './check-tasks-pairs.dto'; +export * from './cross-check-feedback.dto'; diff --git a/nestjs/src/courses/interviews/interviews.service.ts b/nestjs/src/courses/interviews/interviews.service.ts index 34d03a4ee3..96f48e4a75 100644 --- a/nestjs/src/courses/interviews/interviews.service.ts +++ b/nestjs/src/courses/interviews/interviews.service.ts @@ -68,7 +68,7 @@ export class InterviewsService { 'student.id', 'student.totalScore', 'student.mentorId', - ...this.getPrimaryUserFields(), + ...UsersService.getPrimaryUserFields(), 'taskChecker.id', ]) .where('is.courseId = :courseId', { courseId }) @@ -98,7 +98,7 @@ export class InterviewsService { .leftJoin('student.stageInterviews', 'si') .leftJoin('si.stageInterviewFeedbacks', 'sif') .addSelect([ - ...this.getPrimaryUserFields(), + ...UsersService.getPrimaryUserFields(), 'si.id', 'si.isGoodCandidate', 'si.isCompleted', @@ -166,18 +166,6 @@ export class InterviewsService { return { rating, htmlCss, common, dataStructures }; } - private getPrimaryUserFields(modelName = 'user') { - return [ - `${modelName}.id`, - `${modelName}.firstName`, - `${modelName}.lastName`, - `${modelName}.githubId`, - `${modelName}.cityName`, - `${modelName}.countryName`, - `${modelName}.discord`, - ]; - } - private isGoodCandidate(stageInterviews: StageInterview[]) { return stageInterviews.some(i => i.isCompleted && i.isGoodCandidate); } diff --git a/nestjs/src/courses/score/score.service.ts b/nestjs/src/courses/score/score.service.ts index fcc8a94cb3..00438e4996 100644 --- a/nestjs/src/courses/score/score.service.ts +++ b/nestjs/src/courses/score/score.service.ts @@ -12,6 +12,7 @@ import { orderByFieldMapping, OrderDirection, OrderField, ScoreQueryDto } from ' import { InterviewsService } from '../interviews'; import { ScoreDto, ScoreStudentDto } from './dto/score.dto'; import { TaskResult } from '@entities/taskResult'; +import { UsersService } from 'src/users/users.service'; const defaultFilter: Partial = { activeOnly: 'false', @@ -101,7 +102,7 @@ export class ScoreService { let query = this.studentRepository .createQueryBuilder('student') .innerJoin('student.user', 'user') - .addSelect(this.getPrimaryUserFields()) + .addSelect(UsersService.getPrimaryUserFields()) .leftJoin('student.mentor', 'mentor', 'mentor."isExpelled" = FALSE') .addSelect(['mentor.id', 'mentor.userId']) .leftJoin('student.taskResults', 'tr') @@ -111,7 +112,7 @@ export class ScoreService { .leftJoin('student.taskInterviewResults', 'tir') .addSelect(['tir.id', 'tir.score', 'tir.courseTaskId', 'tr.studentId', 'tir.updatedDate']) .leftJoin('mentor.user', 'mu') - .addSelect(this.getPrimaryUserFields('mu')) + .addSelect(UsersService.getPrimaryUserFields('mu')) .leftJoin('student.stageInterviews', 'si') .leftJoin('si.stageInterviewFeedbacks', 'sif') .addSelect(['sif.stageInterviewId', 'sif.json', 'sif.updatedDate', 'si.isCompleted', 'si.id', 'si.courseTaskId']) @@ -150,14 +151,4 @@ export class ScoreService { orderBy.direction.toUpperCase() as Uppercase, ); } - - private getPrimaryUserFields = (modelName = 'user') => [ - `${modelName}.id`, - `${modelName}.firstName`, - `${modelName}.lastName`, - `${modelName}.githubId`, - `${modelName}.cityName`, - `${modelName}.countryName`, - `${modelName}.discord`, - ]; } diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json index d2df3ad2d3..06c2780afd 100644 --- a/nestjs/src/spec.json +++ b/nestjs/src/spec.json @@ -687,6 +687,24 @@ "tags": ["courses tasks"] } }, + "/courses/{courseId}/cross-checks/{courseTaskId}/student/feedback": { + "get": { + "operationId": "getCrossCheckFeedback", + "summary": "", + "parameters": [ + { "name": "courseId", "required": true, "in": "path", "schema": { "type": "number" } }, + { "name": "courseTaskId", "required": true, "in": "path", "schema": { "type": "number" } } + ], + "responses": { + "403": { "description": "" }, + "default": { + "description": "", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CrossCheckFeedbackDto" } } } + } + }, + "tags": ["courses tasks"] + } + }, "/course/{courseId}/students/score": { "get": { "operationId": "getScore", @@ -2803,6 +2821,38 @@ }, "required": ["name", "id", "checksCount", "completedChecksCount"] }, + "CrossCheckCriteriaDataDto": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "max": { "type": "number" }, + "text": { "type": "string" }, + "type": { "type": "string", "enum": ["title", "subtask", "penalty"] }, + "point": { "type": "number" }, + "textComment": { "type": "string" } + }, + "required": ["key", "text", "type"] + }, + "CrossCheckSolutionReviewDto": { + "type": "object", + "properties": { + "id": { "type": "number" }, + "dateTime": { "type": "number" }, + "comment": { "type": "string" }, + "criteria": { "type": "array", "items": { "$ref": "#/components/schemas/CrossCheckCriteriaDataDto" } }, + "author": { "type": "object" }, + "score": { "type": "number" }, + "messages": { "type": "array", "items": { "$ref": "#/components/schemas/CrossCheckMessageDto" } } + }, + "required": ["id", "dateTime", "comment", "author", "score", "messages"] + }, + "CrossCheckFeedbackDto": { + "type": "object", + "properties": { + "url": { "type": "string" }, + "reviews": { "type": "array", "items": { "$ref": "#/components/schemas/CrossCheckSolutionReviewDto" } } + } + }, "MentorDto": { "type": "object", "properties": { "id": { "type": "number" }, "githubId": { "type": "string" }, "name": { "type": "string" } }, diff --git a/nestjs/src/users/users.service.ts b/nestjs/src/users/users.service.ts index 521b555e73..e5384fc964 100644 --- a/nestjs/src/users/users.service.ts +++ b/nestjs/src/users/users.service.ts @@ -57,4 +57,16 @@ export class UsersService { } return result.join(' '); } + + public static getPrimaryUserFields(modelName = 'user') { + return [ + `${modelName}.id`, + `${modelName}.firstName`, + `${modelName}.lastName`, + `${modelName}.githubId`, + `${modelName}.cityName`, + `${modelName}.countryName`, + `${modelName}.discord`, + ]; + } } diff --git a/server/src/models/taskSolutionResult.ts b/server/src/models/taskSolutionResult.ts index ad0c043294..08e4e0c0ee 100644 --- a/server/src/models/taskSolutionResult.ts +++ b/server/src/models/taskSolutionResult.ts @@ -13,10 +13,10 @@ import { CourseTask } from './courseTask'; import { TaskSolutionReview } from './taskSolution'; export interface CrossCheckCriteriaData { - key: number; + key: string; max?: number; text: string; - type: string; + type: 'title' | 'subtask' | 'penalty'; point?: number; comment?: string; } diff --git a/server/src/routes/course/crossCheck/getFeedback.ts b/server/src/routes/course/crossCheck/getFeedback.ts deleted file mode 100644 index 2edd02a5e2..0000000000 --- a/server/src/routes/course/crossCheck/getFeedback.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Router from '@koa/router'; -import { BAD_REQUEST, OK } from 'http-status-codes'; -import { ILogger } from '../../../logger'; -import { taskResultsService, CrossCheckService } from '../../../services'; -import { setResponse } from '../../utils'; - -export const getFeedback = (_: ILogger) => async (ctx: Router.RouterContext) => { - const { githubId, courseId, courseTaskId } = ctx.params; - const crossCheckService = new CrossCheckService(courseTaskId); - const { student, courseTask } = await crossCheckService.getStudentAndTask(courseId, githubId); - - if (student == null || courseTask == null) { - setResponse(ctx, BAD_REQUEST, { message: 'not valid student or course task' }); - return; - } - if (!CrossCheckService.isCrossCheckTask(courseTask)) { - setResponse(ctx, BAD_REQUEST, { message: 'not supported task' }); - return; - } - - const feedback = await taskResultsService.getTaskSolutionFeedback(student.id, courseTaskId); - const response = { - url: feedback.url, - reviews: feedback.reviews, - }; - setResponse(ctx, OK, response); -}; diff --git a/server/src/routes/course/crossCheck/index.ts b/server/src/routes/course/crossCheck/index.ts index 25520cc718..8fde4e674b 100644 --- a/server/src/routes/course/crossCheck/index.ts +++ b/server/src/routes/course/crossCheck/index.ts @@ -6,7 +6,6 @@ export * from './createMessage'; export * from './updateMessage'; export * from './deleteSolution'; export * from './getAssignments'; -export * from './getFeedback'; export * from './getResult'; export * from './getSolution'; export * from './getTaskDetails'; diff --git a/server/src/routes/course/index.ts b/server/src/routes/course/index.ts index 6e73c7a0af..d94941566c 100644 --- a/server/src/routes/course/index.ts +++ b/server/src/routes/course/index.ts @@ -262,7 +262,6 @@ function addStudentCrossCheckApi(router: Router, logger: ILogger) { router.get(`${baseUrl}/cross-check/solution`, courseGuard, validateGithubId, crossCheck.getSolution(logger)); router.post(`${baseUrl}/cross-check/result`, courseGuard, validateGithubId, crossCheck.createResult(logger)); router.get(`${baseUrl}/cross-check/result`, courseGuard, validateGithubId, crossCheck.getResult(logger)); - router.get(`${baseUrl}/cross-check/feedback`, courseGuard, ...validators, crossCheck.getFeedback(logger)); router.get(`${baseUrl}/cross-check/assignments`, courseGuard, ...validators, crossCheck.getAssignments(logger)); router.post( `/taskSolutionResult/:taskSolutionResultId/task/:courseTaskId/cross-check/messages`, diff --git a/server/src/services/taskResults.service.ts b/server/src/services/taskResults.service.ts index 1c6b274c87..ede7415550 100644 --- a/server/src/services/taskResults.service.ts +++ b/server/src/services/taskResults.service.ts @@ -10,8 +10,6 @@ import { } from '../models'; import { getRepository } from 'typeorm'; import { getPrimaryUserFields } from './course.service'; -import { createName } from './user.service'; -import { CrossCheckMessageAuthorRole } from '../models/taskSolutionResult'; export async function getTaskResult(studentId: number, courseTaskId: number) { return getRepository(TaskResult) @@ -187,50 +185,50 @@ export async function getCrossCheckData( }; } -export async function getTaskSolutionFeedback(studentId: number, courseTaskId: number) { - const comments = ( - await getRepository(TaskSolutionResult) - .createQueryBuilder('tsr') - .select(['tsr.id', 'tsr.comment', 'tsr.anonymous', 'tsr.score', 'tsr.messages', 'tsr.historicalScores']) - .innerJoin('tsr.checker', 'checker') - .innerJoin('checker.user', 'user') - .addSelect(['checker.id', ...getPrimaryUserFields('user')]) - .where('"tsr"."studentId" = :studentId', { studentId }) - .andWhere('"tsr"."courseTaskId" = :courseTaskId', { courseTaskId }) - .getMany() - ).map(c => { - const author = !c.anonymous - ? { - id: c.checker.user.id, - name: createName(c.checker.user), - githubId: c.checker.user.githubId, - discord: c.checker.user.discord, - } - : null; - const [{ dateTime, criteria }] = c.historicalScores.sort((a, b) => b.dateTime - a.dateTime); - const messages = !c.anonymous - ? c.messages - : c.messages.map(message => ({ - ...message, - author: message.role === CrossCheckMessageAuthorRole.Reviewer ? null : message.author, - })); - return { - dateTime, - author, - messages, - id: c.id, - comment: c.comment, - score: c.score, - criteria, - }; - }); - const taskSolution = await getRepository(TaskSolution) - .createQueryBuilder('ts') - .where('"ts"."studentId" = :studentId', { studentId }) - .andWhere('"ts"."courseTaskId" = :courseTaskId', { courseTaskId }) - .getOne(); - return { url: taskSolution?.url, reviews: comments }; -} +// export async function getTaskSolutionFeedback(studentId: number, courseTaskId: number) { +// const comments = ( +// await getRepository(TaskSolutionResult) +// .createQueryBuilder('tsr') +// .select(['tsr.id', 'tsr.comment', 'tsr.anonymous', 'tsr.score', 'tsr.messages', 'tsr.historicalScores']) +// .innerJoin('tsr.checker', 'checker') +// .innerJoin('checker.user', 'user') +// .addSelect(['checker.id', ...getPrimaryUserFields('user')]) +// .where('"tsr"."studentId" = :studentId', { studentId }) +// .andWhere('"tsr"."courseTaskId" = :courseTaskId', { courseTaskId }) +// .getMany() +// ).map(c => { +// const author = !c.anonymous +// ? { +// id: c.checker.user.id, +// name: createName(c.checker.user), +// githubId: c.checker.user.githubId, +// discord: c.checker.user.discord, +// } +// : null; +// const [{ dateTime, criteria }] = c.historicalScores.sort((a, b) => b.dateTime - a.dateTime); +// const messages = !c.anonymous +// ? c.messages +// : c.messages.map(message => ({ +// ...message, +// author: message.role === CrossCheckMessageAuthorRole.Reviewer ? null : message.author, +// })); +// return { +// dateTime, +// author, +// messages, +// id: c.id, +// comment: c.comment, +// score: c.score, +// criteria, +// }; +// }); +// const taskSolution = await getRepository(TaskSolution) +// .createQueryBuilder('ts') +// .where('"ts"."studentId" = :studentId', { studentId }) +// .andWhere('"ts"."courseTaskId" = :courseTaskId', { courseTaskId }) +// .getOne(); +// return { url: taskSolution?.url, reviews: comments }; +// } type TaskArtefactInput = { studentId: number; From 4f017082f75fa302791e60732f2d3f267902543e Mon Sep 17 00:00:00 2001 From: Valery Date: Sat, 5 Aug 2023 19:40:57 +0200 Subject: [PATCH 02/10] refactor: remove commented code --- server/src/services/taskResults.service.ts | 45 ---------------------- 1 file changed, 45 deletions(-) diff --git a/server/src/services/taskResults.service.ts b/server/src/services/taskResults.service.ts index ede7415550..7531c56c51 100644 --- a/server/src/services/taskResults.service.ts +++ b/server/src/services/taskResults.service.ts @@ -185,51 +185,6 @@ export async function getCrossCheckData( }; } -// export async function getTaskSolutionFeedback(studentId: number, courseTaskId: number) { -// const comments = ( -// await getRepository(TaskSolutionResult) -// .createQueryBuilder('tsr') -// .select(['tsr.id', 'tsr.comment', 'tsr.anonymous', 'tsr.score', 'tsr.messages', 'tsr.historicalScores']) -// .innerJoin('tsr.checker', 'checker') -// .innerJoin('checker.user', 'user') -// .addSelect(['checker.id', ...getPrimaryUserFields('user')]) -// .where('"tsr"."studentId" = :studentId', { studentId }) -// .andWhere('"tsr"."courseTaskId" = :courseTaskId', { courseTaskId }) -// .getMany() -// ).map(c => { -// const author = !c.anonymous -// ? { -// id: c.checker.user.id, -// name: createName(c.checker.user), -// githubId: c.checker.user.githubId, -// discord: c.checker.user.discord, -// } -// : null; -// const [{ dateTime, criteria }] = c.historicalScores.sort((a, b) => b.dateTime - a.dateTime); -// const messages = !c.anonymous -// ? c.messages -// : c.messages.map(message => ({ -// ...message, -// author: message.role === CrossCheckMessageAuthorRole.Reviewer ? null : message.author, -// })); -// return { -// dateTime, -// author, -// messages, -// id: c.id, -// comment: c.comment, -// score: c.score, -// criteria, -// }; -// }); -// const taskSolution = await getRepository(TaskSolution) -// .createQueryBuilder('ts') -// .where('"ts"."studentId" = :studentId', { studentId }) -// .andWhere('"ts"."courseTaskId" = :courseTaskId', { courseTaskId }) -// .getOne(); -// return { url: taskSolution?.url, reviews: comments }; -// } - type TaskArtefactInput = { studentId: number; courseTaskId: number; From b05e97a4a630cd7eea593d80b73000aa37822668 Mon Sep 17 00:00:00 2001 From: Valery Date: Sat, 5 Aug 2023 20:05:23 +0200 Subject: [PATCH 03/10] feat: migrate get cross check feedback --- client/src/api/api.ts | 35 +++++++++++++++++-- .../pages/Student/CrossCheckSubmit/index.tsx | 18 ++++------ .../components/CrossCheckCriteriaForm.tsx | 18 +++++----- .../components/CrossCheckHistory.tsx | 5 ++- .../SolutionReview/SolutionReview.tsx | 10 +++--- .../criteria/CrossCheckCriteriaModal.tsx | 4 +-- .../components/criteria/PenaltyCriteria.tsx | 6 ++-- .../components/criteria/SubtaskCriteria.tsx | 6 ++-- .../components/criteria/TitleCriteria.tsx | 4 +-- .../course/student/cross-check-review.tsx | 8 ++--- client/src/services/course.ts | 6 ++-- .../dto/cross-check-feedback.dto.ts | 2 +- nestjs/src/spec.json | 30 ++++++++++------ 13 files changed, 93 insertions(+), 59 deletions(-) diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 42d03796f4..9e820ce381 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -2068,6 +2068,37 @@ export const CriteriaDtoTypeEnum = { export type CriteriaDtoTypeEnum = typeof CriteriaDtoTypeEnum[keyof typeof CriteriaDtoTypeEnum]; +/** + * + * @export + * @interface CrossCheckAuthorDto + */ +export interface CrossCheckAuthorDto { + /** + * + * @type {number} + * @memberof CrossCheckAuthorDto + */ + 'id': number; + /** + * + * @type {string} + * @memberof CrossCheckAuthorDto + */ + 'name': string; + /** + * + * @type {string} + * @memberof CrossCheckAuthorDto + */ + 'githubId': string; + /** + * + * @type {Discord} + * @memberof CrossCheckAuthorDto + */ + 'discord': Discord | null; +} /** * * @export @@ -2333,10 +2364,10 @@ export interface CrossCheckSolutionReviewDto { 'criteria'?: Array; /** * - * @type {object} + * @type {CrossCheckAuthorDto} * @memberof CrossCheckSolutionReviewDto */ - 'author': object; + 'author': CrossCheckAuthorDto | null; /** * * @type {number} diff --git a/client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx b/client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx index 48bc313b38..cbacfa13e3 100644 --- a/client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx +++ b/client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx @@ -12,18 +12,11 @@ import { NoSubmissionAvailable } from 'modules/Course/components/NoSubmissionAva import { useRouter } from 'next/router'; import { useEffect, useMemo, useState } from 'react'; import { useAsync } from 'react-use'; -import { - CourseService, - CrossCheckComment, - CrossCheckCriteria, - CrossCheckReview, - Feedback, - TaskSolution, -} from 'services/course'; +import { CourseService, CrossCheckComment, CrossCheckCriteria, CrossCheckReview, TaskSolution } from 'services/course'; import { CoursePageProps } from 'services/models'; import { urlWithIpPattern } from 'services/validators'; import { getQueryString } from 'utils/queryParams-utils'; -import { CrossCheckMessageDtoRoleEnum } from 'api'; +import { CoursesTasksApi, CrossCheckFeedbackDto, CrossCheckMessageDtoRoleEnum } from 'api'; const colSizes = { xs: 24, sm: 18, md: 12, lg: 10 }; @@ -46,8 +39,9 @@ const createUrlRule = (): Rule => { export function CrossCheckSubmit(props: CoursePageProps) { const [form] = Form.useForm(); const courseService = useMemo(() => new CourseService(props.course.id), [props.course.id]); + const teamDistributionApi = useMemo(() => new CoursesTasksApi(), []); const solutionReviewSettings = useSolutionReviewSettings(); - const [feedback, setFeedback] = useState(null); + const [feedback, setFeedback] = useState(null); const [submittedSolution, setSubmittedSolution] = useState(null as TaskSolution | null); const router = useRouter(); const queryTaskId = router.query.taskId ? +router.query.taskId : null; @@ -139,8 +133,8 @@ export function CrossCheckSubmit(props: CoursePageProps) { return; } - const [feedback, submittedSolution, taskDetails] = await Promise.all([ - courseService.getCrossCheckFeedback(props.session.githubId, courseTask.id), + const [{ data: feedback }, submittedSolution, taskDetails] = await Promise.all([ + teamDistributionApi.getCrossCheckFeedback(Number(props.session.githubId), courseTask.id), courseService.getCrossCheckTaskSolution(props.session.githubId, courseTask.id).catch(() => null), courseService.getCrossCheckTaskDetails(courseTask.id), ]); diff --git a/client/src/modules/CrossCheck/components/CrossCheckCriteriaForm.tsx b/client/src/modules/CrossCheck/components/CrossCheckCriteriaForm.tsx index 6b8b2bb51b..73bc8ab8bc 100644 --- a/client/src/modules/CrossCheck/components/CrossCheckCriteriaForm.tsx +++ b/client/src/modules/CrossCheck/components/CrossCheckCriteriaForm.tsx @@ -5,7 +5,7 @@ import { isEqual } from 'lodash'; import { SubtaskCriteria } from './criteria/SubtaskCriteria'; import { TitleCriteria } from './criteria/TitleCriteria'; import { PenaltyCriteria } from './criteria/PenaltyCriteria'; -import { CrossCheckCriteriaData, SolutionReviewType } from 'services/course'; +import { CrossCheckCriteriaDataDto, CrossCheckSolutionReviewDto } from 'api'; const { Text, Title } = Typography; @@ -18,9 +18,9 @@ export interface CriteriaFormProps { maxScore: number | undefined; score: number; setScore: (value: number) => void; - criteriaData: CrossCheckCriteriaData[]; - setCriteriaData: (newData: CrossCheckCriteriaData[]) => void; - initialData: SolutionReviewType; + criteriaData: CrossCheckCriteriaDataDto[]; + setCriteriaData: (newData: CrossCheckCriteriaDataDto[]) => void; + initialData: CrossCheckSolutionReviewDto; setIsSkipped: (value: boolean) => void; isSkipped: boolean; } @@ -39,7 +39,7 @@ export function CrossCheckCriteriaForm({ const maxScoreValue = maxScore ?? 100; const maxScoreLabel = maxScoreValue ? ` (Max ${maxScoreValue} points)` : ''; - const penaltyData: CrossCheckCriteriaData[] = + const penaltyData: CrossCheckCriteriaDataDto[] = criteriaData?.filter(item => item.type.toLowerCase() === TaskType.Penalty) ?? []; useEffect(() => { @@ -57,7 +57,7 @@ export function CrossCheckCriteriaForm({ } }, [criteriaData, initialData]); - function updateCriteriaData(updatedEntry: CrossCheckCriteriaData) { + function updateCriteriaData(updatedEntry: CrossCheckCriteriaDataDto) { const index = criteriaData.findIndex(item => item.key === updatedEntry.key); const updatedData = [...criteriaData]; updatedData.splice(index, 1, updatedEntry); @@ -112,10 +112,10 @@ export function CrossCheckCriteriaForm({ Criteria {criteriaData ?.filter( - (item: CrossCheckCriteriaData) => + (item: CrossCheckCriteriaDataDto) => item.type.toLowerCase() === TaskType.Title || item.type.toLowerCase() === TaskType.Subtask, ) - .map((item: CrossCheckCriteriaData) => { + .map((item: CrossCheckCriteriaDataDto) => { return item.type.toLowerCase() === TaskType.Title ? ( ) : ( @@ -127,7 +127,7 @@ export function CrossCheckCriteriaForm({ {!!penaltyData?.length && ( <> Penalty - {penaltyData?.map((item: CrossCheckCriteriaData) => ( + {penaltyData?.map((item: CrossCheckCriteriaDataDto) => ( ))} diff --git a/client/src/modules/CrossCheck/components/CrossCheckHistory.tsx b/client/src/modules/CrossCheck/components/CrossCheckHistory.tsx index 1d24fe9c08..b7c5e01f1e 100644 --- a/client/src/modules/CrossCheck/components/CrossCheckHistory.tsx +++ b/client/src/modules/CrossCheck/components/CrossCheckHistory.tsx @@ -1,16 +1,15 @@ import { Dispatch, SetStateAction } from 'react'; import { ClockCircleOutlined, EditFilled, EditOutlined } from '@ant-design/icons'; import { Button, Col, notification, Row, Spin, Tag, Timeline, Typography } from 'antd'; -import { SolutionReviewType } from 'services/course'; import { useSolutionReviewSettings } from 'modules/CrossCheck/hooks'; import { markdownLabel } from 'components/Forms/PreparedComment'; import { SolutionReview } from 'modules/CrossCheck/components/SolutionReview'; import { SolutionReviewSettingsPanel } from 'modules/CrossCheck/components/SolutionReviewSettingsPanel'; -import { CrossCheckMessageDtoRoleEnum } from 'api'; +import { CrossCheckMessageDtoRoleEnum, CrossCheckSolutionReviewDto } from 'api'; type CrossCheckHistoryState = { loading: boolean; - data: SolutionReviewType[]; + data: CrossCheckSolutionReviewDto[]; }; type Props = { diff --git a/client/src/modules/CrossCheck/components/SolutionReview/SolutionReview.tsx b/client/src/modules/CrossCheck/components/SolutionReview/SolutionReview.tsx index 8343c8ce26..dc914ab443 100644 --- a/client/src/modules/CrossCheck/components/SolutionReview/SolutionReview.tsx +++ b/client/src/modules/CrossCheck/components/SolutionReview/SolutionReview.tsx @@ -4,7 +4,7 @@ import PreparedComment, { markdownLabel } from 'components/Forms/PreparedComment import { ScoreIcon } from 'components/Icons/ScoreIcon'; import { SolutionReviewSettings } from 'modules/CrossCheck/constants'; import { useEffect, useMemo, useState } from 'react'; -import { CourseService, CrossCheckCriteriaData, SolutionReviewType } from 'services/course'; +import { CourseService } from 'services/course'; import { formatDateTime } from 'services/formatter'; import { CrossCheckCriteriaModal } from '../criteria/CrossCheckCriteriaModal'; import { StudentDiscord } from '../../../../components/StudentDiscord'; @@ -13,7 +13,7 @@ import { Message } from './Message'; import { MessageSendingPanel } from './MessageSendingPanel'; import { UserAvatar } from './UserAvatar'; import { Username } from './Username'; -import { CrossCheckMessageDtoRoleEnum } from 'api'; +import { CrossCheckCriteriaDataDto, CrossCheckMessageDtoRoleEnum, CrossCheckSolutionReviewDto } from 'api'; const { Text } = Typography; @@ -25,7 +25,7 @@ export type SolutionReviewProps = { reviewNumber: number; settings: SolutionReviewSettings; courseTaskId: number | null; - review: SolutionReviewType; + review: CrossCheckSolutionReviewDto; isActiveReview: boolean; isMessageSendingPanelVisible?: boolean; currentRole: CrossCheckMessageDtoRoleEnum; @@ -50,9 +50,9 @@ function SolutionReview(props: SolutionReviewProps) { const { id, dateTime, author, comment, score, messages, criteria } = review; const [isModalVisible, setIsModalVisible] = useState(false); - const [modalData, setModaldata] = useState([]); + const [modalData, setModaldata] = useState([]); - const showModal = (modalData: CrossCheckCriteriaData[]) => { + const showModal = (modalData: CrossCheckCriteriaDataDto[]) => { setIsModalVisible(true); setModaldata(modalData); }; diff --git a/client/src/modules/CrossCheck/components/criteria/CrossCheckCriteriaModal.tsx b/client/src/modules/CrossCheck/components/criteria/CrossCheckCriteriaModal.tsx index ffcd950309..6e8fd8f9cb 100644 --- a/client/src/modules/CrossCheck/components/criteria/CrossCheckCriteriaModal.tsx +++ b/client/src/modules/CrossCheck/components/criteria/CrossCheckCriteriaModal.tsx @@ -1,11 +1,11 @@ import { Modal, Typography } from 'antd'; -import { CrossCheckCriteriaData } from 'services/course'; import { TaskType } from '../CrossCheckCriteriaForm'; +import { CrossCheckCriteriaDataDto } from 'api'; const { Text, Title } = Typography; type Props = { - modalInfo: CrossCheckCriteriaData[] | null; + modalInfo: CrossCheckCriteriaDataDto[] | null; isModalVisible: boolean; showModal: (isModalVisible: boolean) => void; }; diff --git a/client/src/modules/CrossCheck/components/criteria/PenaltyCriteria.tsx b/client/src/modules/CrossCheck/components/criteria/PenaltyCriteria.tsx index 04819451da..ac7502d6fa 100644 --- a/client/src/modules/CrossCheck/components/criteria/PenaltyCriteria.tsx +++ b/client/src/modules/CrossCheck/components/criteria/PenaltyCriteria.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Radio, RadioChangeEvent, Typography } from 'antd'; -import { CrossCheckCriteriaData } from 'services/course'; +import { CrossCheckCriteriaDataDto } from 'api'; const { Text } = Typography; const { Group } = Radio; @@ -11,8 +11,8 @@ enum HasPenalty { } interface PenaltyCriteriaProps { - penaltyData: CrossCheckCriteriaData; - updateCriteriaData: (updatedEntry: CrossCheckCriteriaData) => void; + penaltyData: CrossCheckCriteriaDataDto; + updateCriteriaData: (updatedEntry: CrossCheckCriteriaDataDto) => void; } export function PenaltyCriteria({ penaltyData, updateCriteriaData }: PenaltyCriteriaProps) { diff --git a/client/src/modules/CrossCheck/components/criteria/SubtaskCriteria.tsx b/client/src/modules/CrossCheck/components/criteria/SubtaskCriteria.tsx index c33f9792e6..07b24022b5 100644 --- a/client/src/modules/CrossCheck/components/criteria/SubtaskCriteria.tsx +++ b/client/src/modules/CrossCheck/components/criteria/SubtaskCriteria.tsx @@ -1,15 +1,15 @@ import { Input, Typography, InputNumber, Slider } from 'antd'; import { useMemo } from 'react'; -import { CrossCheckCriteriaData } from 'services/course'; import isUndefined from 'lodash/isUndefined'; import isNil from 'lodash/isNil'; +import { CrossCheckCriteriaDataDto } from 'api'; const { TextArea } = Input; const { Text } = Typography; interface SubtaskCriteriaProps { - subtaskData: CrossCheckCriteriaData; - updateCriteriaData: (updatedEntry: CrossCheckCriteriaData) => void; + subtaskData: CrossCheckCriteriaDataDto; + updateCriteriaData: (updatedEntry: CrossCheckCriteriaDataDto) => void; } export function SubtaskCriteria({ subtaskData, updateCriteriaData }: SubtaskCriteriaProps) { diff --git a/client/src/modules/CrossCheck/components/criteria/TitleCriteria.tsx b/client/src/modules/CrossCheck/components/criteria/TitleCriteria.tsx index e50737f70e..6950d039d4 100644 --- a/client/src/modules/CrossCheck/components/criteria/TitleCriteria.tsx +++ b/client/src/modules/CrossCheck/components/criteria/TitleCriteria.tsx @@ -1,9 +1,9 @@ import { Typography } from 'antd'; +import { CrossCheckCriteriaDataDto } from 'api'; import React from 'react'; -import { CrossCheckCriteriaData } from 'services/course'; interface TitleCriteriaProps { - titleData: CrossCheckCriteriaData; + titleData: CrossCheckCriteriaDataDto; } const { Text } = Typography; diff --git a/client/src/pages/course/student/cross-check-review.tsx b/client/src/pages/course/student/cross-check-review.tsx index 24bc847b75..465b3f91b5 100644 --- a/client/src/pages/course/student/cross-check-review.tsx +++ b/client/src/pages/course/student/cross-check-review.tsx @@ -1,6 +1,6 @@ import { EyeFilled, EyeInvisibleFilled } from '@ant-design/icons'; import { Button, Checkbox, Col, Form, message, Modal, Row, Typography } from 'antd'; -import { CrossCheckMessageDtoRoleEnum, TasksCriteriaApi } from 'api'; +import { CrossCheckCriteriaDataDto, CrossCheckMessageDtoRoleEnum, CrossCheckSolutionReviewDto, TasksCriteriaApi } from 'api'; import { CourseTaskSelect } from 'components/Forms'; import MarkdownInput from 'components/Forms/MarkdownInput'; import { markdownLabel } from 'components/Forms/PreparedComment'; @@ -16,7 +16,7 @@ import { CrossCheckHistory } from 'modules/CrossCheck/components/CrossCheckHisto import { useRouter } from 'next/router'; import { useEffect, useMemo, useState } from 'react'; import { useAsync, useLocalStorage } from 'react-use'; -import { CourseService, CrossCheckCriteriaData, CrossCheckStatus, SolutionReviewType } from 'services/course'; +import { CourseService, CrossCheckStatus } from 'services/course'; import { CoursePageProps } from 'services/models'; import { getQueryString } from 'utils/queryParams-utils'; @@ -44,12 +44,12 @@ function Page(props: CoursePageProps) { const [submissionDisabled, setSubmissionDisabled] = useState(true); const [historicalCommentSelected, setHistoricalCommentSelected] = useState(form.getFieldValue('comment')); const [isUsernameVisible = false, setIsUsernameVisible] = useLocalStorage(LocalStorage.IsUsernameVisible); - const [state, setState] = useState<{ loading: boolean; data: SolutionReviewType[] }>({ + const [state, setState] = useState<{ loading: boolean; data: CrossCheckSolutionReviewDto[] }>({ loading: false, data: [], }); - const [criteriaData, setCriteriaData] = useState([]); + const [criteriaData, setCriteriaData] = useState([]); const [score, setScore] = useState(0); const [isSkipped, setIsSkipped] = useState(false); diff --git a/client/src/services/course.ts b/client/src/services/course.ts index cd22622c12..e4bb79def3 100644 --- a/client/src/services/course.ts +++ b/client/src/services/course.ts @@ -15,6 +15,7 @@ import { EventDto, CriteriaDto, CrossCheckMessageDto, + CrossCheckCriteriaDataDto, } from 'api'; import { optionalQueryString } from 'utils/optionalQueryString'; @@ -31,7 +32,6 @@ export interface CrossCheckMessageAuthor { githubId: string; } - export interface Verification { id: number; createdDate: string; @@ -344,7 +344,7 @@ export class CourseService { anonymous: boolean; review: CrossCheckReview[]; comments: CrossCheckComment[]; - criteria: CrossCheckCriteriaData[]; + criteria: CrossCheckCriteriaDataDto[]; }, ) { await this.axios.post(`/student/${githubId}/task/${courseTaskId}/cross-check/result`, data); @@ -364,7 +364,7 @@ export class CourseService { comment: string; dateTime: number; anonymous: boolean; - criteria: CrossCheckCriteriaData[]; + criteria: CrossCheckCriteriaDataDto[]; }[]; author: { id: number; diff --git a/nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts b/nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts index 6024ab7659..b17de32998 100644 --- a/nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts +++ b/nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts @@ -63,7 +63,7 @@ export class CrossCheckSolutionReviewDto { @ApiProperty({ required: false, type: [CrossCheckCriteriaDataDto] }) public criteria?: CrossCheckCriteriaDataDto[]; - @ApiProperty({ required: true }) + @ApiProperty({ nullable: true, type: CrossCheckAuthorDto }) public author: CrossCheckAuthorDto | null; @ApiProperty({ required: true }) diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json index 06c2780afd..d5c30fd027 100644 --- a/nestjs/src/spec.json +++ b/nestjs/src/spec.json @@ -2833,6 +2833,25 @@ }, "required": ["key", "text", "type"] }, + "Discord": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "username": { "type": "string" }, + "discriminator": { "type": "string" } + }, + "required": ["id", "username", "discriminator"] + }, + "CrossCheckAuthorDto": { + "type": "object", + "properties": { + "id": { "type": "number" }, + "name": { "type": "string" }, + "githubId": { "type": "string" }, + "discord": { "nullable": true, "allOf": [{ "$ref": "#/components/schemas/Discord" }] } + }, + "required": ["id", "name", "githubId", "discord"] + }, "CrossCheckSolutionReviewDto": { "type": "object", "properties": { @@ -2840,7 +2859,7 @@ "dateTime": { "type": "number" }, "comment": { "type": "string" }, "criteria": { "type": "array", "items": { "$ref": "#/components/schemas/CrossCheckCriteriaDataDto" } }, - "author": { "type": "object" }, + "author": { "nullable": true, "allOf": [{ "$ref": "#/components/schemas/CrossCheckAuthorDto" }] }, "score": { "type": "number" }, "messages": { "type": "array", "items": { "$ref": "#/components/schemas/CrossCheckMessageDto" } } }, @@ -3073,15 +3092,6 @@ "minTotalScore" ] }, - "Discord": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "username": { "type": "string" }, - "discriminator": { "type": "string" } - }, - "required": ["id", "username", "discriminator"] - }, "TeamDistributionStudentDto": { "type": "object", "properties": { From 8975bb4b4af71c2aac0e902fe79ea2cf8e39ebcd Mon Sep 17 00:00:00 2001 From: Valery Date: Sat, 5 Aug 2023 20:06:18 +0200 Subject: [PATCH 04/10] fix: prettier --- client/src/pages/course/student/cross-check-review.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/pages/course/student/cross-check-review.tsx b/client/src/pages/course/student/cross-check-review.tsx index 465b3f91b5..58e9089a0f 100644 --- a/client/src/pages/course/student/cross-check-review.tsx +++ b/client/src/pages/course/student/cross-check-review.tsx @@ -1,6 +1,11 @@ import { EyeFilled, EyeInvisibleFilled } from '@ant-design/icons'; import { Button, Checkbox, Col, Form, message, Modal, Row, Typography } from 'antd'; -import { CrossCheckCriteriaDataDto, CrossCheckMessageDtoRoleEnum, CrossCheckSolutionReviewDto, TasksCriteriaApi } from 'api'; +import { + CrossCheckCriteriaDataDto, + CrossCheckMessageDtoRoleEnum, + CrossCheckSolutionReviewDto, + TasksCriteriaApi, +} from 'api'; import { CourseTaskSelect } from 'components/Forms'; import MarkdownInput from 'components/Forms/MarkdownInput'; import { markdownLabel } from 'components/Forms/PreparedComment'; From cb3d16a24be97b9a26f263db3d526eb76c87b7bd Mon Sep 17 00:00:00 2001 From: Valery Date: Sat, 5 Aug 2023 20:26:23 +0200 Subject: [PATCH 05/10] refactor: create transform function --- .../course-cross-checks.service.ts | 102 ++++++++++-------- 1 file changed, 58 insertions(+), 44 deletions(-) diff --git a/nestjs/src/courses/cross-checks/course-cross-checks.service.ts b/nestjs/src/courses/cross-checks/course-cross-checks.service.ts index 8537953616..b313c4c392 100644 --- a/nestjs/src/courses/cross-checks/course-cross-checks.service.ts +++ b/nestjs/src/courses/cross-checks/course-cross-checks.service.ts @@ -257,52 +257,66 @@ export class CourseCrossCheckService { studentId: number, courseTaskId: number, ): Promise { - const comments = ( - await this.TaskSolutionResultRepository.createQueryBuilder('tsr') - .select(['tsr.id', 'tsr.comment', 'tsr.anonymous', 'tsr.score', 'tsr.messages', 'tsr.historicalScores']) - .innerJoin('tsr.checker', 'checker') - .innerJoin('checker.user', 'user') - .addSelect(['checker.id', ...UsersService.getPrimaryUserFields('user')]) - .where('"tsr"."studentId" = :studentId', { studentId }) - .andWhere('"tsr"."courseTaskId" = :courseTaskId', { courseTaskId }) - .getMany() - ).map(taskSolutionResult => { - const author = !taskSolutionResult.anonymous - ? { - id: taskSolutionResult.checker.user.id, - name: PersonDto.getName(taskSolutionResult.checker.user), - githubId: taskSolutionResult.checker.user.githubId, - discord: taskSolutionResult.checker.user.discord, - } - : null; - - const [lastCheck] = taskSolutionResult.historicalScores.sort((a, b) => b.dateTime - a.dateTime); - - if (!lastCheck) { - throw new BadRequestException('No historical scores found'); - } - - const { dateTime, criteria } = lastCheck; - - const messages = !taskSolutionResult.anonymous - ? taskSolutionResult.messages - : taskSolutionResult.messages.map(message => ({ - ...message, - author: message.role === CrossCheckMessageAuthorRole.Reviewer ? null : message.author, - })); + const taskSolutionResults = await this.TaskSolutionResultRepository.createQueryBuilder('tsr') + .select(['tsr.id', 'tsr.comment', 'tsr.anonymous', 'tsr.score', 'tsr.messages', 'tsr.historicalScores']) + .innerJoin('tsr.checker', 'checker') + .innerJoin('checker.user', 'user') + .addSelect(['checker.id', ...UsersService.getPrimaryUserFields('user')]) + .where('"tsr"."studentId" = :studentId', { studentId }) + .andWhere('"tsr"."courseTaskId" = :courseTaskId', { courseTaskId }) + .getMany(); + + return taskSolutionResults.map(this.transformToCrossCheckSolutionReview); + } - return { - dateTime, - author, - messages, - id: taskSolutionResult.id, - comment: taskSolutionResult.comment ?? '', - score: taskSolutionResult.score, - criteria, - }; - }); + private transformToCrossCheckSolutionReview(taskSolutionResult: TaskSolutionResult): CrossCheckSolutionReview { + const author = this.extractAuthor(taskSolutionResult); + const { dateTime, criteria } = this.getLastCheck(taskSolutionResult); + const messages = this.getMessages(taskSolutionResult); + + return { + dateTime, + author, + messages, + id: taskSolutionResult.id, + comment: taskSolutionResult.comment ?? '', + score: taskSolutionResult.score, + criteria, + }; + } + + private extractAuthor(taskSolutionResult: TaskSolutionResult) { + if (taskSolutionResult.anonymous) { + return null; + } + + return { + id: taskSolutionResult.checker.user.id, + name: PersonDto.getName(taskSolutionResult.checker.user), + githubId: taskSolutionResult.checker.user.githubId, + discord: taskSolutionResult.checker.user.discord, + }; + } + + private getLastCheck(taskSolutionResult: TaskSolutionResult) { + const [lastCheck] = taskSolutionResult.historicalScores.sort((a, b) => b.dateTime - a.dateTime); + + if (!lastCheck) { + throw new BadRequestException('No historical scores found'); + } + + return lastCheck; + } + + private getMessages(taskSolutionResult: TaskSolutionResult) { + if (taskSolutionResult.anonymous) { + return taskSolutionResult.messages.map(message => ({ + ...message, + author: message.role === CrossCheckMessageAuthorRole.Reviewer ? null : message.author, + })); + } - return comments; + return taskSolutionResult.messages; } public async getTaskSolution(studentId: number, courseTaskId: number) { From 10770c687b504bcb3049583cdc7f233021c4d5f6 Mon Sep 17 00:00:00 2001 From: Valery Date: Sat, 5 Aug 2023 21:24:51 +0200 Subject: [PATCH 06/10] fix: fix bugs --- .../src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx | 2 +- nestjs/src/courses/cross-checks/course-cross-checks.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx b/client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx index cbacfa13e3..1ee7df9253 100644 --- a/client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx +++ b/client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx @@ -134,7 +134,7 @@ export function CrossCheckSubmit(props: CoursePageProps) { } const [{ data: feedback }, submittedSolution, taskDetails] = await Promise.all([ - teamDistributionApi.getCrossCheckFeedback(Number(props.session.githubId), courseTask.id), + teamDistributionApi.getCrossCheckFeedback(props.course.id, courseTask.id), courseService.getCrossCheckTaskSolution(props.session.githubId, courseTask.id).catch(() => null), courseService.getCrossCheckTaskDetails(courseTask.id), ]); diff --git a/nestjs/src/courses/cross-checks/course-cross-checks.service.ts b/nestjs/src/courses/cross-checks/course-cross-checks.service.ts index b313c4c392..9861684e36 100644 --- a/nestjs/src/courses/cross-checks/course-cross-checks.service.ts +++ b/nestjs/src/courses/cross-checks/course-cross-checks.service.ts @@ -266,7 +266,7 @@ export class CourseCrossCheckService { .andWhere('"tsr"."courseTaskId" = :courseTaskId', { courseTaskId }) .getMany(); - return taskSolutionResults.map(this.transformToCrossCheckSolutionReview); + return taskSolutionResults.map(taskSolutionResult => this.transformToCrossCheckSolutionReview(taskSolutionResult)); } private transformToCrossCheckSolutionReview(taskSolutionResult: TaskSolutionResult): CrossCheckSolutionReview { From 71613ee52ea73374bf81f059ece76cedec2fb85f Mon Sep 17 00:00:00 2001 From: Valery Date: Sat, 5 Aug 2023 21:37:22 +0200 Subject: [PATCH 07/10] fix: tests --- .../courses/cross-checks/course-cross-checks.service.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nestjs/src/courses/cross-checks/course-cross-checks.service.spec.ts b/nestjs/src/courses/cross-checks/course-cross-checks.service.spec.ts index f5c0b945c9..0dae791d5a 100644 --- a/nestjs/src/courses/cross-checks/course-cross-checks.service.spec.ts +++ b/nestjs/src/courses/cross-checks/course-cross-checks.service.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TaskSolutionChecker } from '@entities/taskSolutionChecker'; import { CourseCrossCheckService } from './course-cross-checks.service'; import { TaskSolution } from '@entities/taskSolution'; +import { TaskSolutionResult } from '@entities/taskSolutionResult'; const mockRawData = [ { @@ -45,6 +46,10 @@ describe('CourseCrossCheckService', () => { provide: getRepositoryToken(TaskSolutionChecker), useValue: {}, }, + { + provide: getRepositoryToken(TaskSolutionResult), + useValue: {}, + }, ], }).compile(); From 3acd48b4b31482f91e7256383c07774f02a5c438 Mon Sep 17 00:00:00 2001 From: Valery Date: Sat, 5 Aug 2023 23:12:00 +0200 Subject: [PATCH 08/10] fix: bug --- .../src/courses/cross-checks/course-cross-checks.service.ts | 2 +- .../src/courses/cross-checks/dto/cross-check-feedback.dto.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nestjs/src/courses/cross-checks/course-cross-checks.service.ts b/nestjs/src/courses/cross-checks/course-cross-checks.service.ts index 9861684e36..00cfd2138a 100644 --- a/nestjs/src/courses/cross-checks/course-cross-checks.service.ts +++ b/nestjs/src/courses/cross-checks/course-cross-checks.service.ts @@ -324,7 +324,7 @@ export class CourseCrossCheckService { .createQueryBuilder('ts') .where('"ts"."studentId" = :studentId', { studentId }) .andWhere('"ts"."courseTaskId" = :courseTaskId', { courseTaskId }) - .getOneOrFail(); + .getOne(); return taskSolution; } diff --git a/nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts b/nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts index b17de32998..0449e4c158 100644 --- a/nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts +++ b/nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts @@ -74,9 +74,9 @@ export class CrossCheckSolutionReviewDto { } export class CrossCheckFeedbackDto { - constructor(crossCheckSolutionReviews: CrossCheckSolutionReview[], taskSolution: TaskSolution) { + constructor(crossCheckSolutionReviews: CrossCheckSolutionReview[], taskSolution: TaskSolution | null) { this.reviews = crossCheckSolutionReviews; - this.url = taskSolution.url; + this.url = taskSolution?.url; } @ApiProperty({ required: false }) From d3d101d70ff78b2b6474541a36b0ca3d823ccaf6 Mon Sep 17 00:00:00 2001 From: Valery Date: Mon, 7 Aug 2023 20:28:42 +0200 Subject: [PATCH 09/10] refactor: update controller --- client/src/api/api.ts | 140 +++++++++--------- .../pages/Student/CrossCheckSubmit/index.tsx | 2 +- .../course-cross-checks.controller.ts | 6 +- nestjs/src/spec.json | 4 +- 4 files changed, 76 insertions(+), 76 deletions(-) diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 9e820ce381..38f38a23e2 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -9519,43 +9519,6 @@ export const CoursesTasksApiAxiosParamCreator = function (configuration?: Config - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @param {number} courseId - * @param {number} courseTaskId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getCrossCheckFeedback: async (courseId: number, courseTaskId: number, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'courseId' is not null or undefined - assertParamExists('getCrossCheckFeedback', 'courseId', courseId) - // verify required parameter 'courseTaskId' is not null or undefined - assertParamExists('getCrossCheckFeedback', 'courseTaskId', courseTaskId) - const localVarPath = `/courses/{courseId}/cross-checks/{courseTaskId}/student/feedback` - .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))) - .replace(`{${"courseTaskId"}}`, encodeURIComponent(String(courseTaskId))); - // 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}; @@ -9633,6 +9596,43 @@ export const CoursesTasksApiAxiosParamCreator = function (configuration?: Config + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {number} courseId + * @param {number} courseTaskId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMyCrossCheckFeedbacks: async (courseId: number, courseTaskId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'courseId' is not null or undefined + assertParamExists('getMyCrossCheckFeedbacks', 'courseId', courseId) + // verify required parameter 'courseTaskId' is not null or undefined + assertParamExists('getMyCrossCheckFeedbacks', 'courseTaskId', courseTaskId) + const localVarPath = `/courses/{courseId}/cross-checks/{courseTaskId}/myFeedbacks` + .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))) + .replace(`{${"courseTaskId"}}`, encodeURIComponent(String(courseTaskId))); + // 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}; @@ -9816,17 +9816,6 @@ export const CoursesTasksApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getCrossCheckCsv(courseId, courseTaskId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, - /** - * - * @param {number} courseId - * @param {number} courseTaskId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getCrossCheckFeedback(courseId: number, courseTaskId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getCrossCheckFeedback(courseId, courseTaskId, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, /** * * @param {number} courseId @@ -9845,6 +9834,17 @@ export const CoursesTasksApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getCrossCheckPairs(courseId, pageSize, current, orderBy, orderDirection, checker, student, url, task, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {number} courseId + * @param {number} courseTaskId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getMyCrossCheckFeedbacks(courseId: number, courseTaskId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMyCrossCheckFeedbacks(courseId, courseTaskId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {CheckTasksDeadlineDto} checkTasksDeadlineDto @@ -9955,16 +9955,6 @@ export const CoursesTasksApiFactory = function (configuration?: Configuration, b getCrossCheckCsv(courseId: number, courseTaskId: number, options?: any): AxiosPromise { return localVarFp.getCrossCheckCsv(courseId, courseTaskId, options).then((request) => request(axios, basePath)); }, - /** - * - * @param {number} courseId - * @param {number} courseTaskId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - getCrossCheckFeedback(courseId: number, courseTaskId: number, options?: any): AxiosPromise { - return localVarFp.getCrossCheckFeedback(courseId, courseTaskId, options).then((request) => request(axios, basePath)); - }, /** * * @param {number} courseId @@ -9982,6 +9972,16 @@ export const CoursesTasksApiFactory = function (configuration?: Configuration, b getCrossCheckPairs(courseId: number, pageSize: number, current: number, orderBy?: string, orderDirection?: string, checker?: string, student?: string, url?: string, task?: string, options?: any): AxiosPromise { return localVarFp.getCrossCheckPairs(courseId, pageSize, current, orderBy, orderDirection, checker, student, url, task, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {number} courseId + * @param {number} courseTaskId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMyCrossCheckFeedbacks(courseId: number, courseTaskId: number, options?: any): AxiosPromise { + return localVarFp.getMyCrossCheckFeedbacks(courseId, courseTaskId, options).then((request) => request(axios, basePath)); + }, /** * * @param {CheckTasksDeadlineDto} checkTasksDeadlineDto @@ -10106,18 +10106,6 @@ export class CoursesTasksApi extends BaseAPI { return CoursesTasksApiFp(this.configuration).getCrossCheckCsv(courseId, courseTaskId, options).then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {number} courseId - * @param {number} courseTaskId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesTasksApi - */ - public getCrossCheckFeedback(courseId: number, courseTaskId: number, options?: AxiosRequestConfig) { - return CoursesTasksApiFp(this.configuration).getCrossCheckFeedback(courseId, courseTaskId, options).then((request) => request(this.axios, this.basePath)); - } - /** * * @param {number} courseId @@ -10137,6 +10125,18 @@ export class CoursesTasksApi extends BaseAPI { return CoursesTasksApiFp(this.configuration).getCrossCheckPairs(courseId, pageSize, current, orderBy, orderDirection, checker, student, url, task, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {number} courseId + * @param {number} courseTaskId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CoursesTasksApi + */ + public getMyCrossCheckFeedbacks(courseId: number, courseTaskId: number, options?: AxiosRequestConfig) { + return CoursesTasksApiFp(this.configuration).getMyCrossCheckFeedbacks(courseId, courseTaskId, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {CheckTasksDeadlineDto} checkTasksDeadlineDto diff --git a/client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx b/client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx index 1ee7df9253..56e1ba8e2e 100644 --- a/client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx +++ b/client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx @@ -134,7 +134,7 @@ export function CrossCheckSubmit(props: CoursePageProps) { } const [{ data: feedback }, submittedSolution, taskDetails] = await Promise.all([ - teamDistributionApi.getCrossCheckFeedback(props.course.id, courseTask.id), + teamDistributionApi.getMyCrossCheckFeedbacks(props.course.id, courseTask.id), courseService.getCrossCheckTaskSolution(props.session.githubId, courseTask.id).catch(() => null), courseService.getCrossCheckTaskDetails(courseTask.id), ]); diff --git a/nestjs/src/courses/cross-checks/course-cross-checks.controller.ts b/nestjs/src/courses/cross-checks/course-cross-checks.controller.ts index 8658b3d81e..0a964482bb 100644 --- a/nestjs/src/courses/cross-checks/course-cross-checks.controller.ts +++ b/nestjs/src/courses/cross-checks/course-cross-checks.controller.ts @@ -104,13 +104,13 @@ export class CourseCrossCheckController { res.end(parsedData); } - @Get(':courseTaskId/student/feedback') - @ApiOperation({ operationId: 'getCrossCheckFeedback' }) + @Get(':courseTaskId/myFeedbacks') + @ApiOperation({ operationId: 'getMyCrossCheckFeedbacks' }) @ApiForbiddenResponse() @ApiResponse({ type: CrossCheckFeedbackDto }) @RequiredRoles([CourseRole.Manager, Role.Admin, CourseRole.Student], true) @UseGuards(DefaultGuard, RoleGuard) - public async getCrossCheckFeedback( + public async getMyCrossCheckFeedbacks( @StudentId() studentId: number, @Param('courseId', ParseIntPipe) _courseId: number, @Param('courseTaskId', ParseIntPipe) courseTaskId: number, diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json index d5c30fd027..f4d1663f48 100644 --- a/nestjs/src/spec.json +++ b/nestjs/src/spec.json @@ -687,9 +687,9 @@ "tags": ["courses tasks"] } }, - "/courses/{courseId}/cross-checks/{courseTaskId}/student/feedback": { + "/courses/{courseId}/cross-checks/{courseTaskId}/myFeedbacks": { "get": { - "operationId": "getCrossCheckFeedback", + "operationId": "getMyCrossCheckFeedbacks", "summary": "", "parameters": [ { "name": "courseId", "required": true, "in": "path", "schema": { "type": "number" } }, From dddfae052d81ee9a54e4b071a41a8bdb2d975080 Mon Sep 17 00:00:00 2001 From: Valery Date: Mon, 7 Aug 2023 20:35:39 +0200 Subject: [PATCH 10/10] refactor: update controller --- .../src/courses/cross-checks/course-cross-checks.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nestjs/src/courses/cross-checks/course-cross-checks.controller.ts b/nestjs/src/courses/cross-checks/course-cross-checks.controller.ts index 0a964482bb..b500be0eb2 100644 --- a/nestjs/src/courses/cross-checks/course-cross-checks.controller.ts +++ b/nestjs/src/courses/cross-checks/course-cross-checks.controller.ts @@ -104,7 +104,7 @@ export class CourseCrossCheckController { res.end(parsedData); } - @Get(':courseTaskId/myFeedbacks') + @Get(':courseTaskId/feedbacks/my') @ApiOperation({ operationId: 'getMyCrossCheckFeedbacks' }) @ApiForbiddenResponse() @ApiResponse({ type: CrossCheckFeedbackDto })