сделал добавление и удаление ответа на задание
This commit is contained in:
parent
1fb12bc7e4
commit
d57f2de2da
38
api/app/application/solution_files_repository.py
Normal file
38
api/app/application/solution_files_repository.py
Normal file
@ -0,0 +1,38 @@
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.domain.models import SolutionFile
|
||||
|
||||
|
||||
class SolutionFilesRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_by_id(self, file_id: int) -> Optional[SolutionFile]:
|
||||
query = (
|
||||
select(SolutionFile)
|
||||
.filter_by(id=file_id)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().first()
|
||||
|
||||
async def get_by_solution_id(self, solution_id: int) -> Sequence[Optional[SolutionFile]]:
|
||||
query = (
|
||||
select(SolutionFile)
|
||||
.filter_by(solution_id=solution_id)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def create(self, solution_file: SolutionFile) -> SolutionFile:
|
||||
self.db.add(solution_file)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(solution_file)
|
||||
return solution_file
|
||||
|
||||
async def delete(self, solution_file: SolutionFile) -> SolutionFile:
|
||||
await self.db.delete(solution_file)
|
||||
await self.db.commit()
|
||||
return solution_file
|
||||
50
api/app/application/solutions_repository.py
Normal file
50
api/app/application/solutions_repository.py
Normal file
@ -0,0 +1,50 @@
|
||||
from typing import Optional, List
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.domain.models import Solution
|
||||
|
||||
|
||||
class SolutionsRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_by_id(self, solution_id: int) -> Optional[Solution]:
|
||||
query = (
|
||||
select(Solution)
|
||||
.filter_by(id=solution_id)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().first()
|
||||
|
||||
async def get_by_task_id(self, task_id: int) -> Optional[List[Solution]]:
|
||||
query = (
|
||||
select(Solution)
|
||||
.filter_by(task_id=task_id)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_by_task_id_and_student_id(self, task_id: int, student_id: int) -> Optional[List[Solution]]:
|
||||
query = (
|
||||
select(Solution)
|
||||
.filter_by(task_id=task_id, student_id=student_id)
|
||||
.options(
|
||||
selectinload(Solution.files),
|
||||
)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def create(self, solution: Solution) -> Solution:
|
||||
self.db.add(solution)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(solution)
|
||||
return solution
|
||||
|
||||
async def delete(self, solution: Solution) -> Solution:
|
||||
await self.db.delete(solution)
|
||||
await self.db.commit()
|
||||
return solution
|
||||
125
api/app/controllers/solutions_router.py
Normal file
125
api/app/controllers/solutions_router.py
Normal file
@ -0,0 +1,125 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Response, File, UploadFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
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.models import User
|
||||
from app.infrastructure.dependencies import require_auth_user
|
||||
from app.infrastructure.solution_files_service import SolutionFilesService
|
||||
from app.infrastructure.solutions_service import SolutionsService
|
||||
|
||||
solution_router = APIRouter()
|
||||
|
||||
|
||||
@solution_router.get(
|
||||
'/task/{task_id}/',
|
||||
response_model=List[SolutionRead],
|
||||
summary='Get all solutions for task',
|
||||
description='Get all solutions for task',
|
||||
)
|
||||
async def get_solutions_by_task(
|
||||
task_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_auth_user),
|
||||
):
|
||||
service = SolutionsService(db)
|
||||
solutions = await service.get_by_task_id(task_id)
|
||||
return solutions
|
||||
|
||||
|
||||
@solution_router.get(
|
||||
'/task/{task_id}/student/{student_id}/',
|
||||
response_model=List[SolutionRead],
|
||||
summary='Get all solutions for task',
|
||||
description='Get all solutions for task',
|
||||
)
|
||||
async def get_solutions_by_task_and_student(
|
||||
task_id: int,
|
||||
student_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_auth_user),
|
||||
):
|
||||
service = SolutionsService(db)
|
||||
solutions = await service.get_by_task_id_and_student_id(task_id, student_id)
|
||||
return solutions
|
||||
|
||||
|
||||
@solution_router.post(
|
||||
'/{task_id}/',
|
||||
response_model=SolutionAfterCreate,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary='Send solution',
|
||||
description='Send solution',
|
||||
)
|
||||
async def create_solution(
|
||||
task_id: int,
|
||||
solution_data: SolutionCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_auth_user),
|
||||
):
|
||||
service = SolutionsService(db)
|
||||
return await service.create(solution_data, current_user, task_id)
|
||||
|
||||
|
||||
@solution_router.delete(
|
||||
'/{solution_id}/',
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary='Delete my solution',
|
||||
description='Delete my solution',
|
||||
)
|
||||
async def delete_solution(
|
||||
solution_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_auth_user),
|
||||
):
|
||||
service = SolutionsService(db)
|
||||
await service.delete(solution_id, current_user)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@solution_router.get(
|
||||
'/file/{file_id}/',
|
||||
response_class=FileResponse,
|
||||
)
|
||||
async def get_file(
|
||||
file_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_auth_user),
|
||||
):
|
||||
task_files_service = SolutionFilesService(db)
|
||||
return await task_files_service.get_file_by_id(file_id)
|
||||
|
||||
|
||||
@solution_router.get(
|
||||
'/files/{solution_id}/',
|
||||
response_model=Optional[List[ReadSolutionFile]],
|
||||
summary='Get a files list by task ID',
|
||||
description='Get a files list by task ID',
|
||||
)
|
||||
async def get_files(
|
||||
solution_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_auth_user),
|
||||
):
|
||||
task_files_service = SolutionFilesService(db)
|
||||
return await task_files_service.get_files_list_by_solution(solution_id)
|
||||
|
||||
|
||||
@solution_router.post(
|
||||
'/files/{task_id}/upload/',
|
||||
response_model=ReadSolutionFile,
|
||||
summary='Upload a file',
|
||||
description='Upload a file',
|
||||
)
|
||||
async def upload_file(
|
||||
task_id: int,
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_auth_user),
|
||||
):
|
||||
task_files_service = SolutionFilesService(db)
|
||||
return await task_files_service.upload_file(task_id, file)
|
||||
@ -0,0 +1,107 @@
|
||||
"""0003 добавил таблицу с комментариями к решению
|
||||
|
||||
Revision ID: f8fd9a27eaa7
|
||||
Revises: 33d77ac5ed79
|
||||
Create Date: 2025-11-29 16:43:22.620248
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'f8fd9a27eaa7'
|
||||
down_revision: Union[str, Sequence[str], None] = '33d77ac5ed79'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('solution_comments',
|
||||
sa.Column('comment_text', sa.String(), nullable=False),
|
||||
sa.Column('comment_autor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('solution_id', sa.Integer(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['comment_autor_id'], ['public.users.id'], ),
|
||||
sa.ForeignKeyConstraint(['solution_id'], ['public.solutions.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='public'
|
||||
)
|
||||
op.drop_constraint(op.f('course_teachers_teacher_id_fkey'), 'course_teachers', type_='foreignkey')
|
||||
op.drop_constraint(op.f('course_teachers_course_id_fkey'), 'course_teachers', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'course_teachers', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'course_teachers', 'users', ['teacher_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('enrollments_student_id_fkey'), 'enrollments', type_='foreignkey')
|
||||
op.drop_constraint(op.f('enrollments_course_id_fkey'), 'enrollments', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'enrollments', 'users', ['student_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'enrollments', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('lesson_files_lesson_id_fkey'), 'lesson_files', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'lesson_files', 'lessons', ['lesson_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('lessons_creator_id_fkey'), 'lessons', type_='foreignkey')
|
||||
op.drop_constraint(op.f('lessons_course_id_fkey'), 'lessons', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'lessons', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'lessons', 'users', ['creator_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('solution_files_solution_id_fkey'), 'solution_files', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'solution_files', 'solutions', ['solution_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('solutions_student_id_fkey'), 'solutions', type_='foreignkey')
|
||||
op.drop_constraint(op.f('solutions_task_id_fkey'), 'solutions', type_='foreignkey')
|
||||
op.drop_constraint(op.f('solutions_assessment_autor_id_fkey'), 'solutions', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'solutions', 'users', ['assessment_autor_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'solutions', 'tasks', ['task_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'solutions', 'users', ['student_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('task_files_task_id_fkey'), 'task_files', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'task_files', 'tasks', ['task_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('tasks_course_id_fkey'), 'tasks', type_='foreignkey')
|
||||
op.drop_constraint(op.f('tasks_creator_id_fkey'), 'tasks', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'tasks', 'users', ['creator_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'tasks', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.drop_constraint(op.f('users_role_id_fkey'), 'users', type_='foreignkey')
|
||||
op.drop_constraint(op.f('users_status_id_fkey'), 'users', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
op.create_foreign_key(None, 'users', 'statuses', ['status_id'], ['id'], source_schema='public', referent_schema='public')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'users', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'users', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('users_status_id_fkey'), 'users', 'statuses', ['status_id'], ['id'])
|
||||
op.create_foreign_key(op.f('users_role_id_fkey'), 'users', 'roles', ['role_id'], ['id'])
|
||||
op.drop_constraint(None, 'tasks', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'tasks', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('tasks_creator_id_fkey'), 'tasks', 'users', ['creator_id'], ['id'])
|
||||
op.create_foreign_key(op.f('tasks_course_id_fkey'), 'tasks', 'courses', ['course_id'], ['id'])
|
||||
op.drop_constraint(None, 'task_files', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('task_files_task_id_fkey'), 'task_files', 'tasks', ['task_id'], ['id'])
|
||||
op.drop_constraint(None, 'solutions', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'solutions', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'solutions', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('solutions_assessment_autor_id_fkey'), 'solutions', 'users', ['assessment_autor_id'], ['id'])
|
||||
op.create_foreign_key(op.f('solutions_task_id_fkey'), 'solutions', 'tasks', ['task_id'], ['id'])
|
||||
op.create_foreign_key(op.f('solutions_student_id_fkey'), 'solutions', 'users', ['student_id'], ['id'])
|
||||
op.drop_constraint(None, 'solution_files', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('solution_files_solution_id_fkey'), 'solution_files', 'solutions', ['solution_id'], ['id'])
|
||||
op.drop_constraint(None, 'lessons', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'lessons', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('lessons_course_id_fkey'), 'lessons', 'courses', ['course_id'], ['id'])
|
||||
op.create_foreign_key(op.f('lessons_creator_id_fkey'), 'lessons', 'users', ['creator_id'], ['id'])
|
||||
op.drop_constraint(None, 'lesson_files', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('lesson_files_lesson_id_fkey'), 'lesson_files', 'lessons', ['lesson_id'], ['id'])
|
||||
op.drop_constraint(None, 'enrollments', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'enrollments', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('enrollments_course_id_fkey'), 'enrollments', 'courses', ['course_id'], ['id'])
|
||||
op.create_foreign_key(op.f('enrollments_student_id_fkey'), 'enrollments', 'users', ['student_id'], ['id'])
|
||||
op.drop_constraint(None, 'course_teachers', schema='public', type_='foreignkey')
|
||||
op.drop_constraint(None, 'course_teachers', schema='public', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('course_teachers_course_id_fkey'), 'course_teachers', 'courses', ['course_id'], ['id'])
|
||||
op.create_foreign_key(op.f('course_teachers_teacher_id_fkey'), 'course_teachers', 'users', ['teacher_id'], ['id'])
|
||||
op.drop_table('solution_comments', schema='public')
|
||||
# ### end Alembic commands ###
|
||||
11
api/app/domain/entities/solution_files.py
Normal file
11
api/app/domain/entities/solution_files.py
Normal file
@ -0,0 +1,11 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ReadSolutionFile(BaseModel):
|
||||
id: int
|
||||
filename: str
|
||||
file_path: str
|
||||
solution_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
36
api/app/domain/entities/solutions.py
Normal file
36
api/app/domain/entities/solutions.py
Normal file
@ -0,0 +1,36 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.domain.entities.solution_files import ReadSolutionFile
|
||||
from app.domain.entities.users import UserRead
|
||||
|
||||
|
||||
class SolutionBase(BaseModel):
|
||||
answer_text: str = Field(...)
|
||||
assessment: Optional[int] = Field(default=None)
|
||||
|
||||
|
||||
class SolutionCreate(SolutionBase):
|
||||
pass
|
||||
|
||||
|
||||
class SolutionAfterCreate(SolutionBase):
|
||||
id: int
|
||||
|
||||
assessment_autor_id: Optional[int] = None
|
||||
assessment_autor: Optional[UserRead] = None
|
||||
student_id: int
|
||||
task_id: int = Field(...)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class SolutionRead(SolutionAfterCreate):
|
||||
created_at: datetime
|
||||
|
||||
files: Optional[List[ReadSolutionFile]] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@ -15,6 +15,7 @@ from app.domain.models.enrollments import Enrollment
|
||||
from app.domain.models.lesson_files import LessonFile
|
||||
from app.domain.models.lessons import Lesson
|
||||
from app.domain.models.roles import Role
|
||||
from app.domain.models.solution_comments import SolutionComment
|
||||
from app.domain.models.solution_files import SolutionFile
|
||||
from app.domain.models.solutions import Solution
|
||||
from app.domain.models.statuses import Status
|
||||
|
||||
19
api/app/domain/models/solution_comments.py
Normal file
19
api/app/domain/models/solution_comments.py
Normal file
@ -0,0 +1,19 @@
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy import String, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
|
||||
from app.domain.models.base import RootTable
|
||||
|
||||
|
||||
class SolutionComment(RootTable):
|
||||
__tablename__ = 'solution_comments'
|
||||
|
||||
comment_text: Mapped[str] = mapped_column(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)
|
||||
|
||||
comment_autor: Mapped['User'] = relationship('User', back_populates='solution_comments')
|
||||
solution: Mapped['Solution'] = relationship('Solution', back_populates='solution_comments')
|
||||
@ -10,14 +10,18 @@ class Solution(RootTable):
|
||||
__tablename__ = 'solutions'
|
||||
|
||||
answer_text: Mapped[str] = mapped_column(nullable=True)
|
||||
assessment_text: Mapped[str] = mapped_column(String(50), nullable=True)
|
||||
assessment: Mapped[int] = mapped_column(nullable=True)
|
||||
|
||||
assessment_autor_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=True)
|
||||
task_id: Mapped[int] = mapped_column(ForeignKey('tasks.id'), nullable=False)
|
||||
student_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False)
|
||||
|
||||
assessment_autor: Mapped['User'] = relationship('User', back_populates='assessments',
|
||||
foreign_keys=[assessment_autor_id])
|
||||
foreign_keys=[assessment_autor_id], lazy='joined')
|
||||
student: Mapped['User'] = relationship('User', back_populates='my_solutions', foreign_keys=[student_id])
|
||||
|
||||
files: Mapped[List['SolutionFile']] = relationship('SolutionFile', back_populates='solution')
|
||||
solution_comments: Mapped[List['SolutionComment']] = relationship(
|
||||
'SolutionComment',
|
||||
back_populates='solution',
|
||||
)
|
||||
|
||||
@ -46,6 +46,11 @@ class User(PhotoAbstract):
|
||||
back_populates='student',
|
||||
foreign_keys=[Solution.student_id],
|
||||
)
|
||||
from app.domain.models.solution_comments import SolutionComment
|
||||
solution_comments: Mapped[List['SolutionComment']] = relationship(
|
||||
'SolutionComment',
|
||||
back_populates='comment_autor',
|
||||
)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
118
api/app/infrastructure/solution_files_service.py
Normal file
118
api/app/infrastructure/solution_files_service.py
Normal file
@ -0,0 +1,118 @@
|
||||
import os
|
||||
import uuid
|
||||
from typing import List
|
||||
|
||||
import aiofiles
|
||||
from fastapi import UploadFile, HTTPException
|
||||
from starlette.responses import FileResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app.application.solution_files_repository import SolutionFilesRepository
|
||||
from app.application.solutions_repository import SolutionsRepository
|
||||
from app.domain.entities.solution_files import ReadSolutionFile
|
||||
from app.domain.models import SolutionFile
|
||||
|
||||
|
||||
class SolutionFilesService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.solution_files_repository = SolutionFilesRepository(db)
|
||||
self.solutions_repository = SolutionsRepository(db)
|
||||
|
||||
async def get_file_by_id(self, file_id: int) -> FileResponse:
|
||||
solution_file = await self.solution_files_repository.get_by_id(file_id)
|
||||
|
||||
if not solution_file:
|
||||
raise HTTPException(404, "Файл с таким ID не найден")
|
||||
|
||||
return FileResponse(
|
||||
solution_file.file_path,
|
||||
media_type=self.get_media_type(solution_file.filename),
|
||||
filename=os.path.basename(solution_file.filename),
|
||||
)
|
||||
|
||||
async def get_files_list_by_solution(self, solution_id: int) -> List[ReadSolutionFile]:
|
||||
solution = await self.solutions_repository.get_by_id(solution_id)
|
||||
|
||||
if solution is None:
|
||||
raise HTTPException(404, "Лекционный материал не найден")
|
||||
|
||||
solution_files = await self.solution_files_repository.get_by_solution_id(solution_id)
|
||||
|
||||
response = []
|
||||
for solution_file in solution_files:
|
||||
response.append(
|
||||
ReadSolutionFile.model_validate(
|
||||
solution_file
|
||||
)
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
async def upload_file(self, solution_id: int, file: UploadFile) -> ReadSolutionFile:
|
||||
solution = await self.solutions_repository.get_by_id(solution_id)
|
||||
|
||||
if solution is None:
|
||||
raise HTTPException(404, "Лекционный материал не найден")
|
||||
|
||||
file_path = await self.save_file(file, f'uploads/solutions/{solution.id}')
|
||||
|
||||
solution_file_model = SolutionFile(
|
||||
filename=file.filename,
|
||||
file_path=file_path,
|
||||
solution_id=solution.id,
|
||||
)
|
||||
|
||||
solution_file_model = await self.solution_files_repository.create(solution_file_model)
|
||||
|
||||
return ReadSolutionFile.model_validate(solution_file_model)
|
||||
|
||||
async def delete_file(self, file_id: int) -> ReadSolutionFile:
|
||||
solution_file = await self.solution_files_repository.get_by_id(file_id)
|
||||
|
||||
if solution_file is None:
|
||||
raise HTTPException(404, "Файл не найден")
|
||||
|
||||
if not os.path.exists(solution_file.file_path):
|
||||
raise HTTPException(404, "Файл не найден на диске")
|
||||
|
||||
if os.path.exists(solution_file.file_path):
|
||||
os.remove(solution_file.file_path)
|
||||
|
||||
solution_file = await self.solution_files_repository.delete(solution_file)
|
||||
|
||||
return ReadSolutionFile.model_validate(solution_file)
|
||||
|
||||
async def save_file(self, file: UploadFile, upload_dir: str = 'uploads/solutions') -> str:
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
filename = self.generate_filename(file)
|
||||
file_path = os.path.join(upload_dir, filename)
|
||||
|
||||
async with aiofiles.open(file_path, 'wb') as out_file:
|
||||
content = await file.read()
|
||||
await out_file.write(content)
|
||||
return file_path
|
||||
|
||||
@staticmethod
|
||||
def generate_filename(file: UploadFile) -> str:
|
||||
return secure_filename(f"{uuid.uuid4()}_{file.filename}")
|
||||
|
||||
@staticmethod
|
||||
def get_media_type(filename: str) -> str:
|
||||
extension = filename.split('.')[-1].lower()
|
||||
if extension in ['jpeg', 'jpg', 'png']:
|
||||
return f"image/{extension}"
|
||||
if extension == 'pdf':
|
||||
return "application/pdf"
|
||||
if extension in ['zip']:
|
||||
return "application/zip"
|
||||
if extension in ['doc', 'docx']:
|
||||
return "application/msword"
|
||||
if extension in ['xls', 'xlsx']:
|
||||
return "application/vnd.ms-excel"
|
||||
if extension in ['ppt', 'pptx']:
|
||||
return "application/vnd.ms-powerpoint"
|
||||
if extension in ['txt']:
|
||||
return "text/plain"
|
||||
|
||||
return "application/octet-stream"
|
||||
118
api/app/infrastructure/solutions_service.py
Normal file
118
api/app/infrastructure/solutions_service.py
Normal file
@ -0,0 +1,118 @@
|
||||
import os
|
||||
from typing import Optional, List
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
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.models import User, Solution
|
||||
from app.settings import Settings
|
||||
|
||||
|
||||
class SolutionsService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.solutions_repository = SolutionsRepository(db)
|
||||
self.tasks_repository = TasksRepository(db)
|
||||
self.users_repository = UsersRepository(db)
|
||||
self.solution_files_repository = SolutionFilesRepository(db)
|
||||
self.settings = Settings()
|
||||
|
||||
async def get_by_task_id(self, task_id: int) -> Optional[List[SolutionRead]]:
|
||||
task_model = await self.tasks_repository.get_by_id(task_id)
|
||||
|
||||
if task_model is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Задание не найдено"
|
||||
)
|
||||
|
||||
solutions = await self.solutions_repository.get_by_task_id(task_id)
|
||||
|
||||
response = []
|
||||
for solution in solutions:
|
||||
response.append(
|
||||
SolutionRead.model_validate(solution)
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
async def get_by_task_id_and_student_id(self, task_id: int, student_id: int) -> Optional[List[SolutionRead]]:
|
||||
task_model = await self.tasks_repository.get_by_id(task_id)
|
||||
|
||||
if task_model is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Задание не найдено"
|
||||
)
|
||||
|
||||
student_model = await self.users_repository.get_by_id(student_id)
|
||||
|
||||
if student_model is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Такой пользователь не найден"
|
||||
)
|
||||
|
||||
solutions = await self.solutions_repository.get_by_task_id_and_student_id(task_id, student_id)
|
||||
|
||||
response = []
|
||||
for solution in solutions:
|
||||
response.append(
|
||||
SolutionRead.model_validate(solution)
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
async def create(self, solution: SolutionCreate, creator: User, task_id: int) -> SolutionAfterCreate:
|
||||
task_model = await self.tasks_repository.get_by_id(task_id)
|
||||
|
||||
if task_model is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Задание не найдено"
|
||||
)
|
||||
|
||||
model_solution = Solution(
|
||||
answer_text=solution.answer_text,
|
||||
assessment=solution.assessment,
|
||||
task_id=task_id,
|
||||
student_id=creator.id,
|
||||
)
|
||||
created_solution = await self.solutions_repository.create(model_solution)
|
||||
return SolutionAfterCreate.model_validate(created_solution)
|
||||
|
||||
async def delete(self, solution_id: int, current_user: User) -> Optional[SolutionRead]:
|
||||
solution = await self.solutions_repository.get_by_id(solution_id)
|
||||
if solution is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail='Такое решение не найдено'
|
||||
)
|
||||
|
||||
is_admin = current_user.role.title == self.settings.root_role_name
|
||||
if solution.student_id != current_user.id and not is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Доступ запрещён"
|
||||
)
|
||||
|
||||
solution_files = await self.solution_files_repository.get_by_solution_id(solution.id)
|
||||
for file in solution_files:
|
||||
solution_file = await self.solution_files_repository.get_by_id(file.id)
|
||||
|
||||
if solution_file is None:
|
||||
raise HTTPException(404, "Файл не найден")
|
||||
|
||||
if not os.path.exists(solution_file.file_path):
|
||||
raise HTTPException(404, "Файл не найден на диске")
|
||||
|
||||
if os.path.exists(solution_file.file_path):
|
||||
os.remove(solution_file.file_path)
|
||||
|
||||
await self.solution_files_repository.delete(solution_file)
|
||||
|
||||
await self.solutions_repository.delete(solution)
|
||||
@ -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.solutions_router import solution_router
|
||||
from app.controllers.statuses_router import statuses_router
|
||||
from app.controllers.tasks_router import tasks_router
|
||||
from app.controllers.users_router import users_router
|
||||
@ -29,6 +30,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_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'])
|
||||
api_app.include_router(users_router, prefix=f'{settings.prefix}/users', tags=['users'])
|
||||
|
||||
73
web/src/Api/solutionsApi.js
Normal file
73
web/src/Api/solutionsApi.js
Normal file
@ -0,0 +1,73 @@
|
||||
import {createApi} from "@reduxjs/toolkit/query/react";
|
||||
import {baseQueryWithAuth} from "./baseQuery.js";
|
||||
|
||||
|
||||
export const solutionsApi = createApi({
|
||||
reducerPath: "solutionsApi",
|
||||
baseQuery: baseQueryWithAuth,
|
||||
tagTypes: ["lesson"],
|
||||
endpoints: (builder) => ({
|
||||
getTaskSolutions: builder.query({
|
||||
query: (taskId) => ({
|
||||
url: `/solutions/task/${taskId}/`,
|
||||
method: "GET"
|
||||
}),
|
||||
providesTags: ["lesson"],
|
||||
}),
|
||||
getTaskStudentSolutions: builder.query({
|
||||
query: ({taskId, studentId}) => ({
|
||||
url: `/solutions/task/${taskId}/student/${studentId}/`,
|
||||
method: "GET"
|
||||
}),
|
||||
providesTags: ["lesson"],
|
||||
}),
|
||||
createSolution: builder.mutation({
|
||||
query: ({taskId, solution}) => ({
|
||||
url: `/solutions/${taskId}/`,
|
||||
method: "POST",
|
||||
body: solution,
|
||||
}),
|
||||
invalidatesTags: ["lesson"],
|
||||
}),
|
||||
deleteSolution: builder.mutation({
|
||||
query: (solutionId) => ({
|
||||
url: `/solutions/${solutionId}/`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
invalidatesTags: ["lesson"],
|
||||
}),
|
||||
getSolutionFilesList: builder.query({
|
||||
query: (solutionId) => ({
|
||||
url: `/solutions/files/${solutionId}/`,
|
||||
method: "GET"
|
||||
}),
|
||||
providesTags: ["lesson"],
|
||||
}),
|
||||
uploadFile: builder.mutation({
|
||||
query: ({solutionId, fileData}) => {
|
||||
if (!(fileData instanceof File)) {
|
||||
throw new Error('Invalid file object');
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileData);
|
||||
return {
|
||||
url: `/solutions/files/${solutionId}/upload/`,
|
||||
method: 'POST',
|
||||
formData: true,
|
||||
body: formData,
|
||||
};
|
||||
},
|
||||
invalidatesTags: ["task"],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetTaskSolutionsQuery,
|
||||
useGetTaskStudentSolutionsQuery,
|
||||
useCreateSolutionMutation,
|
||||
useDeleteSolutionMutation,
|
||||
useGetSolutionFilesListQuery,
|
||||
useUploadFileMutation,
|
||||
} = solutionsApi;
|
||||
|
||||
@ -12,7 +12,7 @@ const useCreateTaskModalForm = ({courseId}) => {
|
||||
} = useSelector((state) => state.tasks);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [createtask, {isLoading}] = useCreateTaskMutation();
|
||||
const [createTask, {isLoading}] = useCreateTaskMutation();
|
||||
const [draftFiles, setDraftFiles] = useState([]);
|
||||
const [uploadFile] = useUploadFileMutation();
|
||||
|
||||
@ -57,7 +57,7 @@ const useCreateTaskModalForm = ({courseId}) => {
|
||||
number: values.number || 1,
|
||||
};
|
||||
|
||||
const response = await createtask({
|
||||
const response = await createTask({
|
||||
courseId,
|
||||
taskData,
|
||||
}).unwrap();
|
||||
|
||||
@ -1,8 +1,32 @@
|
||||
import {Avatar, Button, Col, Divider, Modal, Popconfirm, Row, Space, Spin, Typography} from "antd";
|
||||
import {CloseOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Col, Collapse,
|
||||
Divider,
|
||||
Empty, Flex,
|
||||
Form,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Space,
|
||||
Spin,
|
||||
Tag,
|
||||
Typography,
|
||||
Upload
|
||||
} from "antd";
|
||||
import {
|
||||
CloseOutlined, DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
FileOutlined,
|
||||
PlusOutlined,
|
||||
UploadOutlined,
|
||||
UserOutlined
|
||||
} from "@ant-design/icons";
|
||||
import useViewTaskModal from "./useTaskLessonModal.js";
|
||||
import {ROLES} from "../../../../../Core/constants.js";
|
||||
import JoditEditor from "jodit-react";
|
||||
|
||||
|
||||
const {Panel} = Collapse;
|
||||
const {Title, Text, Paragraph} = Typography;
|
||||
|
||||
const ViewTaskModal = () => {
|
||||
@ -14,7 +38,16 @@ const ViewTaskModal = () => {
|
||||
isCurrentTaskFilesLoading,
|
||||
isCurrentTaskFilesError,
|
||||
downloadFile,
|
||||
downloadingFiles
|
||||
downloadingFiles,
|
||||
currentUser,
|
||||
mySolutions,
|
||||
editorRef,
|
||||
joditConfig,
|
||||
handleAddFile,
|
||||
handleRemoveFile,
|
||||
handleOk,
|
||||
draftFiles,
|
||||
handleDeleSolution
|
||||
} = useViewTaskModal();
|
||||
|
||||
return (
|
||||
@ -102,6 +135,133 @@ const ViewTaskModal = () => {
|
||||
<p>Файлы отсутствуют</p>
|
||||
)}
|
||||
|
||||
{currentUser?.role?.title === ROLES.STUDENT ? (
|
||||
<Col>
|
||||
<Title level={3}>Ваши решения</Title>
|
||||
{mySolutions.length > 0 ? (
|
||||
<Collapse accordion>
|
||||
{mySolutions.map((solution) => (
|
||||
<Panel
|
||||
key={solution.id}
|
||||
header={
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text strong>Решение
|
||||
от {new Date(solution.created_at).toLocaleString("ru-RU")}</Text>
|
||||
{solution.assessment !== null ? (
|
||||
<Tag
|
||||
color={solution.assessment >= 80 ? "green" : solution.assessment >= 60 ? "orange" : "red"}>
|
||||
Оценка: {solution.assessment} / 100
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color="blue">На проверке</Tag>
|
||||
)}
|
||||
|
||||
<Popconfirm
|
||||
title={`Удалить ответ на задание?`}
|
||||
description="Это действие нельзя отменить"
|
||||
onConfirm={(e) => {
|
||||
handleDeleSolution(solution.id)
|
||||
}}
|
||||
okText="Удалить"
|
||||
cancelText="Отмена"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined/>}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</Flex>
|
||||
}
|
||||
extra={
|
||||
solution.assessment !== null && (
|
||||
<Tag color="purple">
|
||||
Проверено: {solution.assessment_autor?.first_name} {solution.assessment_autor?.last_name}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div style={{marginBottom: 16}}>
|
||||
<Text strong>Ответ:</Text>
|
||||
<Paragraph
|
||||
style={{
|
||||
background: "#f9f9f9",
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
margin: "8px 0",
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{solution.answer_text}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{solution.files && solution.files.length > 0 ? (
|
||||
<div>
|
||||
<Text strong>Прикреплённые файлы:</Text>
|
||||
<div style={{
|
||||
marginTop: 8,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8
|
||||
}}>
|
||||
{solution.files.map((file) => (
|
||||
<Button
|
||||
key={file.id}
|
||||
type="dashed"
|
||||
icon={<FileOutlined/>}
|
||||
style={{textAlign: "left"}}
|
||||
onClick={() => downloadFile(file.id, file.filename)}
|
||||
loading={downloadingFiles[file.id]}
|
||||
>
|
||||
<span style={{marginLeft: 8}}>
|
||||
{file.filename} ({(file.file_size / 1024 / 1024).toFixed(2)} МБ)
|
||||
</span>
|
||||
<DownloadOutlined style={{marginLeft: 8, color: "#1890ff"}}/>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Text type="secondary">Файлы не прикреплены</Text>
|
||||
)}
|
||||
</Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
) : (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="Решений пока нет"
|
||||
/>
|
||||
)}
|
||||
<Title level={3}>Добавить решение</Title>
|
||||
<div style={{border: "1px solid #d9d9d9", borderRadius: 8, overflow: "hidden"}}>
|
||||
<JoditEditor
|
||||
ref={editorRef}
|
||||
config={joditConfig}
|
||||
/>
|
||||
</div>
|
||||
<Divider/>
|
||||
<Upload
|
||||
fileList={draftFiles}
|
||||
beforeUpload={(file) => {
|
||||
handleAddFile(file);
|
||||
return false;
|
||||
}}
|
||||
onRemove={(file) => handleRemoveFile(file)}
|
||||
multiple
|
||||
>
|
||||
<Button icon={<UploadOutlined/>}>Выбрать файлы</Button>
|
||||
</Upload>
|
||||
<Divider/>
|
||||
<Button type="primary" onClick={handleOk}>
|
||||
Сохранить решение
|
||||
</Button>
|
||||
</Col>
|
||||
) : [ROLES.ADMIN, ROLES.TEACHER].includes(currentUser?.role?.title) && (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<div style={{textAlign: "right"}}>
|
||||
<Button onClick={handleClose}>
|
||||
Закрыть
|
||||
|
||||
@ -2,8 +2,15 @@ import {useDispatch, useSelector} from "react-redux";
|
||||
import {setSelectedTaskToView} from "../../../../../Redux/Slices/tasksSlice.js";
|
||||
import {notification} from "antd";
|
||||
import CONFIG from "../../../../../Core/сonfig.js";
|
||||
import {useState} from "react";
|
||||
import {useMemo, useRef, useState} from "react";
|
||||
import {useGetTaskFilesListQuery} from "../../../../../Api/tasksApi.js";
|
||||
import {useGetAuthenticatedUserDataQuery} from "../../../../../Api/usersApi.js";
|
||||
import {
|
||||
useCreateSolutionMutation, useDeleteSolutionMutation,
|
||||
useGetTaskStudentSolutionsQuery,
|
||||
useUploadFileMutation
|
||||
} from "../../../../../Api/solutionsApi.js";
|
||||
import {ROLES} from "../../../../../Core/constants.js";
|
||||
|
||||
|
||||
const useViewTaskModal = () => {
|
||||
@ -13,12 +20,113 @@ const useViewTaskModal = () => {
|
||||
selectedTaskToView
|
||||
} = useSelector((state) => state.tasks);
|
||||
|
||||
const [
|
||||
craeteColution,
|
||||
{
|
||||
isLoading: isCreatingSolution,
|
||||
isError: isErrorCreatingSoltion
|
||||
}
|
||||
] = useCreateSolutionMutation();
|
||||
|
||||
const modalIsOpen = selectedTaskToView !== null;
|
||||
|
||||
const [draftFiles, setDraftFiles] = useState([]);
|
||||
const [uploadFile] = useUploadFileMutation();
|
||||
const [deleteSolution] = useDeleteSolutionMutation();
|
||||
|
||||
const handleAddFile = (file) => {
|
||||
const maxSize = 50 * 1024 * 1024; // 50 мегабайт
|
||||
if (file.size > maxSize) {
|
||||
notification.error({
|
||||
message: "Ошибка вставки",
|
||||
description: "Файл слишком большой.",
|
||||
placement: "topRight",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
setDraftFiles((prev) => [...prev, file]);
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleDeleSolution = async (solutionId) => {
|
||||
try {
|
||||
await deleteSolution(solutionId);
|
||||
|
||||
notification.success({
|
||||
title: "Успех",
|
||||
description: "Задание успешно удалено!",
|
||||
placement: "topRight",
|
||||
})
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
title: "Ошибка",
|
||||
description: error?.data?.detail || "Не удалось удалить задание",
|
||||
placement: "topRight",
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFile = (file) => {
|
||||
setDraftFiles((prev) => prev.filter((f) => f.uid !== file.uid));
|
||||
};
|
||||
|
||||
const editorRef = useRef(null);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const content = editorRef.current?.value || "";
|
||||
|
||||
const solutionData = {
|
||||
answer_text: content,
|
||||
};
|
||||
|
||||
const response = await craeteColution({
|
||||
taskId: selectedTaskToView?.id,
|
||||
solution: solutionData,
|
||||
}).unwrap();
|
||||
|
||||
for (const file of draftFiles) {
|
||||
try {
|
||||
await uploadFile({
|
||||
solutionId: response.id,
|
||||
fileData: file,
|
||||
}).unwrap();
|
||||
} catch (error) {
|
||||
console.error(`Error uploading file ${file.name}:`, error);
|
||||
const errorMessage = error.data?.detail
|
||||
? JSON.stringify(error.data.detail, null, 2)
|
||||
: JSON.stringify(error.data || error.message || "Неизвестная ошибка", null, 2);
|
||||
notification.error({
|
||||
title: "Ошибка загрузки файла",
|
||||
description: `Не удалось загрузить файл ${file.name}: ${errorMessage}`,
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
notification.success({
|
||||
title: "Успех",
|
||||
description: "Задание успешно создано!",
|
||||
placement: "topRight",
|
||||
});
|
||||
|
||||
if (editorRef.current) {
|
||||
editorRef.current.value = "";
|
||||
}
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
title: "Ошибка",
|
||||
description: error?.data?.detail || "Не удалось создать задание",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(setSelectedTaskToView(null));
|
||||
};
|
||||
|
||||
|
||||
const {
|
||||
data: currentTaskFiles = [],
|
||||
isLoading: isCurrentTaskFilesLoading,
|
||||
@ -27,6 +135,24 @@ const useViewTaskModal = () => {
|
||||
skip: !selectedTaskToView?.id
|
||||
});
|
||||
|
||||
const {
|
||||
data: currentUser = {},
|
||||
isLoading: isCurrentUserLoading,
|
||||
isError: isCurrentUserError,
|
||||
} = useGetAuthenticatedUserDataQuery();
|
||||
|
||||
const {
|
||||
data: mySolutions = [],
|
||||
isLoading: isMySolutionsLoading,
|
||||
isError: isMySolutionsError
|
||||
} = useGetTaskStudentSolutionsQuery({
|
||||
taskId: selectedTaskToView?.id,
|
||||
studentId: currentUser?.id
|
||||
}, {
|
||||
skip: !selectedTaskToView?.id || currentUser?.role?.title !== ROLES.STUDENT,
|
||||
pollingInterval: 5000,
|
||||
});
|
||||
|
||||
const [downloadingFiles, setDownloadingFiles] = useState({});
|
||||
|
||||
const downloadFile = async (fileId, fileName) => {
|
||||
@ -41,16 +167,15 @@ const useViewTaskModal = () => {
|
||||
placement: "topRight",
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch(`${CONFIG.BASE_URL}/tasks/file/${fileId}/`, {
|
||||
const response = await fetch(`${CONFIG.BASE_URL}/solutions/file/${fileId}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
notification.error({
|
||||
@ -59,7 +184,7 @@ const useViewTaskModal = () => {
|
||||
placement: "topRight",
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || contentType.includes('text/html')) {
|
||||
@ -105,6 +230,70 @@ const useViewTaskModal = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const joditConfig = useMemo(
|
||||
() => ({
|
||||
readonly: false,
|
||||
height: 150,
|
||||
toolbarAdaptive: false,
|
||||
buttons: [
|
||||
"bold", "italic", "underline", "strikethrough", "|",
|
||||
"superscript", "subscript", "|",
|
||||
"ul", "ol", "outdent", "indent", "|",
|
||||
"font", "fontsize", "brush", "paragraph", "|",
|
||||
"align", "hr", "|",
|
||||
"table", "link", "image", "video", "symbols", "|",
|
||||
"undo", "redo", "cut", "copy", "paste", "selectall", "eraser", "|",
|
||||
"find", "source", "fullsize", "print", "preview",
|
||||
],
|
||||
autofocus: false,
|
||||
preserveSelection: true,
|
||||
askBeforePasteHTML: false,
|
||||
askBeforePasteFromWord: false,
|
||||
defaultActionOnPaste: "insert_clear_html",
|
||||
spellcheck: true,
|
||||
placeholder: "Заполните содержимое задания",
|
||||
showCharsCounter: true,
|
||||
showWordsCounter: true,
|
||||
showXPathInStatusbar: false,
|
||||
toolbarSticky: true,
|
||||
toolbarButtonSize: "middle",
|
||||
cleanHTML: {
|
||||
removeEmptyElements: true,
|
||||
replaceNBSP: false,
|
||||
},
|
||||
hotkeys: {
|
||||
"ctrl + shift + f": "find",
|
||||
"ctrl + b": "bold",
|
||||
"ctrl + i": "italic",
|
||||
"ctrl + u": "underline",
|
||||
},
|
||||
image: {
|
||||
editSrc: true,
|
||||
editTitle: true,
|
||||
editAlt: true,
|
||||
openOnDblClick: false,
|
||||
},
|
||||
video: {
|
||||
allowedSources: ["youtube", "vimeo"],
|
||||
},
|
||||
uploader: {
|
||||
insertImageAsBase64URI: true,
|
||||
},
|
||||
paste: {
|
||||
insertAsBase64: true,
|
||||
mimeTypes: ["image/png", "image/jpeg", "image/gif"],
|
||||
maxFileSize: 5 * 1024 * 1024,
|
||||
error: () => {
|
||||
notification.error({
|
||||
title: "Ошибка вставки",
|
||||
description: "Файл слишком большой или неподдерживаемый формат.",
|
||||
placement: "topRight",
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
selectedTaskToView,
|
||||
@ -114,7 +303,16 @@ const useViewTaskModal = () => {
|
||||
isCurrentTaskFilesLoading,
|
||||
isCurrentTaskFilesError,
|
||||
downloadFile,
|
||||
downloadingFiles
|
||||
downloadingFiles,
|
||||
currentUser,
|
||||
mySolutions,
|
||||
editorRef,
|
||||
joditConfig,
|
||||
handleAddFile,
|
||||
handleRemoveFile,
|
||||
handleOk,
|
||||
draftFiles,
|
||||
handleDeleSolution
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
setSelectedTaskToUpdate,
|
||||
setSelectedTaskToView
|
||||
} from "../../../Redux/Slices/tasksSlice.js";
|
||||
import {useGetTasksByCourseIdQuery} from "../../../Api/tasksApi.js";
|
||||
import {useDeleteTaskMutation, useGetTasksByCourseIdQuery} from "../../../Api/tasksApi.js";
|
||||
|
||||
|
||||
const useCourseDetailPage = (courseId) => {
|
||||
@ -58,6 +58,10 @@ const useCourseDetailPage = (courseId) => {
|
||||
deleteLesson,
|
||||
] = useDeleteLessonMutation();
|
||||
|
||||
const [
|
||||
deleteTask,
|
||||
] = useDeleteTaskMutation();
|
||||
|
||||
const handleDeleteLesson = async (lessonId) => {
|
||||
try {
|
||||
await deleteLesson(lessonId);
|
||||
@ -77,7 +81,20 @@ const useCourseDetailPage = (courseId) => {
|
||||
};
|
||||
|
||||
const handleDeleteTask = async (taskId) => {
|
||||
|
||||
try {
|
||||
await deleteTask(taskId);
|
||||
notification.success({
|
||||
title: "Успешно",
|
||||
description: "Задание удалено",
|
||||
placement: "topRight",
|
||||
});
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
title: "Ошибка",
|
||||
description: error?.data?.detail || "Произошла ошибка при удалении задания",
|
||||
placement: "topRight",
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -11,6 +11,7 @@ import {statusesApi} from "../Api/statusesApi.js";
|
||||
import {coursesApi} from "../Api/coursesApi.js";
|
||||
import {lessonsApi} from "../Api/lessonsApi.js";
|
||||
import {tasksApi} from "../Api/tasksApi.js";
|
||||
import {solutionsApi} from "../Api/solutionsApi.js";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
@ -32,6 +33,8 @@ export const store = configureStore({
|
||||
|
||||
tasks: tasksReducer,
|
||||
[tasksApi.reducerPath]: tasksApi.reducer,
|
||||
|
||||
[solutionsApi.reducerPath]: solutionsApi.reducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => (
|
||||
getDefaultMiddleware().concat(
|
||||
@ -42,6 +45,7 @@ export const store = configureStore({
|
||||
coursesApi.middleware,
|
||||
lessonsApi.middleware,
|
||||
tasksApi.middleware,
|
||||
solutionsApi.middleware,
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user