сделал управление файлами
This commit is contained in:
parent
79e343374e
commit
eb498051a1
@ -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,
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -93,4 +93,5 @@ export const {
|
||||
useGetLessonFilesListQuery,
|
||||
useGetDownloadFileQuery,
|
||||
useUploadFileMutation,
|
||||
useDeleteFileMutation,
|
||||
} = lessonsApi;
|
||||
|
||||
@ -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}) => {
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{isFilesLoading ? (
|
||||
<Spin/>
|
||||
) : files.length > 0 ? (
|
||||
files.map((file) => (
|
||||
<Row key={file.id} align="middle" justify="space-between">
|
||||
<span>{file.filename || "Не указан"}</span>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => downloadFile(file.id, file.filename)}
|
||||
loading={downloadingFiles[file.id] || false}
|
||||
disabled={downloadingFiles[file.id] || deletingFiles[file.id] || false}
|
||||
type={"dashed"}
|
||||
style={{marginRight: 8}}
|
||||
>
|
||||
{downloadingFiles[file.id] ? "Загрузка..." : "Скачать"}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={"Вы уверены, что хотите удалить файл?"}
|
||||
onConfirm={() => deleteFile(file.id, file.filename)}
|
||||
>
|
||||
<Button
|
||||
loading={deletingFiles[file.id] || false}
|
||||
disabled={deletingFiles[file.id] || downloadingFiles[file.id] || false}
|
||||
type={"dashed"}
|
||||
danger
|
||||
>
|
||||
{deletingFiles[file.id] ? "Удаление..." : "Удалить"}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
<Divider/>
|
||||
</Row>
|
||||
))
|
||||
) : (
|
||||
<p>Файлы отсутствуют</p>
|
||||
)}
|
||||
<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,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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -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 = () => {
|
||||
)}
|
||||
|
||||
<Divider/>
|
||||
<Title level={3}>Прикрепленные файлы</Title>
|
||||
{isCurrentLessonFilesLoading ? (
|
||||
<Spin/>
|
||||
) : currentLessonFiles.length > 0 ? (
|
||||
currentLessonFiles.map((file) => (
|
||||
<Row key={file.id} align="middle" justify="space-between">
|
||||
<span>{file.filename || "Не указан"}</span>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => downloadFile(file.id, file.filename)}
|
||||
loading={downloadingFiles[file.id] || false}
|
||||
disabled={downloadingFiles[file.id] || false}
|
||||
type={"dashed"}
|
||||
style={{marginRight: 8}}
|
||||
>
|
||||
{downloadingFiles[file.id] ? "Загрузка..." : "Скачать"}
|
||||
</Button>
|
||||
</div>
|
||||
<Divider/>
|
||||
</Row>
|
||||
))
|
||||
) : (
|
||||
<p>Файлы отсутствуют</p>
|
||||
)}
|
||||
|
||||
<div style={{textAlign: "right"}}>
|
||||
<Button onClick={handleClose}>
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {setSelectedLessonToView} from "../../../../../Redux/Slices/lessonsSlice.js";
|
||||
import {useGetLessonFilesListQuery} from "../../../../../Api/lessonsApi.js";
|
||||
import {notification} from "antd";
|
||||
import CONFIG from "../../../../../Core/сonfig.js";
|
||||
import {useState} from "react";
|
||||
|
||||
|
||||
const useViewLessonModal = () => {
|
||||
@ -15,10 +19,102 @@ const useViewLessonModal = () => {
|
||||
dispatch(setSelectedLessonToView(null));
|
||||
};
|
||||
|
||||
const {
|
||||
data: currentLessonFiles = [],
|
||||
isLoading: isCurrentLessonFilesLoading,
|
||||
isError: isCurrentLessonFilesError
|
||||
} = useGetLessonFilesListQuery(selectedLessonToView?.id, {
|
||||
skip: !selectedLessonToView?.id
|
||||
});
|
||||
|
||||
const [downloadingFiles, setDownloadingFiles] = useState({});
|
||||
|
||||
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}));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
selectedLessonToView,
|
||||
modalIsOpen,
|
||||
handleClose,
|
||||
currentLessonFiles,
|
||||
isCurrentLessonFilesLoading,
|
||||
isCurrentLessonFilesError,
|
||||
downloadFile,
|
||||
downloadingFiles
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user