сделал загрузку файлов при создании

This commit is contained in:
Андрей Дувакин 2025-11-29 00:55:32 +05:00
parent 2dd0419726
commit 79e343374e
8 changed files with 338 additions and 5 deletions

View File

@ -0,0 +1,38 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import LessonFile
class LessonFilesRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_id(self, file_id: int) -> Optional[LessonFile]:
query = (
select(LessonFile)
.filter_by(id=file_id)
)
result = await self.db.execute(query)
return result.scalars().first()
async def get_by_lesson_id(self, lesson_id: int) -> Optional[LessonFile]:
query = (
select(LessonFile)
.filter_by(lesson_id=lesson_id)
)
result = await self.db.execute(query)
return result.scalars().all()
async def create(self, lesson_file: LessonFile) -> LessonFile:
self.db.add(lesson_file)
await self.db.commit()
await self.db.refresh(lesson_file)
return lesson_file
async def delete(self, lesson_file: LessonFile) -> LessonFile:
await self.db.delete(lesson_file)
await self.db.commit()
return lesson_file

View File

@ -1,12 +1,15 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, status
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.lesson_files import ReadLessonFile
from app.domain.entities.lessons import LessonCreate, LessonUpdate, LessonRead
from app.domain.models import User
from app.infrastructure.dependencies import require_auth_user, require_teacher, require_admin
from app.infrastructure.lesson_files_service import LessonFilesService
from app.infrastructure.lessons_service import LessonsService
lessons_router = APIRouter()
@ -88,3 +91,64 @@ async def delete_lesson(
lessons_service = LessonsService(db)
await lessons_service.delete(lesson_id, current_user)
return None
@lessons_router.get(
'/files/{lesson_id}/',
response_model=Optional[ReadLessonFile],
summary='Get a files list by lesson ID',
description='Get a files list by lesson ID',
)
async def get_files(
lesson_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_auth_user),
):
lesson_files_service = LessonFilesService(db)
return await lesson_files_service.get_files_list_by_lesson(lesson_id)
@lessons_router.get(
'/file/{file_id}/',
response_class=FileResponse,
summary='Get a file by ID',
description='Get a file by ID',
)
async def get_file(
file_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_auth_user),
):
lesson_files_service = LessonFilesService(db)
return await lesson_files_service.get_file_by_id(file_id)
@lessons_router.post(
'/files/{lesson_id}/upload/',
response_model=ReadLessonFile,
summary='Upload a file',
description='Upload a file',
)
async def upload_file(
lesson_id: int,
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
user: User = Depends(require_teacher),
):
lesson_files_service = LessonFilesService(db)
return await lesson_files_service.upload_file(lesson_id, file)
@lessons_router.delete(
'/files/{file_id}/',
response_model=Optional[ReadLessonFile],
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),
):
lesson_files_service = LessonFilesService(db)
return await lesson_files_service.delete_file(file_id)

View File

@ -0,0 +1,11 @@
from pydantic import BaseModel
class ReadLessonFile(BaseModel):
id: int
filename: str
file_path: str
lesson_id: int
class Config:
from_attributes = True

View 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.lesson_files_repository import LessonFilesRepository
from app.application.lessons_repository import LessonsRepository
from app.domain.entities.lesson_files import ReadLessonFile
from app.domain.models import LessonFile
class LessonFilesService:
def __init__(self, db: AsyncSession):
self.lesson_files_repository = LessonFilesRepository(db)
self.lessons_repository = LessonsRepository(db)
async def get_file_by_id(self, file_id: int) -> FileResponse:
lesson_file = self.lesson_files_repository.get_by_id(file_id)
if not lesson_file:
raise HTTPException(404, "Файл с таким ID не найден")
return FileResponse(
lesson_file.filename,
media_type=self.get_media_type(lesson_file.filename),
filename=os.path.basename(lesson_file.file_path),
)
async def get_files_list_by_lesson(self, lesson_id: int) -> List[ReadLessonFile]:
lesson = await self.lessons_repository.get_by_id(lesson_id)
if lesson is None:
raise HTTPException(404, "Лекционный материал не найден")
lesson_files = await self.lesson_files_repository.get_by_lesson_id(lesson_id)
response = []
for lesson_file in lesson_files:
response.append(
ReadLessonFile.model_validate(
lesson_file
)
)
return response
async def upload_file(self, lesson_id: int, file: UploadFile) -> ReadLessonFile:
lesson = await self.lessons_repository.get_by_id(lesson_id)
if lesson is None:
raise HTTPException(404, "Лекционный материал не найден")
file_path = await self.save_file(file, f'uploads/lessons/{lesson.id}')
lesson_file_model = LessonFile(
filename=file_path,
file_path=file.filename,
lesson_id=lesson.id,
)
lesson_file_model = await self.lesson_files_repository.create(lesson_file_model)
return ReadLessonFile.model_validate(lesson_file_model)
async def delete_file(self, file_id: int) -> ReadLessonFile:
lesson_file = await self.lesson_files_repository.get_by_id(file_id)
if lesson_file is None:
raise HTTPException(404, "Файл не найден")
if not os.path.exists(lesson_file.file_path):
raise HTTPException(404, "Файл не найден на диске")
if os.path.exists(lesson_file.file_path):
os.remove(lesson_file.file_path)
lesson_file = await self.lesson_files_repository.delete(lesson_file)
return ReadLessonFile.model_validate(lesson_file)
async def save_file(self, file: UploadFile, upload_dir: str = 'uploads/lessons') -> 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"

