сделал загрузку файлов при создании
This commit is contained in:
parent
2dd0419726
commit
79e343374e
38
api/app/application/lesson_files_repository.py
Normal file
38
api/app/application/lesson_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 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
|
||||
@ -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)
|
||||
|
||||
11
api/app/domain/entities/lesson_files.py
Normal file
11
api/app/domain/entities/lesson_files.py
Normal 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
|
||||
118
api/app/infrastructure/lesson_files_service.py
Normal file
118
api/app/infrastructure/lesson_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.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"
|
||||
@ -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
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user