сделал загрузку файлов при создании
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 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 sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from starlette.responses import FileResponse
|
||||||
|
|
||||||
from app.database.session import get_db
|
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.entities.lessons import LessonCreate, LessonUpdate, LessonRead
|
||||||
from app.domain.models import User
|
from app.domain.models import User
|
||||||
from app.infrastructure.dependencies import require_auth_user, require_teacher, require_admin
|
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
|
from app.infrastructure.lessons_service import LessonsService
|
||||||
|
|
||||||
lessons_router = APIRouter()
|
lessons_router = APIRouter()
|
||||||
@ -88,3 +91,64 @@ async def delete_lesson(
|
|||||||
lessons_service = LessonsService(db)
|
lessons_service = LessonsService(db)
|
||||||
await lessons_service.delete(lesson_id, current_user)
|
await lessons_service.delete(lesson_id, current_user)
|
||||||
return None
|
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
|
pyjwt==2.9.0
|
||||||
fastapi==0.115.0
|
fastapi==0.115.0
|
||||||
pydantic[email]==2.11.4
|
pydantic[email]==2.11.4
|
||||||
|
aiofiles==25.1.0
|
||||||
@ -44,6 +44,43 @@ export const lessonsApi = createApi({
|
|||||||
}),
|
}),
|
||||||
invalidatesTags: ["lesson"],
|
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,
|
useCreateLessonMutation,
|
||||||
useUpdateLessonMutation,
|
useUpdateLessonMutation,
|
||||||
useDeleteLessonMutation,
|
useDeleteLessonMutation,
|
||||||
|
useGetLessonFilesListQuery,
|
||||||
|
useGetDownloadFileQuery,
|
||||||
|
useUploadFileMutation,
|
||||||
} = lessonsApi;
|
} = lessonsApi;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import useCreateLessonModalForm from "./useCreateLessonModalForm.js";
|
import useCreateLessonModalForm from "./useCreateLessonModalForm.js";
|
||||||
import {Button, Form, Input, InputNumber, Modal, Upload} from "antd";
|
import {Button, Form, Input, InputNumber, Modal, Upload} from "antd";
|
||||||
import JoditEditor from "jodit-react";
|
import JoditEditor from "jodit-react";
|
||||||
|
import {UploadOutlined} from "@ant-design/icons";
|
||||||
|
|
||||||
const {TextArea} = Input;
|
const {TextArea} = Input;
|
||||||
|
|
||||||
@ -13,6 +14,9 @@ const CreateLessonModalForm = ({courseId}) => {
|
|||||||
joditConfig,
|
joditConfig,
|
||||||
editorRef,
|
editorRef,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
handleAddFile,
|
||||||
|
handleRemoveFile,
|
||||||
|
draftFiles,
|
||||||
} = useCreateLessonModalForm({courseId});
|
} = useCreateLessonModalForm({courseId});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,6 +65,21 @@ const CreateLessonModalForm = ({courseId}) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</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>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import {useDispatch, useSelector} from "react-redux";
|
import {useDispatch, useSelector} from "react-redux";
|
||||||
import {setOpenModalCreateLesson} from "../../../../../Redux/Slices/lessonsSlice.js";
|
import {setOpenModalCreateLesson} from "../../../../../Redux/Slices/lessonsSlice.js";
|
||||||
import {Form, notification} from "antd";
|
import {Form, notification} from "antd";
|
||||||
import {useMemo, useRef} from "react";
|
import {useMemo, useRef, useState} from "react";
|
||||||
import {useCreateLessonMutation} from "../../../../../Api/lessonsApi.js";
|
import {useCreateLessonMutation, useUploadFileMutation} from "../../../../../Api/lessonsApi.js";
|
||||||
|
|
||||||
|
|
||||||
const useCreateLessonModalForm = ({courseId}) => {
|
const useCreateLessonModalForm = ({courseId}) => {
|
||||||
@ -13,6 +13,8 @@ const useCreateLessonModalForm = ({courseId}) => {
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const [createLesson, {isLoading}] = useCreateLessonMutation();
|
const [createLesson, {isLoading}] = useCreateLessonMutation();
|
||||||
|
const [draftFiles, setDraftFiles] = useState([]);
|
||||||
|
const [uploadFile] = useUploadFileMutation();
|
||||||
|
|
||||||
const isModalOpen = openModalCreateLesson;
|
const isModalOpen = openModalCreateLesson;
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
@ -25,6 +27,24 @@ const useCreateLessonModalForm = ({courseId}) => {
|
|||||||
dispatch(setOpenModalCreateLesson(false));
|
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 () => {
|
const handleOk = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
@ -37,11 +57,30 @@ const useCreateLessonModalForm = ({courseId}) => {
|
|||||||
number: values.number || 1,
|
number: values.number || 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
await createLesson({
|
const response = await createLesson({
|
||||||
courseId,
|
courseId,
|
||||||
lessonData,
|
lessonData,
|
||||||
}).unwrap();
|
}).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({
|
notification.success({
|
||||||
title: "Успех",
|
title: "Успех",
|
||||||
description: "Лекция успешно создана!",
|
description: "Лекция успешно создана!",
|
||||||
@ -130,6 +169,9 @@ const useCreateLessonModalForm = ({courseId}) => {
|
|||||||
joditConfig,
|
joditConfig,
|
||||||
editorRef,
|
editorRef,
|
||||||
handleOk,
|
handleOk,
|
||||||
|
handleAddFile,
|
||||||
|
handleRemoveFile,
|
||||||
|
draftFiles,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user