View File

@ -7,3 +7,4 @@ werkzeug==3.1.3
pyjwt==2.9.0
fastapi==0.115.0
pydantic[email]==2.11.4
aiofiles==25.1.0

View File

@ -44,6 +44,43 @@ export const lessonsApi = createApi({
}),
invalidatesTags: ["lesson"],
}),
getLessonFilesList: builder.query({
query: (lessonId) => ({
url: `/lessons/files/${lessonId}/`,
method: "GET",
}),
providesTags: ["lesson"],
}),
getDownloadFile: builder.query({
query: (fileId) => ({
url: `/lessons/file/${fileId}/`,
method: "GET",
}),
providesTags: ["lesson"],
}),
uploadFile: builder.mutation({
query: ({lesson_id, fileData}) => {
if (!(fileData instanceof File)) {
throw new Error('Invalid file object');
}
const formData = new FormData();
formData.append('file', fileData);
return {
url: `/lessons/files/${lesson_id}/upload/`,
method: 'POST',
formData: true,
body: formData,
};
},
invalidatesTags: ["lesson"],
}),
deleteFile: builder.mutation({
query: (fileId) => ({
url: `/lessons/files/${fileId}/`,
method: "DELETE",
}),
invalidatesTags: ["lesson"],
}),
}),
});
@ -53,4 +90,7 @@ export const {
useCreateLessonMutation,
useUpdateLessonMutation,
useDeleteLessonMutation,
useGetLessonFilesListQuery,
useGetDownloadFileQuery,
useUploadFileMutation,
} = lessonsApi;

View File

@ -1,6 +1,7 @@
import useCreateLessonModalForm from "./useCreateLessonModalForm.js";
import {Button, Form, Input, InputNumber, Modal, Upload} from "antd";
import JoditEditor from "jodit-react";
import {UploadOutlined} from "@ant-design/icons";
const {TextArea} = Input;
@ -13,6 +14,9 @@ const CreateLessonModalForm = ({courseId}) => {
joditConfig,
editorRef,
isLoading,
handleAddFile,
handleRemoveFile,
draftFiles,
} = useCreateLessonModalForm({courseId});
return (
@ -61,6 +65,21 @@ const CreateLessonModalForm = ({courseId}) => {
/>
</div>
</Form.Item>
<Form.Item name="files" label="Прикрепить файлы">
<Upload
fileList={draftFiles}
beforeUpload={(file) => {
handleAddFile(file);
return false;
}}
onRemove={(file) => handleRemoveFile(file)}
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
multiple
>
<Button icon={<UploadOutlined/>}>Выбрать файлы</Button>
</Upload>
</Form.Item>
</Form>
</Modal>
);

View File

@ -1,8 +1,8 @@
import {useDispatch, useSelector} from "react-redux";
import {setOpenModalCreateLesson} from "../../../../../Redux/Slices/lessonsSlice.js";
import {Form, notification} from "antd";
import {useMemo, useRef} from "react";
import {useCreateLessonMutation} from "../../../../../Api/lessonsApi.js";
import {useMemo, useRef, useState} from "react";
import {useCreateLessonMutation, useUploadFileMutation} from "../../../../../Api/lessonsApi.js";
const useCreateLessonModalForm = ({courseId}) => {
@ -13,6 +13,8 @@ const useCreateLessonModalForm = ({courseId}) => {
const [form] = Form.useForm();
const [createLesson, {isLoading}] = useCreateLessonMutation();
const [draftFiles, setDraftFiles] = useState([]);
const [uploadFile] = useUploadFileMutation();
const isModalOpen = openModalCreateLesson;
const editorRef = useRef(null);
@ -25,6 +27,24 @@ const useCreateLessonModalForm = ({courseId}) => {
dispatch(setOpenModalCreateLesson(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();
@ -37,11 +57,30 @@ const useCreateLessonModalForm = ({courseId}) => {
number: values.number || 1,
};
await createLesson({
const response = await createLesson({
courseId,
lessonData,
}).unwrap();
for (const file of draftFiles) {
try {
await uploadFile({
lesson_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: "Лекция успешно создана!",
@ -130,6 +169,9 @@ const useCreateLessonModalForm = ({courseId}) => {
joditConfig,
editorRef,
handleOk,
handleAddFile,
handleRemoveFile,
draftFiles,
}
};