diff --git a/api/app/controllers/lessons_router.py b/api/app/controllers/lessons_router.py
index 9ed81a9..974159a 100644
--- a/api/app/controllers/lessons_router.py
+++ b/api/app/controllers/lessons_router.py
@@ -95,7 +95,7 @@ async def delete_lesson(
@lessons_router.get(
'/files/{lesson_id}/',
- response_model=Optional[ReadLessonFile],
+ response_model=Optional[List[ReadLessonFile]],
summary='Get a files list by lesson ID',
description='Get a files list by lesson ID',
)
@@ -111,8 +111,6 @@ async def get_files(
@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,
diff --git a/api/app/infrastructure/lesson_files_service.py b/api/app/infrastructure/lesson_files_service.py
index 76d590a..2069ed2 100644
--- a/api/app/infrastructure/lesson_files_service.py
+++ b/api/app/infrastructure/lesson_files_service.py
@@ -20,15 +20,15 @@ class LessonFilesService:
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)
+ lesson_file = await self.lesson_files_repository.get_by_id(file_id)
if not lesson_file:
raise HTTPException(404, "Файл с таким ID не найден")
return FileResponse(
- lesson_file.filename,
+ lesson_file.file_path,
media_type=self.get_media_type(lesson_file.filename),
- filename=os.path.basename(lesson_file.file_path),
+ filename=os.path.basename(lesson_file.filename),
)
async def get_files_list_by_lesson(self, lesson_id: int) -> List[ReadLessonFile]:
@@ -58,8 +58,8 @@ class LessonFilesService:
file_path = await self.save_file(file, f'uploads/lessons/{lesson.id}')
lesson_file_model = LessonFile(
- filename=file_path,
- file_path=file.filename,
+ filename=file.filename,
+ file_path=file_path,
lesson_id=lesson.id,
)
diff --git a/web/src/Api/lessonsApi.js b/web/src/Api/lessonsApi.js
index 6b3558e..da8d82f 100644
--- a/web/src/Api/lessonsApi.js
+++ b/web/src/Api/lessonsApi.js
@@ -93,4 +93,5 @@ export const {
useGetLessonFilesListQuery,
useGetDownloadFileQuery,
useUploadFileMutation,
+ useDeleteFileMutation,
} = lessonsApi;
diff --git a/web/src/Components/Pages/CourseDetailPage/Components/UpdateLessonModalForm/UpdateLessonModalForm.jsx b/web/src/Components/Pages/CourseDetailPage/Components/UpdateLessonModalForm/UpdateLessonModalForm.jsx
index f7b4984..210a953 100644
--- a/web/src/Components/Pages/CourseDetailPage/Components/UpdateLessonModalForm/UpdateLessonModalForm.jsx
+++ b/web/src/Components/Pages/CourseDetailPage/Components/UpdateLessonModalForm/UpdateLessonModalForm.jsx
@@ -1,7 +1,8 @@
import useUpdateLessonModalForm from "./useUpdateLessonModalForm.js";
-import {Button, Form, Input, InputNumber, Modal, Upload} from "antd";
+import {Button, Divider, Form, Input, InputNumber, Modal, Popconfirm, Row, Spin, Upload} from "antd";
import JoditEditor from "jodit-react";
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
+import {UploadOutlined} from "@ant-design/icons";
const {TextArea} = Input;
@@ -16,6 +17,15 @@ const UpdateLessonModalForm = ({courseId}) => {
isLoading,
initialContent,
currentLesson,
+ isFilesLoading,
+ downloadFile,
+ files,
+ downloadingFiles,
+ deletingFiles,
+ deleteFile,
+ draftFiles,
+ handleAddFile,
+ handleRemoveFile,
} = useUpdateLessonModalForm({courseId});
if (isLoading) {
@@ -71,6 +81,57 @@ const UpdateLessonModalForm = ({courseId}) => {
/>
+
+ {isFilesLoading ? (
+
+ ) : files.length > 0 ? (
+ files.map((file) => (
+
+ {file.filename || "Не указан"}
+
+
+
deleteFile(file.id, file.filename)}
+ >
+
+
+
+
+
+ ))
+ ) : (
+
Файлы отсутствуют
+ )}
+
+ {
+ handleAddFile(file);
+ return false;
+ }}
+ onRemove={(file) => handleRemoveFile(file)}
+ accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
+ multiple
+ >
+
+
+
);
diff --git a/web/src/Components/Pages/CourseDetailPage/Components/UpdateLessonModalForm/useUpdateLessonModalForm.js b/web/src/Components/Pages/CourseDetailPage/Components/UpdateLessonModalForm/useUpdateLessonModalForm.js
index 137ff3c..ac7f3f6 100644
--- a/web/src/Components/Pages/CourseDetailPage/Components/UpdateLessonModalForm/useUpdateLessonModalForm.js
+++ b/web/src/Components/Pages/CourseDetailPage/Components/UpdateLessonModalForm/useUpdateLessonModalForm.js
@@ -1,8 +1,14 @@
import {useDispatch, useSelector} from "react-redux";
import {Form, notification} from "antd";
-import {useEffect, useMemo, useRef} from "react";
-import {useUpdateLessonMutation} from "../../../../../Api/lessonsApi.js";
+import {useEffect, useMemo, useRef, useState} from "react";
+import {
+ useDeleteFileMutation,
+ useGetLessonFilesListQuery,
+ useUpdateLessonMutation,
+ useUploadFileMutation
+} from "../../../../../Api/lessonsApi.js";
import {setSelectedLessonToUpdate} from "../../../../../Redux/Slices/lessonsSlice.js";
+import CONFIG from "../../../../../Core/сonfig.js";
const useUpdateLessonModalForm = () => {
@@ -11,9 +17,119 @@ const useUpdateLessonModalForm = () => {
const editorRef = useRef(null);
const {selectedLessonToUpdate} = useSelector((state) => state.lessons);
- const [updateLesson, {isLoading}] = useUpdateLessonMutation();
+ const [updateLesson, {isLoading: isLoadingUpdateLesson}] = useUpdateLessonMutation();
const isModalOpen = selectedLessonToUpdate !== null;
+ const [draftFiles, setDraftFiles] = useState([]);
+ const [uploadFile] = useUploadFileMutation();
+ const [deleteFileMut] = useDeleteFileMutation();
+ const [downloadingFiles, setDownloadingFiles] = useState({});
+ const [deletingFiles, setDeletingFiles] = useState({});
+
+ const {
+ data: currentLessonFiles = [],
+ isLoading: isCurrentLessonFilesLoading,
+ isError: isCurrentLessonFilesError
+ } = useGetLessonFilesListQuery(selectedLessonToUpdate?.id, {
+ skip: !selectedLessonToUpdate?.id
+ });
+
+ const downloadFile = async (fileId, fileName) => {
+ try {
+ setDownloadingFiles((prev) => ({ ...prev, [fileId]: true }));
+
+ const token = localStorage.getItem('access_token');
+ if (!token) {
+ notification.error({
+ title: "Ошибка",
+ description: "Токен не найден",
+ placement: "topRight",
+ });
+ return;
+ }
+
+ const response = await fetch(`${CONFIG.BASE_URL}/lessons/file/${fileId}/`, {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ notification.error({
+ title: "Ошибка",
+ description: errorText || "Не удалось скачать файл",
+ placement: "topRight",
+ });
+ return;
+ }
+
+ const contentType = response.headers.get('content-type');
+ if (!contentType || contentType.includes('text/html')) {
+ const errorText = await response.text();
+ notification.error({
+ title: "Ошибка",
+ description: errorText || "Не удалось скачать файл",
+ placement: "topRight",
+ });
+ return;
+ }
+
+ let safeFileName = fileName || "file";
+ if (!safeFileName.match(/\.[a-zA-Z0-9]+$/)) {
+ if (contentType.includes('application/pdf')) {
+ safeFileName += '.pdf';
+ } else if (contentType.includes('image/jpeg')) {
+ safeFileName += '.jpg';
+ } else if (contentType.includes('image/png')) {
+ safeFileName += '.png';
+ } else {
+ safeFileName += '.bin';
+ }
+ }
+
+ const blob = await response.blob();
+ const downloadUrl = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = downloadUrl;
+ link.setAttribute("download", safeFileName);
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(downloadUrl);
+ } catch (error) {
+ notification.error({
+ title: "Ошибка",
+ description: error.message || "Не удалось скачать файл",
+ placement: "topRight",
+ });
+ } finally {
+ setDownloadingFiles((prev) => ({...prev, [fileId]: false}));
+ }
+ };
+
+ const deleteFile = async (fileId, fileName) => {
+ try {
+ setDeletingFiles((prev) => ({...prev, [fileId]: true}));
+ await deleteFileMut(fileId).unwrap();
+ notification.success({
+ title: "Файл удален",
+ description: `Файл ${fileName || "неизвестный"} успешно удален.`,
+ placement: "topRight",
+ });
+ } catch (error) {
+ console.error("Error deleting file:", error);
+ notification.error({
+ title: "Ошибка при удалении файла",
+ description: `Не удалось удалить файл ${fileName || "неизвестный"}: ${error.data?.detail || error.message}`,
+ placement: "topRight",
+ });
+ } finally {
+ setDeletingFiles((prev) => ({...prev, [fileId]: false}));
+ }
+ };
useEffect(() => {
if (selectedLessonToUpdate && isModalOpen) {
@@ -56,6 +172,27 @@ const useUpdateLessonModalForm = () => {
lessonData,
}).unwrap();
+ for (const file of draftFiles) {
+ try {
+ await uploadFile({
+ lesson_id: selectedLessonToUpdate.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",
+ });
+ }
+ }
+
+ setDraftFiles([]);
+
notification.success({
title: "Успех",
description: "Лекция успешно обновлена!",
@@ -135,6 +272,24 @@ const useUpdateLessonModalForm = () => {
[]
);
+ 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 initialContent = selectedLessonToUpdate?.text || "";
return {
@@ -144,9 +299,19 @@ const useUpdateLessonModalForm = () => {
form,
joditConfig,
editorRef,
- isLoading,
+ isLoading: isLoadingUpdateLesson,
initialContent,
currentLesson: selectedLessonToUpdate,
+ currentLessonFiles,
+ isFilesLoading: isCurrentLessonFilesLoading,
+ downloadFile,
+ files: currentLessonFiles,
+ downloadingFiles,
+ deletingFiles,
+ deleteFile,
+ draftFiles,
+ handleAddFile,
+ handleRemoveFile,
};
};
diff --git a/web/src/Components/Pages/CourseDetailPage/Components/ViewLessonModalForm/ViewLessonModal.jsx b/web/src/Components/Pages/CourseDetailPage/Components/ViewLessonModalForm/ViewLessonModal.jsx
index 82fccc1..fab86ba 100644
--- a/web/src/Components/Pages/CourseDetailPage/Components/ViewLessonModalForm/ViewLessonModal.jsx
+++ b/web/src/Components/Pages/CourseDetailPage/Components/ViewLessonModalForm/ViewLessonModal.jsx
@@ -1,5 +1,5 @@
import useViewLessonModal from "./useViewLessonModal.js";
-import {Avatar, Button, Col, Divider, Modal, Space, Typography} from "antd";
+import {Avatar, Button, Col, Divider, Modal, Popconfirm, Row, Space, Spin, Typography} from "antd";
import {CloseOutlined, UserOutlined} from "@ant-design/icons";
@@ -10,6 +10,11 @@ const ViewLessonModal = () => {
selectedLessonToView,
modalIsOpen,
handleClose,
+ currentLessonFiles,
+ isCurrentLessonFilesLoading,
+ isCurrentLessonFilesError,
+ downloadFile,
+ downloadingFiles
} = useViewLessonModal();
return (
@@ -72,6 +77,30 @@ const ViewLessonModal = () => {
)}
+ Прикрепленные файлы
+ {isCurrentLessonFilesLoading ? (
+
+ ) : currentLessonFiles.length > 0 ? (
+ currentLessonFiles.map((file) => (
+
+ {file.filename || "Не указан"}
+
+
+
+
+
+ ))
+ ) : (
+ Файлы отсутствуют
+ )}