diff --git a/api/app/application/solution_comments_repository.py b/api/app/application/solution_comments_repository.py
new file mode 100644
index 0000000..ad9d408
--- /dev/null
+++ b/api/app/application/solution_comments_repository.py
@@ -0,0 +1,39 @@
+from typing import Optional, List
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.domain.models import SolutionComment
+
+
+class SolutionCommentsRepository:
+ def __init__(self, db: AsyncSession):
+ self.db = db
+
+ async def get_by_id(self, comment_id: int) -> Optional[SolutionComment]:
+ query = (
+ select(SolutionComment)
+ .filter_by(id=comment_id)
+ )
+ result = await self.db.execute(query)
+ return result.scalars().first()
+
+ async def get_by_solution_id(self, solution_id: int) -> Optional[List[SolutionComment]]:
+ query = (
+ select(SolutionComment)
+ .filter_by(solution_id=solution_id)
+ .order_by(SolutionComment.created_at)
+ )
+ result = await self.db.execute(query)
+ return result.scalars().all()
+
+ async def create(self, comment: SolutionComment) -> SolutionComment:
+ self.db.add(comment)
+ await self.db.commit()
+ await self.db.refresh(comment)
+ return comment
+
+ async def delete(self, comment: SolutionComment) -> SolutionComment:
+ await self.db.delete(comment)
+ await self.db.commit()
+ return comment
diff --git a/api/app/application/solutions_repository.py b/api/app/application/solutions_repository.py
index f5779a8..37b79fb 100644
--- a/api/app/application/solutions_repository.py
+++ b/api/app/application/solutions_repository.py
@@ -25,6 +25,7 @@ class SolutionsRepository:
.filter_by(task_id=task_id)
.options(
selectinload(Solution.files),
+ selectinload(Solution.solution_comments),
)
)
result = await self.db.execute(query)
@@ -36,6 +37,7 @@ class SolutionsRepository:
.filter_by(task_id=task_id, student_id=student_id)
.options(
selectinload(Solution.files),
+ selectinload(Solution.solution_comments),
)
)
result = await self.db.execute(query)
diff --git a/api/app/controllers/solution_comments_router.py b/api/app/controllers/solution_comments_router.py
new file mode 100644
index 0000000..7d2feb5
--- /dev/null
+++ b/api/app/controllers/solution_comments_router.py
@@ -0,0 +1,55 @@
+from typing import List, Optional
+
+from fastapi import APIRouter, Depends, status, Response
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.database.session import get_db
+from app.domain.entities.solutions import SolutionCommentRead, SolutionCommentCreate
+from app.domain.models import SolutionComment, User
+from app.infrastructure.dependencies import require_auth_user
+from app.infrastructure.solution_comments_service import SolutionCommentsService
+
+solution_comments_router = APIRouter()
+
+
+@solution_comments_router.get(
+ '/solution/{solution_id}/',
+ response_model=List[SolutionCommentRead],
+ summary='Returns all comments for solution',
+ description='Returns all comments for solution',
+)
+async def get_solution_comments(
+ solution_id: int,
+ db: AsyncSession = Depends(get_db),
+ user: User = Depends(require_auth_user),
+):
+ solution_comments_service = SolutionCommentsService(db)
+ return await solution_comments_service.get_by_solution_id(solution_id)
+
+@solution_comments_router.post(
+ '/solution/{solution_id}/',
+ response_model=SolutionCommentRead,
+ summary='Creates a new solution comment',
+ description='Creates a new solution comment',
+)
+async def create_solution_comment(
+ solution_id: int,
+ solution_comment: SolutionCommentCreate,
+ db: AsyncSession = Depends(get_db),
+ user: User = Depends(require_auth_user),
+):
+ solution_comments_service = SolutionCommentsService(db)
+ return await solution_comments_service.create(solution_comment, user, solution_id)
+
+@solution_comments_router.delete(
+ '/{comment_id}/',
+ summary='Deletes a solution comment',
+ description='Deletes a solution comment',
+)
+async def delete_solution_comment(
+ comment_id: int,
+ db: AsyncSession = Depends(get_db),
+ user: User = Depends(require_auth_user),
+):
+ solution_comments_service = SolutionCommentsService(db)
+ return await solution_comments_service.delete(comment_id)
diff --git a/api/app/domain/entities/solutions.py b/api/app/domain/entities/solutions.py
index 803daa7..e85e7e0 100644
--- a/api/app/domain/entities/solutions.py
+++ b/api/app/domain/entities/solutions.py
@@ -27,14 +27,33 @@ class SolutionAfterCreate(SolutionBase):
from_attributes = True
-class SolutionRead(SolutionAfterCreate):
- created_at: datetime
+class AssessmentCreate(BaseModel):
+ assessment: int = Field(...)
- files: Optional[List[ReadSolutionFile]] = []
+
+class SolutionCommentBase(BaseModel):
+ comment_text: str = Field(...)
+
+
+class SolutionCommentCreate(SolutionCommentBase):
+ pass
+
+
+class SolutionCommentRead(SolutionCommentBase):
+ comment_autor_id: int
+ solution_id: int
+
+ comment_autor: UserRead
class Config:
from_attributes = True
-class AssessmentCreate(BaseModel):
- assessment: int = Field(...)
+class SolutionRead(SolutionAfterCreate):
+ created_at: datetime
+
+ files: Optional[List[ReadSolutionFile]] = []
+ solution_comments: Optional[List[SolutionCommentRead]] = []
+
+ class Config:
+ from_attributes = True
diff --git a/api/app/domain/models/solution_comments.py b/api/app/domain/models/solution_comments.py
index 9bec13e..377cd9f 100644
--- a/api/app/domain/models/solution_comments.py
+++ b/api/app/domain/models/solution_comments.py
@@ -1,9 +1,6 @@
-from typing import List
-
-from sqlalchemy import String, ForeignKey
+from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
-
from app.domain.models.base import RootTable
@@ -15,5 +12,5 @@ class SolutionComment(RootTable):
comment_autor_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False)
solution_id: Mapped[int] = mapped_column(ForeignKey('solutions.id'), nullable=False)
- comment_autor: Mapped['User'] = relationship('User', back_populates='solution_comments')
+ comment_autor: Mapped['User'] = relationship('User', back_populates='solution_comments', lazy='joined')
solution: Mapped['Solution'] = relationship('Solution', back_populates='solution_comments')
\ No newline at end of file
diff --git a/api/app/infrastructure/solution_comments_service.py b/api/app/infrastructure/solution_comments_service.py
new file mode 100644
index 0000000..85fb08f
--- /dev/null
+++ b/api/app/infrastructure/solution_comments_service.py
@@ -0,0 +1,75 @@
+from http.client import HTTPException
+from typing import Optional, List
+
+from fastapi import HTTPException, status
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.application.solution_comments_repository import SolutionCommentsRepository
+from app.application.solutions_repository import SolutionsRepository
+from app.domain.entities.solutions import SolutionCommentRead, SolutionCommentCreate
+from app.domain.models import User, SolutionComment
+
+
+class SolutionCommentsService:
+ def __init__(self, db: AsyncSession):
+ self.solution_comments_repository = SolutionCommentsRepository(db)
+ self.solutions_repository = SolutionsRepository(db)
+
+ async def get_by_id(self, comment_id) -> Optional[SolutionCommentRead]:
+ comment_model = await self.solution_comments_repository.get_by_id(comment_id)
+
+ if comment_model is None:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Комментарий не найден"
+ )
+
+ return SolutionCommentRead.model_validate(comment_model)
+
+ async def get_by_solution_id(self, solution_id) -> Optional[List[SolutionCommentRead]]:
+ 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="Решение не найдено"
+ )
+
+ comments = await self.solution_comments_repository.get_by_solution_id(solution_id)
+ response = []
+ for comment in comments:
+ response.append(SolutionCommentRead.model_validate(comment))
+
+ return response
+
+ async def create(
+ self, comment: SolutionCommentCreate, user: User, solution_id: int
+ ) -> Optional[SolutionCommentRead]:
+ 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="Решение не найдено"
+ )
+
+ comment_model = SolutionComment(
+ comment_text=comment.comment_text,
+ comment_autor_id=user.id,
+ solution_id=solution_model.id,
+ )
+
+ comment_model = await self.solution_comments_repository.create(comment_model)
+
+ return SolutionCommentRead.model_validate(comment_model)
+
+ async def delete(self, comment_id: int) -> None:
+ comment_model = await self.solution_comments_repository.get_by_id(comment_id)
+
+ if comment_model is None:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Комментарий не найден"
+ )
+
+ await self.solution_comments_repository.delete(comment_model)
diff --git a/api/app/main.py b/api/app/main.py
index ec663bb..57b5862 100644
--- a/api/app/main.py
+++ b/api/app/main.py
@@ -6,6 +6,7 @@ from app.controllers.courses_router import courses_router
from app.controllers.lessons_router import lessons_router
from app.controllers.register_router import register_router
from app.controllers.roles_router import roles_router
+from app.controllers.solution_comments_router import solution_comments_router
from app.controllers.solutions_router import solution_router
from app.controllers.statuses_router import statuses_router
from app.controllers.tasks_router import tasks_router
@@ -30,6 +31,7 @@ def start_app():
api_app.include_router(lessons_router, prefix=f'{settings.prefix}/lessons', tags=['lessons'])
api_app.include_router(register_router, prefix=f'{settings.prefix}/register', tags=['register'])
api_app.include_router(roles_router, prefix=f'{settings.prefix}/roles', tags=['roles'])
+ api_app.include_router(solution_comments_router, prefix=f'{settings.prefix}/comments', tags=['comments'])
api_app.include_router(solution_router, prefix=f'{settings.prefix}/solutions', tags=['solutions'])
api_app.include_router(statuses_router, prefix=f'{settings.prefix}/statuses', tags=['statuses'])
api_app.include_router(tasks_router, prefix=f'{settings.prefix}/tasks', tags=['tasks'])
diff --git a/web/src/Api/commentsApi.js b/web/src/Api/commentsApi.js
new file mode 100644
index 0000000..8e27398
--- /dev/null
+++ b/web/src/Api/commentsApi.js
@@ -0,0 +1,39 @@
+import {createApi} from "@reduxjs/toolkit/query/react";
+import {baseQueryWithAuth} from "./baseQuery.js";
+
+
+export const commentsApi = createApi({
+ reducerPath: "commentsApi",
+ baseQuery: baseQueryWithAuth,
+ tagTypes: ["comments"],
+ endpoints: (builder) => ({
+ getAllCommentsBySolutionId: builder.query({
+ query: (solutionId) => ({
+ url: `/comments/solution/${solutionId}/`,
+ method: "GET"
+ }),
+ providesTags: ["comments"],
+ }),
+ createComment: builder.mutation({
+ query: ({solutionId, comment}) => ({
+ url: `/comments/solution/${solutionId}/`,
+ method: "POST",
+ body: comment,
+ }),
+ invalidatesTags: ["comments"],
+ }),
+ deleteComment: builder.mutation({
+ query: (commentId) => ({
+ url: `/comments/${commentId}/`,
+ method: "DELETE",
+ }),
+ invalidatesTags: ["comments"],
+ }),
+ })
+});
+
+export const {
+ useGetAllCommentsBySolutionIdQuery,
+ useCreateCommentMutation,
+ useDeleteCommentMutation,
+} = commentsApi;
\ No newline at end of file
diff --git a/web/src/Api/solutionsApi.js b/web/src/Api/solutionsApi.js
index 9731783..93cdc95 100644
--- a/web/src/Api/solutionsApi.js
+++ b/web/src/Api/solutionsApi.js
@@ -5,21 +5,21 @@ import {baseQueryWithAuth} from "./baseQuery.js";
export const solutionsApi = createApi({
reducerPath: "solutionsApi",
baseQuery: baseQueryWithAuth,
- tagTypes: ["lesson"],
+ tagTypes: ["solution"],
endpoints: (builder) => ({
getTaskSolutions: builder.query({
query: (taskId) => ({
url: `/solutions/task/${taskId}/`,
method: "GET"
}),
- providesTags: ["lesson"],
+ providesTags: ["solution"],
}),
getTaskStudentSolutions: builder.query({
query: ({taskId, studentId}) => ({
url: `/solutions/task/${taskId}/student/${studentId}/`,
method: "GET"
}),
- providesTags: ["lesson"],
+ providesTags: ["solution"],
}),
createSolution: builder.mutation({
query: ({taskId, solution}) => ({
@@ -27,21 +27,21 @@ export const solutionsApi = createApi({
method: "POST",
body: solution,
}),
- invalidatesTags: ["lesson"],
+ invalidatesTags: ["solution"],
}),
deleteSolution: builder.mutation({
query: (solutionId) => ({
url: `/solutions/${solutionId}/`,
method: "DELETE",
}),
- invalidatesTags: ["lesson"],
+ invalidatesTags: ["solution"],
}),
getSolutionFilesList: builder.query({
query: (solutionId) => ({
url: `/solutions/files/${solutionId}/`,
method: "GET"
}),
- providesTags: ["lesson"],
+ providesTags: ["solution"],
}),
uploadFile: builder.mutation({
query: ({solutionId, fileData}) => {
@@ -57,7 +57,7 @@ export const solutionsApi = createApi({
body: formData,
};
},
- invalidatesTags: ["task"],
+ invalidatesTags: ["solution"],
}),
createAssessment: builder.mutation({
query: ({solutionId, assessment}) => ({
@@ -65,7 +65,7 @@ export const solutionsApi = createApi({
method: "POST",
body: assessment,
}),
- invalidatesTags: ["lesson"],
+ invalidatesTags: ["solution"],
}),
}),
});
diff --git a/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/ViewTaskModal.jsx b/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/ViewTaskModal.jsx
index 7979806..545949c 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, Input, InputNumber,
+ Form, Input, InputNumber, List,
Modal,
Popconfirm,
Row,
@@ -51,6 +51,8 @@ const ViewTaskModal = () => {
allSolutions,
onAssessmentFinish,
assessmentForm,
+ onCommentSubmit,
+ commentForm
} = useViewTaskModal();
return (
@@ -228,6 +230,91 @@ const ViewTaskModal = () => {
) : (