сделал комментарии к решению
This commit is contained in:
parent
9195f4eacc
commit
be01b5fc22
39
api/app/application/solution_comments_repository.py
Normal file
39
api/app/application/solution_comments_repository.py
Normal file
@ -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
|
||||||
@ -25,6 +25,7 @@ class SolutionsRepository:
|
|||||||
.filter_by(task_id=task_id)
|
.filter_by(task_id=task_id)
|
||||||
.options(
|
.options(
|
||||||
selectinload(Solution.files),
|
selectinload(Solution.files),
|
||||||
|
selectinload(Solution.solution_comments),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result = await self.db.execute(query)
|
result = await self.db.execute(query)
|
||||||
@ -36,6 +37,7 @@ class SolutionsRepository:
|
|||||||
.filter_by(task_id=task_id, student_id=student_id)
|
.filter_by(task_id=task_id, student_id=student_id)
|
||||||
.options(
|
.options(
|
||||||
selectinload(Solution.files),
|
selectinload(Solution.files),
|
||||||
|
selectinload(Solution.solution_comments),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result = await self.db.execute(query)
|
result = await self.db.execute(query)
|
||||||
|
|||||||
55
api/app/controllers/solution_comments_router.py
Normal file
55
api/app/controllers/solution_comments_router.py
Normal file
@ -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)
|
||||||
@ -27,14 +27,33 @@ class SolutionAfterCreate(SolutionBase):
|
|||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
class SolutionRead(SolutionAfterCreate):
|
class AssessmentCreate(BaseModel):
|
||||||
created_at: datetime
|
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:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
class AssessmentCreate(BaseModel):
|
class SolutionRead(SolutionAfterCreate):
|
||||||
assessment: int = Field(...)
|
created_at: datetime
|
||||||
|
|
||||||
|
files: Optional[List[ReadSolutionFile]] = []
|
||||||
|
solution_comments: Optional[List[SolutionCommentRead]] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
from typing import List
|
from sqlalchemy import ForeignKey
|
||||||
|
|
||||||
from sqlalchemy import String, ForeignKey
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
|
||||||
from app.domain.models.base import RootTable
|
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)
|
comment_autor_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False)
|
||||||
solution_id: Mapped[int] = mapped_column(ForeignKey('solutions.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')
|
solution: Mapped['Solution'] = relationship('Solution', back_populates='solution_comments')
|
||||||
75
api/app/infrastructure/solution_comments_service.py
Normal file
75
api/app/infrastructure/solution_comments_service.py
Normal file
@ -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)
|
||||||
@ -6,6 +6,7 @@ from app.controllers.courses_router import courses_router
|
|||||||
from app.controllers.lessons_router import lessons_router
|
from app.controllers.lessons_router import lessons_router
|
||||||
from app.controllers.register_router import register_router
|
from app.controllers.register_router import register_router
|
||||||
from app.controllers.roles_router import roles_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.solutions_router import solution_router
|
||||||
from app.controllers.statuses_router import statuses_router
|
from app.controllers.statuses_router import statuses_router
|
||||||
from app.controllers.tasks_router import tasks_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(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(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(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(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(statuses_router, prefix=f'{settings.prefix}/statuses', tags=['statuses'])
|
||||||
api_app.include_router(tasks_router, prefix=f'{settings.prefix}/tasks', tags=['tasks'])
|
api_app.include_router(tasks_router, prefix=f'{settings.prefix}/tasks', tags=['tasks'])
|
||||||
|
|||||||
39
web/src/Api/commentsApi.js
Normal file
39
web/src/Api/commentsApi.js
Normal file
@ -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;
|
||||||
@ -5,21 +5,21 @@ import {baseQueryWithAuth} from "./baseQuery.js";
|
|||||||
export const solutionsApi = createApi({
|
export const solutionsApi = createApi({
|
||||||
reducerPath: "solutionsApi",
|
reducerPath: "solutionsApi",
|
||||||
baseQuery: baseQueryWithAuth,
|
baseQuery: baseQueryWithAuth,
|
||||||
tagTypes: ["lesson"],
|
tagTypes: ["solution"],
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
getTaskSolutions: builder.query({
|
getTaskSolutions: builder.query({
|
||||||
query: (taskId) => ({
|
query: (taskId) => ({
|
||||||
url: `/solutions/task/${taskId}/`,
|
url: `/solutions/task/${taskId}/`,
|
||||||
method: "GET"
|
method: "GET"
|
||||||
}),
|
}),
|
||||||
providesTags: ["lesson"],
|
providesTags: ["solution"],
|
||||||
}),
|
}),
|
||||||
getTaskStudentSolutions: builder.query({
|
getTaskStudentSolutions: builder.query({
|
||||||
query: ({taskId, studentId}) => ({
|
query: ({taskId, studentId}) => ({
|
||||||
url: `/solutions/task/${taskId}/student/${studentId}/`,
|
url: `/solutions/task/${taskId}/student/${studentId}/`,
|
||||||
method: "GET"
|
method: "GET"
|
||||||
}),
|
}),
|
||||||
providesTags: ["lesson"],
|
providesTags: ["solution"],
|
||||||
}),
|
}),
|
||||||
createSolution: builder.mutation({
|
createSolution: builder.mutation({
|
||||||
query: ({taskId, solution}) => ({
|
query: ({taskId, solution}) => ({
|
||||||
@ -27,21 +27,21 @@ export const solutionsApi = createApi({
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: solution,
|
body: solution,
|
||||||
}),
|
}),
|
||||||
invalidatesTags: ["lesson"],
|
invalidatesTags: ["solution"],
|
||||||
}),
|
}),
|
||||||
deleteSolution: builder.mutation({
|
deleteSolution: builder.mutation({
|
||||||
query: (solutionId) => ({
|
query: (solutionId) => ({
|
||||||
url: `/solutions/${solutionId}/`,
|
url: `/solutions/${solutionId}/`,
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
}),
|
}),
|
||||||
invalidatesTags: ["lesson"],
|
invalidatesTags: ["solution"],
|
||||||
}),
|
}),
|
||||||
getSolutionFilesList: builder.query({
|
getSolutionFilesList: builder.query({
|
||||||
query: (solutionId) => ({
|
query: (solutionId) => ({
|
||||||
url: `/solutions/files/${solutionId}/`,
|
url: `/solutions/files/${solutionId}/`,
|
||||||
method: "GET"
|
method: "GET"
|
||||||
}),
|
}),
|
||||||
providesTags: ["lesson"],
|
providesTags: ["solution"],
|
||||||
}),
|
}),
|
||||||
uploadFile: builder.mutation({
|
uploadFile: builder.mutation({
|
||||||
query: ({solutionId, fileData}) => {
|
query: ({solutionId, fileData}) => {
|
||||||
@ -57,7 +57,7 @@ export const solutionsApi = createApi({
|
|||||||
body: formData,
|
body: formData,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
invalidatesTags: ["task"],
|
invalidatesTags: ["solution"],
|
||||||
}),
|
}),
|
||||||
createAssessment: builder.mutation({
|
createAssessment: builder.mutation({
|
||||||
query: ({solutionId, assessment}) => ({
|
query: ({solutionId, assessment}) => ({
|
||||||
@ -65,7 +65,7 @@ export const solutionsApi = createApi({
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: assessment,
|
body: assessment,
|
||||||
}),
|
}),
|
||||||
invalidatesTags: ["lesson"],
|
invalidatesTags: ["solution"],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
Col, Collapse,
|
Col, Collapse,
|
||||||
Divider,
|
Divider,
|
||||||
Empty, Flex,
|
Empty, Flex,
|
||||||
Form, Input, InputNumber,
|
Form, Input, InputNumber, List,
|
||||||
Modal,
|
Modal,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Row,
|
Row,
|
||||||
@ -51,6 +51,8 @@ const ViewTaskModal = () => {
|
|||||||
allSolutions,
|
allSolutions,
|
||||||
onAssessmentFinish,
|
onAssessmentFinish,
|
||||||
assessmentForm,
|
assessmentForm,
|
||||||
|
onCommentSubmit,
|
||||||
|
commentForm
|
||||||
} = useViewTaskModal();
|
} = useViewTaskModal();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -228,6 +230,91 @@ const ViewTaskModal = () => {
|
|||||||
) : (
|
) : (
|
||||||
<Text type="secondary">Файлы не прикреплены</Text>
|
<Text type="secondary">Файлы не прикреплены</Text>
|
||||||
)}
|
)}
|
||||||
|
<div style={{ marginTop: 32 }}>
|
||||||
|
<Title level={4}>Комментарии к решению</Title>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxHeight: 400,
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: "8px 0",
|
||||||
|
border: "1px solid #f0f0f0",
|
||||||
|
borderRadius: 8,
|
||||||
|
background: "#fafafa",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{solution.solution_comments && solution.solution_comments.length > 0 ? (
|
||||||
|
<List
|
||||||
|
dataSource={solution.solution_comments}
|
||||||
|
renderItem={(comment) => (
|
||||||
|
<List.Item style={{ padding: "12px 16px", borderBottom: "1px solid #f0f0f0" }}>
|
||||||
|
<List.Item.Meta
|
||||||
|
avatar={
|
||||||
|
<Avatar style={{ backgroundColor: "#1890ff" }}>
|
||||||
|
{comment.comment_autor.first_name[0]}
|
||||||
|
{comment.comment_autor.last_name[0]}
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<Text strong>
|
||||||
|
{comment.comment_autor.first_name} {comment.comment_autor.last_name}
|
||||||
|
</Text>
|
||||||
|
{comment.comment_autor.role?.title === "teacher" && (
|
||||||
|
<Tag color="gold" size="small">Преподаватель</Tag>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{new Date(comment.created_at || Date.now()).toLocaleString("ru-RU")}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 8,
|
||||||
|
paddingLeft: 56,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: comment.comment_text}}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description="Пока нет комментариев"
|
||||||
|
style={{ margin: "20px 0" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
onFinish={(values) => onCommentSubmit(solution.id, values.comment)}
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
form={commentForm}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="comment"
|
||||||
|
rules={[{ required: true, message: "Напишите комментарий" }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={3}
|
||||||
|
placeholder="Напишите комментарий к решению..."
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item style={{ marginBottom: 0 }}>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
Отправить комментарий
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
))}
|
))}
|
||||||
</Collapse>
|
</Collapse>
|
||||||
@ -336,7 +423,7 @@ const ViewTaskModal = () => {
|
|||||||
<Text type="secondary">Файлы не прикреплены</Text>
|
<Text type="secondary">Файлы не прикреплены</Text>
|
||||||
)}
|
)}
|
||||||
<Title level={3}>Оценка</Title>
|
<Title level={3}>Оценка</Title>
|
||||||
<Form form={assessmentForm} name={"assessmentForm"} onFinish={() => {
|
<Form form={assessmentForm} onFinish={() => {
|
||||||
onAssessmentFinish(solution.id)
|
onAssessmentFinish(solution.id)
|
||||||
}}>
|
}}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@ -360,6 +447,91 @@ const ViewTaskModal = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
<div style={{ marginTop: 32 }}>
|
||||||
|
<Title level={4}>Комментарии к решению</Title>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxHeight: 400,
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: "8px 0",
|
||||||
|
border: "1px solid #f0f0f0",
|
||||||
|
borderRadius: 8,
|
||||||
|
background: "#fafafa",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{solution.solution_comments && solution.solution_comments.length > 0 ? (
|
||||||
|
<List
|
||||||
|
dataSource={solution.solution_comments}
|
||||||
|
renderItem={(comment) => (
|
||||||
|
<List.Item style={{ padding: "12px 16px", borderBottom: "1px solid #f0f0f0" }}>
|
||||||
|
<List.Item.Meta
|
||||||
|
avatar={
|
||||||
|
<Avatar style={{ backgroundColor: "#1890ff" }}>
|
||||||
|
{comment.comment_autor.first_name[0]}
|
||||||
|
{comment.comment_autor.last_name[0]}
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<Text strong>
|
||||||
|
{comment.comment_autor.first_name} {comment.comment_autor.last_name}
|
||||||
|
</Text>
|
||||||
|
{comment.comment_autor.role?.title === "teacher" && (
|
||||||
|
<Tag color="gold" size="small">Преподаватель</Tag>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{new Date(comment.created_at || Date.now()).toLocaleString("ru-RU")}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 8,
|
||||||
|
paddingLeft: 56,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: comment.comment_text}}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description="Пока нет комментариев"
|
||||||
|
style={{ margin: "20px 0" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
onFinish={(values) => onCommentSubmit(solution.id, values.comment)}
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
form={commentForm}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="comment"
|
||||||
|
rules={[{ required: true, message: "Напишите комментарий" }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={3}
|
||||||
|
placeholder="Напишите комментарий к решению..."
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item style={{ marginBottom: 0 }}>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
Отправить комментарий
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
))}
|
))}
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
useUploadFileMutation
|
useUploadFileMutation
|
||||||
} from "../../../../../Api/solutionsApi.js";
|
} from "../../../../../Api/solutionsApi.js";
|
||||||
import {ROLES} from "../../../../../Core/constants.js";
|
import {ROLES} from "../../../../../Core/constants.js";
|
||||||
|
import {useCreateCommentMutation} from "../../../../../Api/commentsApi.js";
|
||||||
|
|
||||||
|
|
||||||
const useViewTaskModal = () => {
|
const useViewTaskModal = () => {
|
||||||
@ -37,6 +38,34 @@ const useViewTaskModal = () => {
|
|||||||
const [uploadFile] = useUploadFileMutation();
|
const [uploadFile] = useUploadFileMutation();
|
||||||
const [deleteSolution] = useDeleteSolutionMutation();
|
const [deleteSolution] = useDeleteSolutionMutation();
|
||||||
|
|
||||||
|
const [createComment, {
|
||||||
|
isLoading: isCreatingComment,
|
||||||
|
isError: isErrorCreatingComment
|
||||||
|
}] = useCreateCommentMutation();
|
||||||
|
|
||||||
|
const onCommentSubmit = async (solutionId, commentText) => {
|
||||||
|
if (!commentText?.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createComment({
|
||||||
|
solutionId: solutionId,
|
||||||
|
comment: {
|
||||||
|
comment_text: commentText.trim()
|
||||||
|
}
|
||||||
|
}).unwrap();
|
||||||
|
commentForm.resetFields();
|
||||||
|
notification.success({
|
||||||
|
message: "Комментарий отправлен",
|
||||||
|
description: "Ваш комментарий успешно добавлен",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
message: "Ошибка",
|
||||||
|
description: error?.data?.detail || "Не удалось отправить комментарий",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddFile = (file) => {
|
const handleAddFile = (file) => {
|
||||||
const maxSize = 50 * 1024 * 1024; // 50 мегабайт
|
const maxSize = 50 * 1024 * 1024; // 50 мегабайт
|
||||||
if (file.size > maxSize) {
|
if (file.size > maxSize) {
|
||||||
@ -165,6 +194,7 @@ const useViewTaskModal = () => {
|
|||||||
pollingInterval: 5000,
|
pollingInterval: 5000,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [commentForm] = Form.useForm();
|
||||||
const [downloadingFiles, setDownloadingFiles] = useState({});
|
const [downloadingFiles, setDownloadingFiles] = useState({});
|
||||||
|
|
||||||
const downloadFile = async (fileId, fileName) => {
|
const downloadFile = async (fileId, fileName) => {
|
||||||
@ -356,6 +386,7 @@ const useViewTaskModal = () => {
|
|||||||
allSolutions,
|
allSolutions,
|
||||||
onAssessmentFinish,
|
onAssessmentFinish,
|
||||||
assessmentForm,
|
assessmentForm,
|
||||||
|
onCommentSubmit,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {coursesApi} from "../Api/coursesApi.js";
|
|||||||
import {lessonsApi} from "../Api/lessonsApi.js";
|
import {lessonsApi} from "../Api/lessonsApi.js";
|
||||||
import {tasksApi} from "../Api/tasksApi.js";
|
import {tasksApi} from "../Api/tasksApi.js";
|
||||||
import {solutionsApi} from "../Api/solutionsApi.js";
|
import {solutionsApi} from "../Api/solutionsApi.js";
|
||||||
|
import {commentsApi} from "../Api/commentsApi.js";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
@ -35,6 +36,8 @@ export const store = configureStore({
|
|||||||
[tasksApi.reducerPath]: tasksApi.reducer,
|
[tasksApi.reducerPath]: tasksApi.reducer,
|
||||||
|
|
||||||
[solutionsApi.reducerPath]: solutionsApi.reducer,
|
[solutionsApi.reducerPath]: solutionsApi.reducer,
|
||||||
|
|
||||||
|
[commentsApi.reducerPath]: commentsApi.reducer,
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) => (
|
middleware: (getDefaultMiddleware) => (
|
||||||
getDefaultMiddleware().concat(
|
getDefaultMiddleware().concat(
|
||||||
@ -46,6 +49,7 @@ export const store = configureStore({
|
|||||||
lessonsApi.middleware,
|
lessonsApi.middleware,
|
||||||
tasksApi.middleware,
|
tasksApi.middleware,
|
||||||
solutionsApi.middleware,
|
solutionsApi.middleware,
|
||||||
|
commentsApi.middleware,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user