diff --git a/client/src/api/api.ts b/client/src/api/api.ts index ec104fd7d..fdb5aa519 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -3777,6 +3777,31 @@ export interface MentorRegistryDto { */ 'comment': string | null; } +/** + * + * @export + * @interface MentorReviewAssignDto + */ +export interface MentorReviewAssignDto { + /** + * + * @type {number} + * @memberof MentorReviewAssignDto + */ + 'courseTaskId': number; + /** + * + * @type {number} + * @memberof MentorReviewAssignDto + */ + 'mentorId': number; + /** + * + * @type {number} + * @memberof MentorReviewAssignDto + */ + 'studentId': number; +} /** * * @export @@ -3790,11 +3815,17 @@ export interface MentorReviewDto { */ 'id': number; /** - * Task name + * Course task name * @type {string} * @memberof MentorReviewDto */ 'taskName': string; + /** + * Course task id + * @type {number} + * @memberof MentorReviewDto + */ + 'taskId': number; /** * Task solution url * @type {string} @@ -3831,6 +3862,12 @@ export interface MentorReviewDto { * @memberof MentorReviewDto */ 'student': string; + /** + * Student id + * @type {number} + * @memberof MentorReviewDto + */ + 'studentId': number; /** * Task solution review date * @type {string} @@ -13379,6 +13416,45 @@ export class GratitudesApi extends BaseAPI { */ export const MentorReviewsApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @param {number} courseId + * @param {MentorReviewAssignDto} mentorReviewAssignDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + assignReviewer: async (courseId: number, mentorReviewAssignDto: MentorReviewAssignDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'courseId' is not null or undefined + assertParamExists('assignReviewer', 'courseId', courseId) + // verify required parameter 'mentorReviewAssignDto' is not null or undefined + assertParamExists('assignReviewer', 'mentorReviewAssignDto', mentorReviewAssignDto) + const localVarPath = `/course/{courseId}/mentor-reviews` + .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(mentorReviewAssignDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {string} current @@ -13456,6 +13532,17 @@ export const MentorReviewsApiAxiosParamCreator = function (configuration?: Confi export const MentorReviewsApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = MentorReviewsApiAxiosParamCreator(configuration) return { + /** + * + * @param {number} courseId + * @param {MentorReviewAssignDto} mentorReviewAssignDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async assignReviewer(courseId: number, mentorReviewAssignDto: MentorReviewAssignDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.assignReviewer(courseId, mentorReviewAssignDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {string} current @@ -13482,6 +13569,16 @@ export const MentorReviewsApiFp = function(configuration?: Configuration) { export const MentorReviewsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = MentorReviewsApiFp(configuration) return { + /** + * + * @param {number} courseId + * @param {MentorReviewAssignDto} mentorReviewAssignDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + assignReviewer(courseId: number, mentorReviewAssignDto: MentorReviewAssignDto, options?: any): AxiosPromise { + return localVarFp.assignReviewer(courseId, mentorReviewAssignDto, options).then((request) => request(axios, basePath)); + }, /** * * @param {string} current @@ -13507,6 +13604,18 @@ export const MentorReviewsApiFactory = function (configuration?: Configuration, * @extends {BaseAPI} */ export class MentorReviewsApi extends BaseAPI { + /** + * + * @param {number} courseId + * @param {MentorReviewAssignDto} mentorReviewAssignDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof MentorReviewsApi + */ + public assignReviewer(courseId: number, mentorReviewAssignDto: MentorReviewAssignDto, options?: AxiosRequestConfig) { + return MentorReviewsApiFp(this.configuration).assignReviewer(courseId, mentorReviewAssignDto, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {string} current diff --git a/client/src/components/UserSearch.tsx b/client/src/components/UserSearch.tsx index 1f62455cc..0a29fee3e 100644 --- a/client/src/components/UserSearch.tsx +++ b/client/src/components/UserSearch.tsx @@ -47,7 +47,7 @@ export function UserSearch(props: UserProps) { {...otherProps} defaultValue={undefined} defaultActiveFirstOption={false} - showArrow={defaultValues ? Boolean(defaultValues.length) : false} + suffixIcon={defaultValues ? Boolean(defaultValues.length) : false} filterOption={false} onSearch={handleSearch} placeholder={defaultValues?.length ?? 0 > 0 ? 'Select...' : 'Search...'} diff --git a/client/src/modules/MentorTasksReview/components/AssignReviewerModal/AssignReviewerModal.tsx b/client/src/modules/MentorTasksReview/components/AssignReviewerModal/AssignReviewerModal.tsx new file mode 100644 index 000000000..962d83c15 --- /dev/null +++ b/client/src/modules/MentorTasksReview/components/AssignReviewerModal/AssignReviewerModal.tsx @@ -0,0 +1,90 @@ +import { Col, Form, Row, Typography } from 'antd'; +import React, { useState } from 'react'; +import { ModalSubmitForm } from 'components/Forms/ModalSubmitForm'; +import { MentorReviewDto, MentorReviewsApi } from 'api'; +import isEmpty from 'lodash/isEmpty'; +import { MentorSearch } from 'components/MentorSearch'; +import { useActiveCourseContext } from 'modules/Course/contexts'; +import { useLoading } from 'components/useLoading'; + +const mentorReviewsApi = new MentorReviewsApi(); + +export interface AssignReviewerModalProps { + review: MentorReviewDto | null; + onClose: () => void; + onSubmit: () => void; +} + +const { Link } = Typography; + +const MODAL_TITLE = 'Assign Reviewer for'; +const SUCCESS_MESSAGE = 'Reviewer has been successfully assigned'; + +function AssignReviewerModal({ review, onClose, onSubmit }: AssignReviewerModalProps) { + const { course } = useActiveCourseContext(); + const [loading, withLoading] = useLoading(false); + + const [submitted, setSubmitted] = useState(false); + const [errorText, setErrorText] = useState(''); + + const courseId = course.id; + const { solutionUrl, taskDescriptionUrl, taskName, student, taskId, studentId } = review || {}; + + const assignReviewer = withLoading(async (courseId, courseTaskId, mentorId, studentId) => { + await mentorReviewsApi.assignReviewer(courseId, { courseTaskId, mentorId, studentId }); + }); + + const handleSubmit = async (values: any) => { + const { mentorId } = values; + try { + if (mentorId) { + await assignReviewer(course.id, taskId, mentorId, studentId); + setSubmitted(true); + onSubmit(); + } + } catch (e: any) { + const error = e.response?.data?.message ?? e.message; + setErrorText(error); + } + }; + + const handleClose = () => { + setErrorText(''); + setSubmitted(false); + onClose(); + }; + + return ( + + + + + + {taskName} + + + + + {solutionUrl} + + + + + + + + + ); +} + +export default AssignReviewerModal; diff --git a/client/src/modules/MentorTasksReview/components/AssignReviewerModal/index.ts b/client/src/modules/MentorTasksReview/components/AssignReviewerModal/index.ts new file mode 100644 index 000000000..a78d45352 --- /dev/null +++ b/client/src/modules/MentorTasksReview/components/AssignReviewerModal/index.ts @@ -0,0 +1 @@ +export { default } from './AssignReviewerModal'; diff --git a/client/src/modules/MentorTasksReview/components/ReviewsTable/index.tsx b/client/src/modules/MentorTasksReview/components/ReviewsTable/index.tsx index 72a9daf80..ea7b31540 100644 --- a/client/src/modules/MentorTasksReview/components/ReviewsTable/index.tsx +++ b/client/src/modules/MentorTasksReview/components/ReviewsTable/index.tsx @@ -1,26 +1,46 @@ import { Table, TablePaginationConfig, TableProps } from 'antd'; import { CourseTaskDto, MentorReviewDto } from 'api'; import { getColumns } from './renderers'; +import AssignReviewerModal from '../AssignReviewerModal'; +import { useState } from 'react'; type Props = { content: MentorReviewDto[]; pagination: false | TablePaginationConfig; handleChange?: TableProps['onChange']; + handleReviewerAssigned: () => void; loading?: boolean; tasks: CourseTaskDto[]; + isManager: boolean; }; -export default function MentorReviewsTable({ content, pagination, handleChange, loading, tasks }: Props) { +export default function MentorReviewsTable({ + content, + pagination, + handleChange, + handleReviewerAssigned, + loading, + tasks, + isManager, +}: Props) { + const [modalData, setModalData] = useState(null); + + const handleClick = (review: MentorReviewDto) => setModalData(review); + const handleClose = () => setModalData(null); + return ( - - showHeader - dataSource={content} - size="small" - columns={getColumns(tasks)} - onChange={handleChange} - rowKey="id" - pagination={pagination} - loading={loading} - /> + <> + + showHeader + dataSource={content} + size="small" + columns={getColumns(tasks, handleClick, isManager)} + onChange={handleChange} + rowKey="id" + pagination={pagination} + loading={loading} + /> + + ); } diff --git a/client/src/modules/MentorTasksReview/components/ReviewsTable/renderers.tsx b/client/src/modules/MentorTasksReview/components/ReviewsTable/renderers.tsx index 51e2085c9..9dbeee23c 100644 --- a/client/src/modules/MentorTasksReview/components/ReviewsTable/renderers.tsx +++ b/client/src/modules/MentorTasksReview/components/ReviewsTable/renderers.tsx @@ -1,3 +1,4 @@ +import Button from 'antd/lib/button'; import { ColumnsType } from 'antd/lib/table'; import { CourseTaskDto, MentorReviewDto } from 'api'; import { GithubUserLink } from 'components/GithubUserLink'; @@ -16,6 +17,7 @@ export enum ColumnKey { Checker = 'checker', ReviewedDate = 'reviewedAt', Score = 'score', + Actions = 'actions', } enum ColumnName { @@ -26,68 +28,90 @@ enum ColumnName { Checker = 'Checker', ReviewedDate = 'Reviewed Date', Score = 'Score', + Actions = 'Actions', } -export const getColumns = (tasks: CourseTaskDto[]): ColumnsType => [ - { - key: ColumnKey.TaskName, - title: ColumnName.TaskName, - dataIndex: ColumnKey.TaskName, - width: '15%', - render: (taskName, review) => renderTask(taskName, review.taskDescriptionUrl), - filters: tasks.map(task => ({ text: task.name, value: task.id })), - }, - { - key: ColumnKey.Student, - title: ColumnName.Student, - dataIndex: ColumnKey.Student, - width: '15%', - render: (_v, review) => , - ...getSearchProps(ColumnKey.Student), - }, - { - key: ColumnKey.SubmittedDate, - title: ColumnName.SubmittedDate, - dataIndex: ColumnKey.SubmittedDate, - width: '15%', - sorter: true, - render: (_v, review) => dateTimeRenderer(review.submittedAt), - }, - { - key: ColumnKey.SubmittedLink, - title: ColumnName.SubmittedLink, - dataIndex: ColumnKey.SubmittedLink, - width: '15%', - render: solutionUrl => ( - - {stringTrimRenderer(solutionUrl)} - - ), - }, - { - key: ColumnKey.Checker, - title: ColumnName.Checker, - dataIndex: ColumnKey.Checker, - width: '15%', - render: checker => (checker ? : null), - }, - { - key: ColumnKey.ReviewedDate, - title: ColumnName.ReviewedDate, - dataIndex: ColumnKey.ReviewedDate, - width: '15%', - sorter: true, - render: (_v, review) => dateTimeRenderer(review.reviewedAt), - }, - { - key: ColumnKey.Score, - title: ColumnName.Score, - dataIndex: ColumnKey.Score, - width: '10%', - render: (_v, review) => ( - <> - {review.score ?? 0} / {review.maxScore} - - ), - }, -]; +export const getColumns = ( + tasks: CourseTaskDto[], + handleClick: (review: MentorReviewDto) => void, + isManager: boolean, +): ColumnsType => { + const columns: ColumnsType = [ + { + key: ColumnKey.TaskName, + title: ColumnName.TaskName, + dataIndex: ColumnKey.TaskName, + width: '15%', + render: (taskName, review) => renderTask(taskName, review.taskDescriptionUrl), + filters: tasks.map(task => ({ text: task.name, value: task.id })), + }, + { + key: ColumnKey.Student, + title: ColumnName.Student, + dataIndex: ColumnKey.Student, + width: '12.5%', + render: (_v, review) => , + ...getSearchProps(ColumnKey.Student), + }, + { + key: ColumnKey.SubmittedDate, + title: ColumnName.SubmittedDate, + dataIndex: ColumnKey.SubmittedDate, + width: '12.5%', + sorter: true, + render: (_v, review) => dateTimeRenderer(review.submittedAt), + }, + { + key: ColumnKey.SubmittedLink, + title: ColumnName.SubmittedLink, + dataIndex: ColumnKey.SubmittedLink, + width: '12.5%', + render: solutionUrl => ( + + {stringTrimRenderer(solutionUrl)} + + ), + }, + { + key: ColumnKey.Checker, + title: ColumnName.Checker, + dataIndex: ColumnKey.Checker, + width: '12.5%', + render: checker => (checker ? : null), + }, + { + key: ColumnKey.ReviewedDate, + title: ColumnName.ReviewedDate, + dataIndex: ColumnKey.ReviewedDate, + width: '12.5%', + sorter: true, + render: (_v, review) => dateTimeRenderer(review.reviewedAt), + }, + { + align: 'right', + key: ColumnKey.Score, + title: ColumnName.Score, + dataIndex: ColumnKey.Score, + width: '10%', + render: (_v, review) => ( + <> + {review.score ?? 0} / {review.maxScore} + + ), + }, + { + align: 'center', + key: ColumnKey.Actions, + title: ColumnName.Actions, + dataIndex: ColumnKey.Actions, + width: '12.5%', + render: (_v, review) => ( + + ), + }, + ]; + + return isManager ? columns : columns.filter(column => column.key !== ColumnKey.Actions); +}; diff --git a/client/src/modules/MentorTasksReview/pages/MentorTasksReview.tsx b/client/src/modules/MentorTasksReview/pages/MentorTasksReview.tsx index 5f5d95e81..70f813fda 100644 --- a/client/src/modules/MentorTasksReview/pages/MentorTasksReview.tsx +++ b/client/src/modules/MentorTasksReview/pages/MentorTasksReview.tsx @@ -3,8 +3,8 @@ import { MentorReviewDto, MentorReviewsApi } from 'api'; import { IPaginationInfo } from 'common/types/pagination'; import { AdminPageLayout } from 'components/PageLayout'; import { useLoading } from 'components/useLoading'; -import { useActiveCourseContext } from 'modules/Course/contexts'; -import { useState } from 'react'; +import { SessionContext, useActiveCourseContext } from 'modules/Course/contexts'; +import { useContext, useMemo, useState } from 'react'; import { useAsync } from 'react-use'; import type { PageProps } from './getServerSideProps'; import MentorReviewsTable from '../components/ReviewsTable'; @@ -12,6 +12,7 @@ import { FilterValue } from 'antd/es/table/interface'; import { ColumnKey } from '../components/ReviewsTable/renderers'; import { SorterResult } from 'antd/lib/table/interface'; import { sortDirectionMap } from './MentorTasksReview.constants'; +import { isCourseManager } from 'domain/user'; const { Text } = Typography; @@ -24,6 +25,9 @@ type ReviewsState = { export const MentorTasksReview = ({ tasks }: PageProps) => { const { courses, course } = useActiveCourseContext(); + const session = useContext(SessionContext); + + const isManager = useMemo(() => isCourseManager(session, course.id), [session, course.id]); const [reviews, setReviews] = useState({ content: [], @@ -58,6 +62,10 @@ export const MentorTasksReview = ({ tasks }: PageProps) => { }, ); + const handleReviewerAssigned = async () => { + await getMentorReviews(reviews.pagination); + }; + useAsync(async () => await getMentorReviews(reviews.pagination), [course]); return ( @@ -67,13 +75,15 @@ export const MentorTasksReview = ({ tasks }: PageProps) => { Submitted tasks {course.name} - You can assign a checker for the student’s task + {isManager && You can assign a checker for the student’s task} ); diff --git a/nestjs/src/courses/mentor-reviews/dto/mentor-review-assign.dto.ts b/nestjs/src/courses/mentor-reviews/dto/mentor-review-assign.dto.ts new file mode 100644 index 000000000..922af5012 --- /dev/null +++ b/nestjs/src/courses/mentor-reviews/dto/mentor-review-assign.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt } from 'class-validator'; + +export class MentorReviewAssignDto { + @ApiProperty() + @IsInt() + courseTaskId: number; + + @ApiProperty() + @IsInt() + mentorId: number; + + @ApiProperty() + @IsInt() + studentId: number; +} diff --git a/nestjs/src/courses/mentor-reviews/dto/mentor-reviews.dto.ts b/nestjs/src/courses/mentor-reviews/dto/mentor-reviews.dto.ts index a98124a99..13888f4ca 100644 --- a/nestjs/src/courses/mentor-reviews/dto/mentor-reviews.dto.ts +++ b/nestjs/src/courses/mentor-reviews/dto/mentor-reviews.dto.ts @@ -7,12 +7,14 @@ export class MentorReviewDto { constructor(taskSolution: TaskSolution) { this.id = taskSolution.id; this.taskName = taskSolution.courseTask.task.name; + this.taskId = taskSolution.courseTask.id; this.solutionUrl = taskSolution.url; this.submittedAt = new Date(taskSolution.createdDate); this.checker = this.getChecker(taskSolution); this.score = taskSolution.student.taskResults?.at(0)?.score; this.maxScore = taskSolution.courseTask.maxScore; this.student = taskSolution.student.user.githubId; + this.studentId = taskSolution.student.id; this.reviewedAt = taskSolution.student.taskResults?.at(0)?.updatedDate ? new Date(taskSolution.student.taskResults.at(0)!.updatedDate) : undefined; @@ -20,17 +22,22 @@ export class MentorReviewDto { } private getChecker(taskSolution: TaskSolution) { - return taskSolution.student.taskResults?.at(0)?.score !== undefined - ? taskSolution.student.taskResults.at(0)?.lastChecker?.githubId - : taskSolution.student.taskChecker?.at(0)?.mentor.user.githubId ?? taskSolution.student.mentor?.user.githubId; + if (taskSolution.student.taskResults?.at(0)?.score !== undefined) { + return taskSolution.student.taskResults.at(0)?.lastChecker?.githubId; + } + + return taskSolution.student.taskChecker?.at(0)?.mentor.user.githubId ?? taskSolution.student.mentor?.user.githubId; } @ApiProperty({ description: 'Task solution id' }) id: number; - @ApiProperty({ description: 'Task name' }) + @ApiProperty({ description: 'Course task name' }) taskName: string; + @ApiProperty({ description: 'Course task id' }) + taskId: number; + @ApiProperty({ description: 'Task solution url' }) solutionUrl: string; @@ -49,6 +56,9 @@ export class MentorReviewDto { @ApiProperty({ description: 'Student github id' }) student: string; + @ApiProperty({ description: 'Student id' }) + studentId: number; + @ApiProperty({ description: 'Task solution review date' }) reviewedAt?: Date; diff --git a/nestjs/src/courses/mentor-reviews/mentor-reviews.controller.ts b/nestjs/src/courses/mentor-reviews/mentor-reviews.controller.ts index b42fbc67f..ace3667e6 100644 --- a/nestjs/src/courses/mentor-reviews/mentor-reviews.controller.ts +++ b/nestjs/src/courses/mentor-reviews/mentor-reviews.controller.ts @@ -1,8 +1,9 @@ import { CourseRole, DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth'; import { MentorReviewsService } from './mentor-reviews.service'; -import { Controller, Get, Param, ParseIntPipe, Query, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Param, ParseIntPipe, Post, Query, UseGuards } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { MentorReviewsDto, MentorReviewsQueryDto } from './dto'; +import { MentorReviewAssignDto } from './dto/mentor-review-assign.dto'; @Controller('course/:courseId/mentor-reviews') @ApiTags('mentor-reviews') @@ -14,7 +15,10 @@ export class MentorReviewsController { @ApiOperation({ operationId: 'getMentorReviews' }) @ApiOkResponse({ type: MentorReviewsDto }) @RequiredRoles([CourseRole.Dementor, CourseRole.Manager, Role.Admin], true) - public async getScore(@Query() query: MentorReviewsQueryDto, @Param('courseId', ParseIntPipe) courseId: number) { + public async getMentorReviews( + @Query() query: MentorReviewsQueryDto, + @Param('courseId', ParseIntPipe) courseId: number, + ) { const page = parseInt(query.current); const limit = parseInt(query.pageSize); const { student, tasks, sortField, sortOrder } = query; @@ -30,4 +34,12 @@ export class MentorReviewsController { return new MentorReviewsDto(mentorReviews); } + + @Post('/') + @ApiOperation({ operationId: 'assignReviewer' }) + @ApiOkResponse({}) + @RequiredRoles([CourseRole.Manager, Role.Admin], true) + public async assignReviewer(@Param('courseId', ParseIntPipe) _courseId: number, @Body() dto: MentorReviewAssignDto) { + return await this.mentorReviewsService.assignReviewer(dto); + } } diff --git a/nestjs/src/courses/mentor-reviews/mentor-reviews.service.ts b/nestjs/src/courses/mentor-reviews/mentor-reviews.service.ts index 00107a82f..a8ab19121 100644 --- a/nestjs/src/courses/mentor-reviews/mentor-reviews.service.ts +++ b/nestjs/src/courses/mentor-reviews/mentor-reviews.service.ts @@ -5,12 +5,16 @@ import { Repository } from 'typeorm'; import { TaskSolution } from '@entities/taskSolution'; import { paginate } from 'src/core/paginate'; import { Checker } from '@entities/courseTask'; +import { TaskChecker } from '../../../../server/src/models'; +import { MentorReviewAssignDto } from './dto/mentor-review-assign.dto'; @Injectable() export class MentorReviewsService { constructor( @InjectRepository(TaskSolution) readonly taskSolutionRepository: Repository, + @InjectRepository(TaskChecker) + readonly taskCheckerRepository: Repository, ) {} private buildMentorReviewsQuery({ @@ -57,6 +61,7 @@ export class MentorReviewsService { 'studentUser.githubId', 'task.name', 'task.descriptionUrl', + 'courseTask.id', 'courseTask.maxScore', 'taskSolution.studentId', 'taskSolution.url', @@ -112,4 +117,27 @@ export class MentorReviewsService { return data; } + + private async findTaskCheckerRecord(courseTaskId: number, studentId: number) { + return await this.taskCheckerRepository.findOne({ + where: { studentId, courseTaskId }, + select: { + id: true, + }, + }); + } + + public async assignReviewer({ courseTaskId, mentorId, studentId }: MentorReviewAssignDto) { + const taskCheckerRecord = await this.findTaskCheckerRecord(courseTaskId, studentId); + + if (taskCheckerRecord) { + return await this.taskCheckerRepository.update(taskCheckerRecord.id, { courseTaskId, mentorId, studentId }); + } + + return await this.taskCheckerRepository.insert({ + courseTaskId, + studentId, + mentorId, + }); + } } diff --git a/nestjs/src/courses/mentors/mentors.service.ts b/nestjs/src/courses/mentors/mentors.service.ts index 2b8e45261..a687b73fa 100644 --- a/nestjs/src/courses/mentors/mentors.service.ts +++ b/nestjs/src/courses/mentors/mentors.service.ts @@ -155,7 +155,7 @@ export class MentorsService { } private getStatus(mentorId: number, resultScore: number) { - // resultScore = 0 should be considered as result + // resultScore = 0 should be considered as a result const hasScore = resultScore !== null; if (!mentorId && !hasScore) { return SolutionItemStatus.RandomTask; @@ -169,10 +169,7 @@ export class MentorsService { return solutions.map(solution => new MentorDashboardDto(solution)); } - private async getRandomSolution( - mentorId: number, - courseId: number, - ): Promise<{ courseTaskId: number; studentId: number }> { + private async getRandomSolution(courseId: number): Promise<{ courseTaskId: number; studentId: number }> { const task = await this.taskSolutionRepository .createQueryBuilder('ts') .leftJoin(TaskResult, 'tr', 'tr."studentId" = ts."studentId" AND tr."courseTaskId" = ts."courseTaskId"') @@ -196,7 +193,7 @@ export class MentorsService { } public async getRandomTask(mentorId: number, courseId: number) { - const { courseTaskId, studentId } = await this.getRandomSolution(mentorId, courseId); + const { courseTaskId, studentId } = await this.getRandomSolution(courseId); if (courseTaskId && studentId) { const checker: Partial = { diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json index 5c27c5475..5516795a4 100644 --- a/nestjs/src/spec.json +++ b/nestjs/src/spec.json @@ -1500,6 +1500,17 @@ } }, "tags": ["mentor-reviews"] + }, + "post": { + "operationId": "assignReviewer", + "summary": "", + "parameters": [{ "name": "courseId", "required": true, "in": "path", "schema": { "type": "number" } }], + "requestBody": { + "required": true, + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MentorReviewAssignDto" } } } + }, + "responses": { "200": { "description": "" } }, + "tags": ["mentor-reviews"] } }, "/users/notifications": { @@ -4046,25 +4057,29 @@ "type": "object", "properties": { "id": { "type": "number", "description": "Task solution id" }, - "taskName": { "type": "string", "description": "Task name" }, + "taskName": { "type": "string", "description": "Course task name" }, + "taskId": { "type": "number", "description": "Course task id" }, "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" }, + "studentId": { "type": "number", "description": "Student id" }, "reviewedAt": { "format": "date-time", "type": "string", "description": "Task solution review date" }, "taskDescriptionUrl": { "type": "string", "description": "Task description url" } }, "required": [ "id", "taskName", + "taskId", "solutionUrl", "submittedAt", "checker", "score", "maxScore", "student", + "studentId", "reviewedAt", "taskDescriptionUrl" ] @@ -4077,6 +4092,15 @@ }, "required": ["content", "pagination"] }, + "MentorReviewAssignDto": { + "type": "object", + "properties": { + "courseTaskId": { "type": "number" }, + "mentorId": { "type": "number" }, + "studentId": { "type": "number" } + }, + "required": ["courseTaskId", "mentorId", "studentId"] + }, "Map": { "type": "object", "properties": {} }, "NotificationUserSettingsDto": { "type": "object",