diff --git a/api/app/application/solutions_repository.py b/api/app/application/solutions_repository.py index d768965..f5779a8 100644 --- a/api/app/application/solutions_repository.py +++ b/api/app/application/solutions_repository.py @@ -23,6 +23,9 @@ class SolutionsRepository: query = ( select(Solution) .filter_by(task_id=task_id) + .options( + selectinload(Solution.files), + ) ) result = await self.db.execute(query) return result.scalars().all() @@ -44,6 +47,11 @@ class SolutionsRepository: await self.db.refresh(solution) return solution + async def update(self, solution: Solution) -> Solution: + await self.db.merge(solution) + await self.db.commit() + return solution + async def delete(self, solution: Solution) -> Solution: await self.db.delete(solution) await self.db.commit() diff --git a/api/app/controllers/solutions_router.py b/api/app/controllers/solutions_router.py index 8ef7550..d1e1eb4 100644 --- a/api/app/controllers/solutions_router.py +++ b/api/app/controllers/solutions_router.py @@ -6,9 +6,9 @@ from starlette.responses import FileResponse from app.database.session import get_db from app.domain.entities.solution_files import ReadSolutionFile -from app.domain.entities.solutions import SolutionCreate, SolutionRead, SolutionAfterCreate +from app.domain.entities.solutions import SolutionCreate, SolutionRead, SolutionAfterCreate, AssessmentCreate from app.domain.models import User -from app.infrastructure.dependencies import require_auth_user +from app.infrastructure.dependencies import require_auth_user, require_teacher from app.infrastructure.solution_files_service import SolutionFilesService from app.infrastructure.solutions_service import SolutionsService @@ -123,3 +123,20 @@ async def upload_file( ): task_files_service = SolutionFilesService(db) return await task_files_service.upload_file(task_id, file) + + +@solution_router.post( + '/assessment/{solution_id}/', + status_code=status.HTTP_204_NO_CONTENT, + summary='Set assessment for solution', + description='Set assessment for solution', +) +async def create_assessment( + solution_id: int, + assessment_data: AssessmentCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_teacher), +): + solutions_service = SolutionsService(db) + await solutions_service.create_assessment(solution_id, assessment_data, current_user) + return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/api/app/domain/entities/solutions.py b/api/app/domain/entities/solutions.py index d11620c..803daa7 100644 --- a/api/app/domain/entities/solutions.py +++ b/api/app/domain/entities/solutions.py @@ -34,3 +34,7 @@ class SolutionRead(SolutionAfterCreate): class Config: from_attributes = True + + +class AssessmentCreate(BaseModel): + assessment: int = Field(...) diff --git a/api/app/infrastructure/solutions_service.py b/api/app/infrastructure/solutions_service.py index 3ae9ad8..7a2c9f8 100644 --- a/api/app/infrastructure/solutions_service.py +++ b/api/app/infrastructure/solutions_service.py @@ -8,7 +8,7 @@ from app.application.solution_files_repository import SolutionFilesRepository from app.application.solutions_repository import SolutionsRepository from app.application.tasks_repository import TasksRepository from app.application.users_repository import UsersRepository -from app.domain.entities.solutions import SolutionRead, SolutionCreate, SolutionAfterCreate +from app.domain.entities.solutions import SolutionRead, SolutionCreate, SolutionAfterCreate, AssessmentCreate from app.domain.models import User, Solution from app.settings import Settings @@ -67,6 +67,20 @@ class SolutionsService: return response + async def create_assessment(self, solution_id: int, assessment: AssessmentCreate, user: User) -> None: + solution_model = await self.solutions_repository.get_by_id(solution_id) + + if solution_model is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Такого решения не найдено" + ) + + solution_model.assessment = assessment.assessment + solution_model.assessment_autor_id = user.id + + await self.solutions_repository.update(solution_model) + async def create(self, solution: SolutionCreate, creator: User, task_id: int) -> SolutionAfterCreate: task_model = await self.tasks_repository.get_by_id(task_id) diff --git a/web/src/Api/solutionsApi.js b/web/src/Api/solutionsApi.js index 9dc96b2..9731783 100644 --- a/web/src/Api/solutionsApi.js +++ b/web/src/Api/solutionsApi.js @@ -59,6 +59,14 @@ export const solutionsApi = createApi({ }, invalidatesTags: ["task"], }), + createAssessment: builder.mutation({ + query: ({solutionId, assessment}) => ({ + url: `/solutions/assessment/${solutionId}/`, + method: "POST", + body: assessment, + }), + invalidatesTags: ["lesson"], + }), }), }); @@ -69,5 +77,6 @@ export const { useDeleteSolutionMutation, useGetSolutionFilesListQuery, useUploadFileMutation, + useCreateAssessmentMutation, } = solutionsApi; diff --git a/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/ViewTaskModal.jsx b/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/ViewTaskModal.jsx index d63d9d5..7979806 100644 --- a/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/ViewTaskModal.jsx +++ b/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/ViewTaskModal.jsx @@ -4,7 +4,7 @@ import { Col, Collapse, Divider, Empty, Flex, - Form, + Form, Input, InputNumber, Modal, Popconfirm, Row, @@ -47,7 +47,10 @@ const ViewTaskModal = () => { handleRemoveFile, handleOk, draftFiles, - handleDeleSolution + handleDeleSolution, + allSolutions, + onAssessmentFinish, + assessmentForm, } = useViewTaskModal(); return ( @@ -214,9 +217,9 @@ const ViewTaskModal = () => { onClick={() => downloadFile(file.id, file.filename)} loading={downloadingFiles[file.id]} > - - {file.filename} ({(file.file_size / 1024 / 1024).toFixed(2)} МБ) - + + {file.filename} + ))} @@ -259,9 +262,116 @@ const ViewTaskModal = () => { ) : [ROLES.ADMIN, ROLES.TEACHER].includes(currentUser?.role?.title) && ( - <> - )} + + Присланные решения + {allSolutions.length > 0 ? ( + + {allSolutions.map((solution) => ( + + Решение + от {new Date(solution.created_at).toLocaleString("ru-RU")} + {solution.assessment !== null ? ( + = 80 ? "green" : solution.assessment >= 60 ? "orange" : "red"}> + Оценка: {solution.assessment} / 100 + + ) : ( + Ждет проверки + )} + + } + extra={ + solution.assessment !== null && ( + + Проверено: {solution.assessment_autor?.first_name} {solution.assessment_autor?.last_name} + + ) + } + > +
+ Ответ: +
Текст ответа отсутствует"}} + /> +
+ {solution.files && solution.files.length > 0 ? ( +
+ Прикреплённые файлы: +
+ {solution.files.map((file) => ( + + ))} +
+
+ ) : ( + Файлы не прикреплены + )} + Оценка +
{ + onAssessmentFinish(solution.id) + }}> + + + + + + +
+ + ))} + + ) : ( + + )} + + )} +