сделал создание задачи
This commit is contained in:
parent
e90be8b54e
commit
ca6727d9bf
@ -43,7 +43,7 @@ class CoursesRepository:
|
|||||||
selectinload(Course.enrollments)
|
selectinload(Course.enrollments)
|
||||||
)
|
)
|
||||||
.join(Course.enrollments)
|
.join(Course.enrollments)
|
||||||
.filter(Enrollment.user_id == user_id)
|
.filter(Enrollment.student_id == user_id)
|
||||||
)
|
)
|
||||||
result = await self.db.execute(query)
|
result = await self.db.execute(query)
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
|
|||||||
38
api/app/application/task_files_repository.py
Normal file
38
api/app/application/task_files_repository.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.domain.models import TaskFile
|
||||||
|
|
||||||
|
|
||||||
|
class TaskFilesRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get_by_id(self, file_id: int) -> Optional[TaskFile]:
|
||||||
|
query = (
|
||||||
|
select(TaskFile)
|
||||||
|
.filter_by(id=file_id)
|
||||||
|
)
|
||||||
|
result = await self.db.execute(query)
|
||||||
|
return result.scalars().first()
|
||||||
|
|
||||||
|
async def get_by_task_id(self, task_id: int) -> Optional[TaskFile]:
|
||||||
|
query = (
|
||||||
|
select(TaskFile)
|
||||||
|
.filter_by(task_id=task_id)
|
||||||
|
)
|
||||||
|
result = await self.db.execute(query)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
async def create(self, task_file: TaskFile) -> TaskFile:
|
||||||
|
self.db.add(task_file)
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(task_file)
|
||||||
|
return task_file
|
||||||
|
|
||||||
|
async def delete(self, task_file: TaskFile) -> TaskFile:
|
||||||
|
await self.db.delete(task_file)
|
||||||
|
await self.db.commit()
|
||||||
|
return task_file
|
||||||
45
api/app/application/tasks_repository.py
Normal file
45
api/app/application/tasks_repository.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.domain.models import Task
|
||||||
|
|
||||||
|
|
||||||
|
class TasksRepository:
|
||||||
|
def __init__(self, db: AsyncSession) -> None:
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get_all_by_course(self, course_id: int) -> List[Task]:
|
||||||
|
query = (
|
||||||
|
select(Task)
|
||||||
|
.filter_by(course_id=course_id)
|
||||||
|
.order_by(Task.number)
|
||||||
|
)
|
||||||
|
result = await self.db.execute(query)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
async def get_by_id(self, task_id: int) -> Optional[Task]:
|
||||||
|
query = (
|
||||||
|
select(Task)
|
||||||
|
.filter_by(id=task_id)
|
||||||
|
)
|
||||||
|
result = await self.db.execute(query)
|
||||||
|
return result.scalars().first()
|
||||||
|
|
||||||
|
async def create(self, task: Task) -> Task:
|
||||||
|
self.db.add(task)
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
async def update(self, task: Task) -> Task:
|
||||||
|
await self.db.merge(task)
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
async def delete(self, task: Task) -> Task:
|
||||||
|
await self.db.delete(task)
|
||||||
|
await self.db.commit()
|
||||||
|
return task
|
||||||
151
api/app/controllers/tasks_router.py
Normal file
151
api/app/controllers/tasks_router.py
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, status, File, UploadFile
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from starlette.responses import FileResponse
|
||||||
|
|
||||||
|
from app.database.session import get_db
|
||||||
|
from app.domain.entities.task_files import ReadTaskFile
|
||||||
|
from app.domain.entities.tasks import TaskRead, TaskCreate
|
||||||
|
from app.domain.models import User
|
||||||
|
from app.infrastructure.dependencies import require_auth_user, require_teacher
|
||||||
|
from app.infrastructure.task_files_service import TaskFilesService
|
||||||
|
from app.infrastructure.tasks_service import TasksService
|
||||||
|
|
||||||
|
tasks_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_router.get(
|
||||||
|
'/course/{course_id}/',
|
||||||
|
response_model=Optional[List[TaskRead]],
|
||||||
|
summary='Get all tasks by course',
|
||||||
|
description='Get all tasks by course',
|
||||||
|
)
|
||||||
|
async def get_course_tasks(
|
||||||
|
course_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(require_auth_user),
|
||||||
|
):
|
||||||
|
tasks_service = TasksService(db)
|
||||||
|
return await tasks_service.get_all_by_course(course_id)
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_router.get(
|
||||||
|
'/{task_id}/',
|
||||||
|
response_model=Optional[TaskRead],
|
||||||
|
summary='Get task by task ID',
|
||||||
|
description='Get task by task ID',
|
||||||
|
)
|
||||||
|
async def get_task(
|
||||||
|
task_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(require_auth_user),
|
||||||
|
):
|
||||||
|
tasks_service = TasksService(db)
|
||||||
|
return await tasks_service.get_by_id(task_id)
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_router.post(
|
||||||
|
'/{course_id}/',
|
||||||
|
response_model=Optional[TaskRead],
|
||||||
|
summary='Create a new task',
|
||||||
|
description='Create a new task',
|
||||||
|
)
|
||||||
|
async def create_task(
|
||||||
|
course_id: int,
|
||||||
|
task_data: TaskCreate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(require_teacher),
|
||||||
|
):
|
||||||
|
tasks_service = TasksService(db)
|
||||||
|
return await tasks_service.create(task_data, current_user, course_id)
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_router.put(
|
||||||
|
'/{task_id}/',
|
||||||
|
response_model=Optional[TaskRead],
|
||||||
|
summary='Update a task',
|
||||||
|
description='Update a task',
|
||||||
|
)
|
||||||
|
async def update_task(
|
||||||
|
task_id: int,
|
||||||
|
task_data: TaskCreate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(require_teacher),
|
||||||
|
):
|
||||||
|
tasks_service = TasksService(db)
|
||||||
|
return await tasks_service.update(task_id, task_data, current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_router.delete(
|
||||||
|
'/{task_id}/',
|
||||||
|
response_model=Optional[TaskRead],
|
||||||
|
summary='Delete a task',
|
||||||
|
description='Delete a task',
|
||||||
|
)
|
||||||
|
async def delete_task(
|
||||||
|
task_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(require_teacher),
|
||||||
|
):
|
||||||
|
tasks_service = TasksService(db)
|
||||||
|
return await tasks_service.delete(task_id, current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_router.get(
|
||||||
|
'/files/{task_id}/',
|
||||||
|
response_model=Optional[List[ReadTaskFile]],
|
||||||
|
summary='Get a files list by task ID',
|
||||||
|
description='Get a files list by task ID',
|
||||||
|
)
|
||||||
|
async def get_files(
|
||||||
|
task_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(require_auth_user),
|
||||||
|
):
|
||||||
|
task_files_service = TaskFilesService(db)
|
||||||
|
return await task_files_service.get_files_list_by_task(task_id)
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_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 = TaskFilesService(db)
|
||||||
|
return await task_files_service.get_file_by_id(file_id)
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_router.post(
|
||||||
|
'/files/{task_id}/upload/',
|
||||||
|
response_model=ReadTaskFile,
|
||||||
|
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_teacher),
|
||||||
|
):
|
||||||
|
task_files_service = TaskFilesService(db)
|
||||||
|
return await task_files_service.upload_file(task_id, file)
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_router.delete(
|
||||||
|
'/files/{file_id}/',
|
||||||
|
response_model=Optional[ReadTaskFile],
|
||||||
|
summary='Delete a file',
|
||||||
|
description='Delete a file',
|
||||||
|
)
|
||||||
|
async def delete_file(
|
||||||
|
file_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(require_teacher),
|
||||||
|
):
|
||||||
|
task_files_service = TaskFilesService(db)
|
||||||
|
return await task_files_service.delete_file(file_id)
|
||||||
11
api/app/domain/entities/task_files.py
Normal file
11
api/app/domain/entities/task_files.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ReadTaskFile(BaseModel):
|
||||||
|
id: int
|
||||||
|
filename: str
|
||||||
|
file_path: str
|
||||||
|
task_id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
30
api/app/domain/entities/tasks.py
Normal file
30
api/app/domain/entities/tasks.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from app.domain.entities.users import UserRead
|
||||||
|
|
||||||
|
|
||||||
|
class TaskBase(BaseModel):
|
||||||
|
title: str = Field(..., max_length=250)
|
||||||
|
description: Optional[str] = None
|
||||||
|
text: Optional[str] = None
|
||||||
|
number: int = Field(..., ge=1)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskCreate(TaskBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TaskUpdate(TaskBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TaskRead(TaskBase):
|
||||||
|
id: int
|
||||||
|
course_id: int
|
||||||
|
creator_id: int
|
||||||
|
|
||||||
|
creator: UserRead
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@ -18,6 +18,6 @@ class Task(RootTable):
|
|||||||
creator_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False)
|
creator_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False)
|
||||||
|
|
||||||
course: Mapped['Course'] = relationship('Course', back_populates='tasks')
|
course: Mapped['Course'] = relationship('Course', back_populates='tasks')
|
||||||
creator: Mapped['User'] = relationship('User', back_populates='created_tasks')
|
creator: Mapped['User'] = relationship('User', back_populates='created_tasks', lazy='joined')
|
||||||
|
|
||||||
files: Mapped[List['TaskFile']] = relationship('TaskFile', back_populates='task')
|
files: Mapped[List['TaskFile']] = relationship('TaskFile', back_populates='task')
|
||||||
|
|||||||
118
api/app/infrastructure/task_files_service.py
Normal file
118
api/app/infrastructure/task_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.task_files_repository import TaskFilesRepository
|
||||||
|
from app.application.tasks_repository import TasksRepository
|
||||||
|
from app.domain.entities.task_files import ReadTaskFile
|
||||||
|
from app.domain.models import TaskFile
|
||||||
|
|
||||||
|
|
||||||
|
class TaskFilesService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.task_files_repository = TaskFilesRepository(db)
|
||||||
|
self.tasks_repository = TasksRepository(db)
|
||||||
|
|
||||||
|
async def get_file_by_id(self, file_id: int) -> FileResponse:
|
||||||
|
task_file = await self.task_files_repository.get_by_id(file_id)
|
||||||
|
|
||||||
|
if not task_file:
|
||||||
|
raise HTTPException(404, "Файл с таким ID не найден")
|
||||||
|
|
||||||
|
return FileResponse(
|
||||||
|
task_file.file_path,
|
||||||
|
media_type=self.get_media_type(task_file.filename),
|
||||||
|
filename=os.path.basename(task_file.filename),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_files_list_by_task(self, task_id: int) -> List[ReadTaskFile]:
|
||||||
|
task = await self.tasks_repository.get_by_id(task_id)
|
||||||
|
|
||||||
|
if task is None:
|
||||||
|
raise HTTPException(404, "Лекционный материал не найден")
|
||||||
|
|
||||||
|
task_files = await self.task_files_repository.get_by_task_id(task_id)
|
||||||
|
|
||||||
|
response = []
|
||||||
|
for task_file in task_files:
|
||||||
|
response.append(
|
||||||
|
ReadTaskFile.model_validate(
|
||||||
|
task_file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def upload_file(self, task_id: int, file: UploadFile) -> ReadTaskFile:
|
||||||
|
task = await self.tasks_repository.get_by_id(task_id)
|
||||||
|
|
||||||
|
if task is None:
|
||||||
|
raise HTTPException(404, "Лекционный материал не найден")
|
||||||
|
|
||||||
|
file_path = await self.save_file(file, f'uploads/tasks/{task.id}')
|
||||||
|
|
||||||
|
task_file_model = TaskFile(
|
||||||
|
filename=file.filename,
|
||||||
|
file_path=file_path,
|
||||||
|
task_id=task.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
task_file_model = await self.task_files_repository.create(task_file_model)
|
||||||
|
|
||||||
|
return ReadTaskFile.model_validate(task_file_model)
|
||||||
|
|
||||||
|
async def delete_file(self, file_id: int) -> ReadTaskFile:
|
||||||
|
task_file = await self.task_files_repository.get_by_id(file_id)
|
||||||
|
|
||||||
|
if task_file is None:
|
||||||
|
raise HTTPException(404, "Файл не найден")
|
||||||
|
|
||||||
|
if not os.path.exists(task_file.file_path):
|
||||||
|
raise HTTPException(404, "Файл не найден на диске")
|
||||||
|
|
||||||
|
if os.path.exists(task_file.file_path):
|
||||||
|
os.remove(task_file.file_path)
|
||||||
|
|
||||||
|
task_file = await self.task_files_repository.delete(task_file)
|
||||||
|
|
||||||
|
return ReadTaskFile.model_validate(task_file)
|
||||||
|
|
||||||
|
async def save_file(self, file: UploadFile, upload_dir: str = 'uploads/tasks') -> 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"
|
||||||
113
api/app/infrastructure/tasks_service.py
Normal file
113
api/app/infrastructure/tasks_service.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import os
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.application.courses_repository import CoursesRepository
|
||||||
|
from app.application.task_files_repository import TaskFilesRepository
|
||||||
|
from app.application.tasks_repository import TasksRepository
|
||||||
|
from app.domain.entities.tasks import TaskCreate, TaskUpdate
|
||||||
|
from app.domain.entities.tasks import TaskRead
|
||||||
|
from app.domain.models import User, Task
|
||||||
|
from app.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
|
class TasksService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.tasks_repository = TasksRepository(db)
|
||||||
|
self.courses_repository = CoursesRepository(db)
|
||||||
|
self.task_files_repository = TaskFilesRepository(db)
|
||||||
|
self.settings = Settings()
|
||||||
|
|
||||||
|
async def get_all_by_course(self, course_id: int) -> List[TaskRead]:
|
||||||
|
tasks = await self.tasks_repository.get_all_by_course(course_id)
|
||||||
|
response = []
|
||||||
|
|
||||||
|
for task in tasks:
|
||||||
|
response.append(TaskRead.model_validate(task))
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def get_by_id(self, task_id: int) -> TaskRead:
|
||||||
|
task = await self.tasks_repository.get_by_id(task_id)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Задание не найдено"
|
||||||
|
)
|
||||||
|
return TaskRead.model_validate(task)
|
||||||
|
|
||||||
|
async def create(self, task_data: TaskCreate, creator: User, course_id) -> TaskRead:
|
||||||
|
course_model = await self.courses_repository.get_by_id(course_id)
|
||||||
|
if not course_model:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Курс не найден"
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
title=task_data.title,
|
||||||
|
description=task_data.description,
|
||||||
|
text=task_data.text,
|
||||||
|
number=task_data.number,
|
||||||
|
course_id=course_id,
|
||||||
|
creator_id=creator.id
|
||||||
|
)
|
||||||
|
|
||||||
|
created_task = await self.tasks_repository.create(task)
|
||||||
|
return TaskRead.model_validate(created_task)
|
||||||
|
|
||||||
|
async def update(self, task_id: int, task_data: TaskUpdate, current_user: User) -> Optional[TaskRead]:
|
||||||
|
task = await self.tasks_repository.get_by_id(task_id)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Задание не найдено"
|
||||||
|
)
|
||||||
|
|
||||||
|
is_admin = current_user.role.title == self.settings.root_role_name
|
||||||
|
if task.creator_id != current_user.id and not is_admin:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Доступ запрещён"
|
||||||
|
)
|
||||||
|
|
||||||
|
update_dict = task_data.dict(exclude_unset=True)
|
||||||
|
for key, value in update_dict.items():
|
||||||
|
setattr(task, key, value)
|
||||||
|
|
||||||
|
updated_task = await self.tasks_repository.update(task)
|
||||||
|
return TaskRead.model_validate(updated_task)
|
||||||
|
|
||||||
|
async def delete(self, task_id: int, current_user: User) -> None:
|
||||||
|
task = await self.tasks_repository.get_by_id(task_id)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Задание не найдено"
|
||||||
|
)
|
||||||
|
|
||||||
|
is_admin = current_user.role.title == self.settings.root_role_name
|
||||||
|
if task.creator_id != current_user.id and not is_admin:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Доступ запрещён"
|
||||||
|
)
|
||||||
|
|
||||||
|
task_files = await self.task_files_repository.get_by_task_id(task_id)
|
||||||
|
for file in task_files:
|
||||||
|
task_file = await self.task_files_repository.get_by_id(file.id)
|
||||||
|
|
||||||
|
if task_file is None:
|
||||||
|
raise HTTPException(404, "Файл не найден")
|
||||||
|
|
||||||
|
if not os.path.exists(task_file.file_path):
|
||||||
|
raise HTTPException(404, "Файл не найден на диске")
|
||||||
|
|
||||||
|
if os.path.exists(task_file.file_path):
|
||||||
|
os.remove(task_file.file_path)
|
||||||
|
|
||||||
|
await self.task_files_repository.delete(task_file)
|
||||||
|
|
||||||
|
await self.tasks_repository.delete(task)
|
||||||
@ -7,6 +7,7 @@ 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.statuses_router import statuses_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
|
from app.controllers.users_router import users_router
|
||||||
from app.settings import Settings
|
from app.settings import Settings
|
||||||
|
|
||||||
@ -29,6 +30,7 @@ def start_app():
|
|||||||
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(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(users_router, prefix=f'{settings.prefix}/users', tags=['users'])
|
api_app.include_router(users_router, prefix=f'{settings.prefix}/users', tags=['users'])
|
||||||
|
|
||||||
return api_app
|
return api_app
|
||||||
|
|||||||
@ -13,6 +13,13 @@ export const lessonsApi = createApi({
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
}),
|
}),
|
||||||
providesTags: ["lesson"],
|
providesTags: ["lesson"],
|
||||||
|
transformResponse: (response) => {
|
||||||
|
return response.map(lesson => ({
|
||||||
|
...lesson,
|
||||||
|
contentType: "lesson",
|
||||||
|
__typename: "Lesson"
|
||||||
|
}));
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
getLessonById: builder.query({
|
getLessonById: builder.query({
|
||||||
query: (lessonId) => ({
|
query: (lessonId) => ({
|
||||||
|
|||||||
104
web/src/Api/tasksApi.js
Normal file
104
web/src/Api/tasksApi.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import {createApi} from "@reduxjs/toolkit/query/react";
|
||||||
|
import {baseQueryWithAuth} from "./baseQuery.js";
|
||||||
|
|
||||||
|
|
||||||
|
export const tasksApi = createApi({
|
||||||
|
reducerPath: "tasksApi",
|
||||||
|
baseQuery: baseQueryWithAuth,
|
||||||
|
tagTypes: ["task"],
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
getTasksByCourseId: builder.query({
|
||||||
|
query: (courseId) => ({
|
||||||
|
url: `/tasks/course/${courseId}/`,
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
providesTags: ["task"],
|
||||||
|
transformResponse: (response) => {
|
||||||
|
return response.map(task => ({
|
||||||
|
...task,
|
||||||
|
contentType: "task",
|
||||||
|
__typename: "Task"
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getTaskById: builder.query({
|
||||||
|
query: (taskId) => ({
|
||||||
|
url: `/tasks/${taskId}/`,
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
providesTags: ["task"],
|
||||||
|
}),
|
||||||
|
createTask: builder.mutation({
|
||||||
|
query: ({courseId, taskData}) => ({
|
||||||
|
url: `/tasks/${courseId}/`,
|
||||||
|
method: "POST",
|
||||||
|
body: taskData,
|
||||||
|
}),
|
||||||
|
invalidatesTags: ["task"],
|
||||||
|
}),
|
||||||
|
updateTask: builder.mutation({
|
||||||
|
query: ({taskId, taskData}) => ({
|
||||||
|
url: `/tasks/${taskId}/`,
|
||||||
|
method: "PUT",
|
||||||
|
body: taskData,
|
||||||
|
}),
|
||||||
|
invalidatesTags: ["task"],
|
||||||
|
}),
|
||||||
|
deleteTask: builder.mutation({
|
||||||
|
query: (taskId) => ({
|
||||||
|
url: `/tasks/${taskId}/`,
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
invalidatesTags: ["task"],
|
||||||
|
}),
|
||||||
|
getTaskFilesList: builder.query({
|
||||||
|
query: (taskId) => ({
|
||||||
|
url: `/tasks/files/${taskId}/`,
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
providesTags: ["task"],
|
||||||
|
}),
|
||||||
|
getDownloadFile: builder.query({
|
||||||
|
query: (fileId) => ({
|
||||||
|
url: `/tasks/file/${fileId}/`,
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
providesTags: ["task"],
|
||||||
|
}),
|
||||||
|
uploadFile: builder.mutation({
|
||||||
|
query: ({task_id, fileData}) => {
|
||||||
|
if (!(fileData instanceof File)) {
|
||||||
|
throw new Error('Invalid file object');
|
||||||
|
}
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', fileData);
|
||||||
|
return {
|
||||||
|
url: `/tasks/files/${task_id}/upload/`,
|
||||||
|
method: 'POST',
|
||||||
|
formData: true,
|
||||||
|
body: formData,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
invalidatesTags: ["task"],
|
||||||
|
}),
|
||||||
|
deleteFile: builder.mutation({
|
||||||
|
query: (fileId) => ({
|
||||||
|
url: `/tasks/files/${fileId}/`,
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
invalidatesTags: ["task"],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
useGetTasksByCourseIdQuery,
|
||||||
|
useGetTaskByIdQuery,
|
||||||
|
useCreateTaskMutation,
|
||||||
|
useUpdateTaskMutation,
|
||||||
|
useDeleteTaskMutation,
|
||||||
|
useGetTaskFilesListQuery,
|
||||||
|
useGetDownloadFileQuery,
|
||||||
|
useUploadFileMutation,
|
||||||
|
useDeleteFileMutation,
|
||||||
|
} = tasksApi;
|
||||||
@ -46,7 +46,7 @@ const CreateLessonModalForm = ({courseId}) => {
|
|||||||
label="Название лекции"
|
label="Название лекции"
|
||||||
rules={[{required: true, message: "Введите название лекции"}]}
|
rules={[{required: true, message: "Введите название лекции"}]}
|
||||||
>
|
>
|
||||||
<Input size="large" placeholder="Введение в JavaScript"/>
|
<Input size="large"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="description" label="Краткое описание">
|
<Form.Item name="description" label="Краткое описание">
|
||||||
@ -74,7 +74,6 @@ const CreateLessonModalForm = ({courseId}) => {
|
|||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
onRemove={(file) => handleRemoveFile(file)}
|
onRemove={(file) => handleRemoveFile(file)}
|
||||||
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
|
|
||||||
multiple
|
multiple
|
||||||
>
|
>
|
||||||
<Button icon={<UploadOutlined/>}>Выбрать файлы</Button>
|
<Button icon={<UploadOutlined/>}>Выбрать файлы</Button>
|
||||||
|
|||||||
@ -118,7 +118,7 @@ const useCreateLessonModalForm = ({courseId}) => {
|
|||||||
askBeforePasteFromWord: false,
|
askBeforePasteFromWord: false,
|
||||||
defaultActionOnPaste: "insert_clear_html",
|
defaultActionOnPaste: "insert_clear_html",
|
||||||
spellcheck: true,
|
spellcheck: true,
|
||||||
placeholder: "Введите результаты приёма...",
|
placeholder: "Заполните содержимое лекционного материала",
|
||||||
showCharsCounter: true,
|
showCharsCounter: true,
|
||||||
showWordsCounter: true,
|
showWordsCounter: true,
|
||||||
showXPathInStatusbar: false,
|
showXPathInStatusbar: false,
|
||||||
|
|||||||
@ -0,0 +1,86 @@
|
|||||||
|
import useCreateTaskModalForm from "./useCreateTaskModalForm.js";
|
||||||
|
import {Button, Form, Input, InputNumber, Modal, Upload} from "antd";
|
||||||
|
import JoditEditor from "jodit-react";
|
||||||
|
import {UploadOutlined} from "@ant-design/icons";
|
||||||
|
|
||||||
|
const {TextArea} = Input;
|
||||||
|
|
||||||
|
const CreateTaskModalForm = ({courseId}) => {
|
||||||
|
const {
|
||||||
|
isModalOpen,
|
||||||
|
handleCancel,
|
||||||
|
handleOk,
|
||||||
|
form,
|
||||||
|
joditConfig,
|
||||||
|
editorRef,
|
||||||
|
isLoading,
|
||||||
|
handleAddFile,
|
||||||
|
handleRemoveFile,
|
||||||
|
draftFiles,
|
||||||
|
} = useCreateTaskModalForm({courseId});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="Создание задания"
|
||||||
|
open={isModalOpen}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
width={1000}
|
||||||
|
footer={[
|
||||||
|
<Button key="cancel" onClick={handleCancel}>
|
||||||
|
Отмена
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
loading={isLoading}
|
||||||
|
onClick={handleOk}
|
||||||
|
>
|
||||||
|
Создать задание
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" preserve={false}>
|
||||||
|
<Form.Item
|
||||||
|
name="title"
|
||||||
|
label="Название задания"
|
||||||
|
rules={[{required: true, message: "Введите название задания"}]}
|
||||||
|
>
|
||||||
|
<Input size="large"/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="description" label="Краткое описание заачи">
|
||||||
|
<TextArea rows={2} placeholder="Что нужно будет сделать..."/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="number" label="Порядковый номер" initialValue={1}>
|
||||||
|
<InputNumber min={1} style={{width: "100%"}}/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="Содержание задания">
|
||||||
|
<div style={{border: "1px solid #d9d9d9", borderRadius: 6}}>
|
||||||
|
<JoditEditor
|
||||||
|
ref={editorRef}
|
||||||
|
config={joditConfig}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="files" label="Прикрепить файлы">
|
||||||
|
<Upload
|
||||||
|
fileList={draftFiles}
|
||||||
|
beforeUpload={(file) => {
|
||||||
|
handleAddFile(file);
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
onRemove={(file) => handleRemoveFile(file)}
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<Button icon={<UploadOutlined/>}>Выбрать файлы</Button>
|
||||||
|
</Upload>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default CreateTaskModalForm;
|
||||||
@ -0,0 +1,178 @@
|
|||||||
|
import {useDispatch, useSelector} from "react-redux";
|
||||||
|
import {Form, notification} from "antd";
|
||||||
|
import {useMemo, useRef, useState} from "react";
|
||||||
|
import {setOpenModalCreateTask} from "../../../../../Redux/Slices/tasksSlice.js";
|
||||||
|
import {useCreateTaskMutation, useUploadFileMutation} from "../../../../../Api/tasksApi.js";
|
||||||
|
|
||||||
|
|
||||||
|
const useCreateTaskModalForm = ({courseId}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const {
|
||||||
|
openModalCreateTask
|
||||||
|
} = useSelector((state) => state.tasks);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const [createtask, {isLoading}] = useCreateTaskMutation();
|
||||||
|
const [draftFiles, setDraftFiles] = useState([]);
|
||||||
|
const [uploadFile] = useUploadFileMutation();
|
||||||
|
|
||||||
|
const isModalOpen = openModalCreateTask;
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
form.resetFields();
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.value = "";
|
||||||
|
}
|
||||||
|
dispatch(setOpenModalCreateTask(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
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 handleRemoveFile = (file) => {
|
||||||
|
setDraftFiles((prev) => prev.filter((f) => f.uid !== file.uid));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
const content = editorRef.current?.value || "";
|
||||||
|
|
||||||
|
const taskData = {
|
||||||
|
title: values.title,
|
||||||
|
description: values.description || null,
|
||||||
|
text: content,
|
||||||
|
number: values.number || 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await createtask({
|
||||||
|
courseId,
|
||||||
|
taskData,
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
for (const file of draftFiles) {
|
||||||
|
try {
|
||||||
|
await uploadFile({
|
||||||
|
task_id: 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",
|
||||||
|
});
|
||||||
|
|
||||||
|
handleCancel();
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
title: "Ошибка",
|
||||||
|
description: error?.data?.detail || "Не удалось создать задание",
|
||||||
|
placement: "topRight",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
isModalOpen,
|
||||||
|
handleCancel,
|
||||||
|
form,
|
||||||
|
joditConfig,
|
||||||
|
editorRef,
|
||||||
|
handleOk,
|
||||||
|
handleAddFile,
|
||||||
|
handleRemoveFile,
|
||||||
|
draftFiles,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCreateTaskModalForm;
|
||||||
@ -126,7 +126,6 @@ const UpdateLessonModalForm = ({courseId}) => {
|
|||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
onRemove={(file) => handleRemoveFile(file)}
|
onRemove={(file) => handleRemoveFile(file)}
|
||||||
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
|
|
||||||
multiple
|
multiple
|
||||||
>
|
>
|
||||||
<Button icon={<UploadOutlined/>}>Выбрать файлы</Button>
|
<Button icon={<UploadOutlined/>}>Выбрать файлы</Button>
|
||||||
|
|||||||
@ -228,7 +228,7 @@ const useUpdateLessonModalForm = () => {
|
|||||||
askBeforePasteFromWord: false,
|
askBeforePasteFromWord: false,
|
||||||
defaultActionOnPaste: "insert_clear_html",
|
defaultActionOnPaste: "insert_clear_html",
|
||||||
spellcheck: true,
|
spellcheck: true,
|
||||||
placeholder: "Введите результаты приёма...",
|
placeholder: "Заполните содержимое лекционного материала",
|
||||||
showCharsCounter: true,
|
showCharsCounter: true,
|
||||||
showWordsCounter: true,
|
showWordsCounter: true,
|
||||||
showXPathInStatusbar: false,
|
showXPathInStatusbar: false,
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.js
|
|||||||
import CreateLessonModalForm from "./Components/CreateLessonModalForm/CreateLessonModalForm.jsx";
|
import CreateLessonModalForm from "./Components/CreateLessonModalForm/CreateLessonModalForm.jsx";
|
||||||
import ViewLessonModal from "./Components/ViewLessonModalForm/ViewLessonModal.jsx";
|
import ViewLessonModal from "./Components/ViewLessonModalForm/ViewLessonModal.jsx";
|
||||||
import UpdateLessonModalForm from "./Components/UpdateLessonModalForm/UpdateLessonModalForm.jsx";
|
import UpdateLessonModalForm from "./Components/UpdateLessonModalForm/UpdateLessonModalForm.jsx";
|
||||||
|
import CreateTaskModalForm from "./Components/CreateTaskModalForm/CreateTaskModalForm.jsx";
|
||||||
|
|
||||||
|
|
||||||
const {Title, Text} = Typography;
|
const {Title, Text} = Typography;
|
||||||
@ -37,6 +38,7 @@ const CourseDetailPage = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {courseId} = useParams();
|
const {courseId} = useParams();
|
||||||
const {
|
const {
|
||||||
|
tasksData,
|
||||||
isTeacherOrAdmin,
|
isTeacherOrAdmin,
|
||||||
lessonsData,
|
lessonsData,
|
||||||
userData,
|
userData,
|
||||||
@ -47,6 +49,10 @@ const CourseDetailPage = () => {
|
|||||||
handleOpenLesson,
|
handleOpenLesson,
|
||||||
handleEditLesson,
|
handleEditLesson,
|
||||||
handleDeleteLesson,
|
handleDeleteLesson,
|
||||||
|
handleCreateTask,
|
||||||
|
handleOpenTask,
|
||||||
|
handleEditTask,
|
||||||
|
handleDeleteTask,
|
||||||
} = useCourseDetailPage(courseId);
|
} = useCourseDetailPage(courseId);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@ -88,15 +94,25 @@ const CourseDetailPage = () => {
|
|||||||
</Empty>
|
</Empty>
|
||||||
) : (
|
) : (
|
||||||
<Row gutter={[24, 24]}>
|
<Row gutter={[24, 24]}>
|
||||||
{lessonsData.map((lesson, index) => (
|
{[...lessonsData, ...tasksData]
|
||||||
<Col xs={24} sm={12} lg={8} xl={6} key={lesson.id}>
|
.sort((a, b) => a.number - b.number)
|
||||||
|
.map((item) => {
|
||||||
|
const isLesson = item.__typename === "Lesson";
|
||||||
|
const isTask = item.__typename === "Task";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col xs={24} sm={12} lg={8} xl={6} key={item.id}>
|
||||||
<Card
|
<Card
|
||||||
hoverable
|
hoverable
|
||||||
style={{height: "100%", cursor: "pointer"}}
|
style={{height: "100%", cursor: "pointer"}}
|
||||||
onClick={() => handleOpenLesson(lesson)}
|
onClick={() =>
|
||||||
|
isLesson ? handleOpenLesson(item) : handleOpenTask(item)
|
||||||
|
}
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<Text strong>{lesson.number}. {lesson.title}</Text>
|
<Text strong>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
extra={
|
extra={
|
||||||
@ -107,15 +123,19 @@ const CourseDetailPage = () => {
|
|||||||
icon={<EditOutlined/>}
|
icon={<EditOutlined/>}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleEditLesson(lesson);
|
isLesson
|
||||||
|
? handleEditLesson(item)
|
||||||
|
: handleEditTask(item);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="Удалить лекцию?"
|
title={`Удалить ${isLesson ? "лекцию" : "задание"}?`}
|
||||||
description="Это действие нельзя отменить"
|
description="Это действие нельзя отменить"
|
||||||
onConfirm={(e) => {
|
onConfirm={(e) => {
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
handleDeleteLesson(lesson.id);
|
isLesson
|
||||||
|
? handleDeleteLesson(item.id)
|
||||||
|
: handleDeleteTask(item.id);
|
||||||
}}
|
}}
|
||||||
okText="Удалить"
|
okText="Удалить"
|
||||||
cancelText="Отмена"
|
cancelText="Отмена"
|
||||||
@ -132,37 +152,71 @@ const CourseDetailPage = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div style={{marginBottom: 16}}>
|
<div style={{marginBottom: 16}}>
|
||||||
{lesson.description ? (
|
<Space vertical>
|
||||||
|
{isTask && (
|
||||||
|
<Tag color="orange" size="small">
|
||||||
|
Задание
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{isLesson && (
|
||||||
|
<Tag color="blue" size="small">
|
||||||
|
Лекция
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{item.description ? (
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
{lesson.description.slice(0, 100)}...
|
{item.description.slice(0, 100)}
|
||||||
|
{item.description.length > 100 && "..."}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text type="secondary" italic>Описание отсутствует</Text>
|
<Text type="secondary" italic>
|
||||||
|
Описание отсутствует
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Text>
|
<Text>
|
||||||
Лекционный материал
|
{isLesson ? "Лекционный материал" : "Задание"}
|
||||||
</Text>
|
</Text>
|
||||||
<div style={{marginTop: 16, display: "flex", alignItems: "center", gap: 8}}>
|
|
||||||
<Avatar size="small" style={{backgroundColor: "#1890ff"}}>
|
<div
|
||||||
{userData?.first_name?.[0] || "У"}
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
size="small"
|
||||||
|
style={{backgroundColor: "#1890ff"}}
|
||||||
|
>
|
||||||
|
{item.creator?.first_name?.[0] || "У"}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Text type="secondary" style={{fontSize: 12}}>
|
<Text type="secondary" style={{fontSize: 12}}>
|
||||||
Создал: {lesson.creator?.first_name} {lesson.creator?.last_name}
|
Создал: {item.creator?.first_name}{" "}
|
||||||
|
{item.creator?.last_name}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CreateLessonModalForm
|
<CreateLessonModalForm
|
||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
/>
|
/>
|
||||||
<ViewLessonModal/>
|
<ViewLessonModal
|
||||||
|
courseId={courseId}
|
||||||
|
/>
|
||||||
<UpdateLessonModalForm/>
|
<UpdateLessonModalForm/>
|
||||||
|
|
||||||
|
<CreateTaskModalForm
|
||||||
|
courseId={courseId}
|
||||||
|
/>
|
||||||
{[CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData.role.title) && (
|
{[CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData.role.title) && (
|
||||||
<FloatButton.Group
|
<FloatButton.Group
|
||||||
placement={"left"}
|
placement={"left"}
|
||||||
@ -179,6 +233,7 @@ const CourseDetailPage = () => {
|
|||||||
<FloatButton
|
<FloatButton
|
||||||
icon={<FormOutlined/>}
|
icon={<FormOutlined/>}
|
||||||
tooltip="Задание"
|
tooltip="Задание"
|
||||||
|
onClick={handleCreateTask}
|
||||||
/>
|
/>
|
||||||
</FloatButton.Group>
|
</FloatButton.Group>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -11,6 +11,12 @@ import {useDeleteLessonMutation, useGetLessonsByCourseIdQuery} from "../../../Ap
|
|||||||
import {ROLES} from "../../../Core/constants.js";
|
import {ROLES} from "../../../Core/constants.js";
|
||||||
import CONFIG from "../../../Core/сonfig.js";
|
import CONFIG from "../../../Core/сonfig.js";
|
||||||
import {notification} from "antd";
|
import {notification} from "antd";
|
||||||
|
import {
|
||||||
|
setOpenModalCreateTask,
|
||||||
|
setSelectedTaskToUpdate,
|
||||||
|
setSelectedTaskToView
|
||||||
|
} from "../../../Redux/Slices/tasksSlice.js";
|
||||||
|
import {useGetTasksByCourseIdQuery} from "../../../Api/tasksApi.js";
|
||||||
|
|
||||||
|
|
||||||
const useCourseDetailPage = (courseId) => {
|
const useCourseDetailPage = (courseId) => {
|
||||||
@ -33,13 +39,21 @@ const useCourseDetailPage = (courseId) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: lessonsData,
|
data: lessonsData = [],
|
||||||
isLoading: isLessonsLoading,
|
isLoading: isLessonsLoading,
|
||||||
isError: isLessonsError,
|
isError: isLessonsError,
|
||||||
} = useGetLessonsByCourseIdQuery(courseId, {
|
} = useGetLessonsByCourseIdQuery(courseId, {
|
||||||
pollingInterval: 10000,
|
pollingInterval: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: tasksData = [],
|
||||||
|
isLoading: isTasksLoading,
|
||||||
|
isError: isTasksError
|
||||||
|
} = useGetTasksByCourseIdQuery(courseId, {
|
||||||
|
pollingInterval: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
const [
|
const [
|
||||||
deleteLesson,
|
deleteLesson,
|
||||||
] = useDeleteLessonMutation();
|
] = useDeleteLessonMutation();
|
||||||
@ -62,6 +76,10 @@ const useCourseDetailPage = (courseId) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteTask = async (taskId) => {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.document.title = `Система обучения lectio - Курс: ${courseData?.title}`;
|
window.document.title = `Система обучения lectio - Курс: ${courseData?.title}`;
|
||||||
}, [courseData]);
|
}, [courseData]);
|
||||||
@ -80,17 +98,34 @@ const useCourseDetailPage = (courseId) => {
|
|||||||
dispatch(setSelectedLessonToUpdate(lesson))
|
dispatch(setSelectedLessonToUpdate(lesson))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateTask = () => {
|
||||||
|
dispatch(setOpenModalCreateTask(true))
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenTask = (task) => {
|
||||||
|
dispatch(setSelectedTaskToView(task))
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditTask = (task) => {
|
||||||
|
dispatch(setSelectedTaskToUpdate(task))
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
tasksData,
|
||||||
isTeacherOrAdmin,
|
isTeacherOrAdmin,
|
||||||
lessonsData,
|
lessonsData,
|
||||||
userData,
|
userData,
|
||||||
courseData,
|
courseData,
|
||||||
isLoading: isUserLoading || isCourseLoading || isLessonsLoading,
|
isLoading: isUserLoading || isCourseLoading || isLessonsLoading || isTasksLoading,
|
||||||
isError: isUserError || isCourseError || isLessonsError,
|
isError: isUserError || isCourseError || isLessonsError || isTasksError,
|
||||||
handleCreateLesson,
|
handleCreateLesson,
|
||||||
handleOpenLesson,
|
handleOpenLesson,
|
||||||
handleEditLesson,
|
handleEditLesson,
|
||||||
handleDeleteLesson,
|
handleDeleteLesson,
|
||||||
|
handleCreateTask,
|
||||||
|
handleOpenTask,
|
||||||
|
handleEditTask,
|
||||||
|
handleDeleteTask,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
32
web/src/Redux/Slices/tasksSlice.js
Normal file
32
web/src/Redux/Slices/tasksSlice.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {createSlice} from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
selectedTaskToUpdate: null,
|
||||||
|
openModalCreateTask: false,
|
||||||
|
selectedTaskToView: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasksSlice = createSlice({
|
||||||
|
name: "tasks",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setSelectedTaskToUpdate: (state, action) => {
|
||||||
|
state.selectedTaskToUpdate = action.payload;
|
||||||
|
},
|
||||||
|
setOpenModalCreateTask: (state, action) => {
|
||||||
|
state.openModalCreateTask = action.payload;
|
||||||
|
},
|
||||||
|
setSelectedTaskToView: (state, action) => {
|
||||||
|
state.selectedTaskToView = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setSelectedTaskToUpdate,
|
||||||
|
setOpenModalCreateTask,
|
||||||
|
setSelectedTaskToView,
|
||||||
|
} = tasksSlice.actions;
|
||||||
|
|
||||||
|
export default tasksSlice.reducer;
|
||||||
@ -3,12 +3,14 @@ import authReducer from "./Slices/authSlice.js";
|
|||||||
import usersReducer from "./Slices/usersSlice.js";
|
import usersReducer from "./Slices/usersSlice.js";
|
||||||
import coursesReducer from "./Slices/coursesSlice.js";
|
import coursesReducer from "./Slices/coursesSlice.js";
|
||||||
import lessonReducer from "./Slices/lessonsSlice.js";
|
import lessonReducer from "./Slices/lessonsSlice.js";
|
||||||
|
import tasksReducer from "./Slices/tasksSlice.js";
|
||||||
import {authApi} from "../Api/authApi.js";
|
import {authApi} from "../Api/authApi.js";
|
||||||
import {usersApi} from "../Api/usersApi.js";
|
import {usersApi} from "../Api/usersApi.js";
|
||||||
import {rolesApi} from "../Api/rolesApi.js";
|
import {rolesApi} from "../Api/rolesApi.js";
|
||||||
import {statusesApi} from "../Api/statusesApi.js";
|
import {statusesApi} from "../Api/statusesApi.js";
|
||||||
import {coursesApi} from "../Api/coursesApi.js";
|
import {coursesApi} from "../Api/coursesApi.js";
|
||||||
import {lessonsApi} from "../Api/lessonsApi.js";
|
import {lessonsApi} from "../Api/lessonsApi.js";
|
||||||
|
import {tasksApi} from "../Api/tasksApi.js";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
@ -26,7 +28,10 @@ export const store = configureStore({
|
|||||||
[coursesApi.reducerPath]: coursesApi.reducer,
|
[coursesApi.reducerPath]: coursesApi.reducer,
|
||||||
|
|
||||||
lessons: lessonReducer,
|
lessons: lessonReducer,
|
||||||
[lessonsApi.reducerPath]: lessonsApi.reducer
|
[lessonsApi.reducerPath]: lessonsApi.reducer,
|
||||||
|
|
||||||
|
tasks: tasksReducer,
|
||||||
|
[tasksApi.reducerPath]: tasksApi.reducer,
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) => (
|
middleware: (getDefaultMiddleware) => (
|
||||||
getDefaultMiddleware().concat(
|
getDefaultMiddleware().concat(
|
||||||
@ -35,7 +40,8 @@ export const store = configureStore({
|
|||||||
rolesApi.middleware,
|
rolesApi.middleware,
|
||||||
statusesApi.middleware,
|
statusesApi.middleware,
|
||||||
coursesApi.middleware,
|
coursesApi.middleware,
|
||||||
lessonsApi.middleware
|
lessonsApi.middleware,
|
||||||
|
tasksApi.middleware,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user