сделал создание и просмотр лекции

This commit is contained in:
Андрей Дувакин 2025-11-28 22:56:51 +05:00
parent 87a14f0eb1
commit 4ec6e96525
10 changed files with 352 additions and 59 deletions

View File

@ -1,6 +1,8 @@
from typing import Optional from typing import Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.domain.entities.users import UserRead
class LessonBase(BaseModel): class LessonBase(BaseModel):
title: str = Field(..., max_length=250) title: str = Field(..., max_length=250)
@ -10,7 +12,7 @@ class LessonBase(BaseModel):
class LessonCreate(LessonBase): class LessonCreate(LessonBase):
course_id: int pass
class LessonUpdate(BaseModel): class LessonUpdate(BaseModel):
@ -25,5 +27,7 @@ class LessonRead(LessonBase):
course_id: int course_id: int
creator_id: int creator_id: int
creator: UserRead
class Config: class Config:
from_attributes = True from_attributes = True

View File

@ -18,6 +18,6 @@ class Lesson(RootTable):
creator_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False) creator_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False)
course: Mapped['Course'] = relationship('Course', back_populates='lessons') course: Mapped['Course'] = relationship('Course', back_populates='lessons')
creator: Mapped['User'] = relationship('User', back_populates='created_lessons') creator: Mapped['User'] = relationship('User', back_populates='created_lessons', lazy='joined')
files: Mapped[List['LessonFile']] = relationship('LessonFile', back_populates='lesson') files: Mapped[List['LessonFile']] = relationship('LessonFile', back_populates='lesson')

View File

@ -48,7 +48,6 @@ def require_admin(user: User = Depends(require_auth_user)):
def require_teacher(user: User = Depends(require_auth_user)): def require_teacher(user: User = Depends(require_auth_user)):
print(user.role.title, user.role.title not in [UserRoles.TEACHER, Settings().root_role_name], [UserRoles.TEACHER, Settings().root_role_name])
if user.role.title not in [UserRoles.TEACHER, Settings().root_role_name]: if user.role.title not in [UserRoles.TEACHER, Settings().root_role_name]:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Ошибка доступа') raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Ошибка доступа')

View File

@ -1,77 +1,68 @@
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 CreateLessonModalForm = () => { const CreateLessonModalForm = ({courseId}) => {
const { const {
isModalOpen, isModalOpen,
handleCancel, handleCancel,
handleOk,
form, form,
joditConfig, joditConfig,
editorRef, editorRef,
} = useCreateLessonModalForm(); isLoading,
} = useCreateLessonModalForm({courseId});
return ( return (
<Modal <Modal
title="Создание лекционного материала"
open={isModalOpen} open={isModalOpen}
onCancel={handleCancel} onCancel={handleCancel}
footer={null} width={1000}
title={"Создание лекционного материала"} footer={[
style={{ <Button key="cancel" onClick={handleCancel}>
minWidth: "70%", Отмена
minHeight: "80%", </Button>,
}} <Button
key="submit"
type="primary"
loading={isLoading}
onClick={handleOk}
>
Создать лекцию
</Button>,
]}
destroyOnHidden
> >
<Form <Form form={form} layout="vertical" preserve={false}>
name={"lesson"}
form={form}
layout={"vertical"}
>
<Form.Item <Form.Item
name="title" name="title"
label="Название" label="Название лекции"
rules={[{required: true, message: "Пожалуйста, введите название"}]} rules={[{required: true, message: "Введите название лекции"}]}
> >
<Input/> <Input size="large" placeholder="Введение в JavaScript"/>
</Form.Item> </Form.Item>
<Form.Item
name="description" <Form.Item name="description" label="Краткое описание">
label="Описание" <TextArea rows={2} placeholder="О чём эта лекция..."/>
>
<Input/>
</Form.Item> </Form.Item>
<Form.Item
name="text" <Form.Item name="number" label="Порядковый номер" initialValue={1}>
label="Текстовый материал" <InputNumber min={1} style={{width: "100%"}}/>
> </Form.Item>
<div className="jodit-container">
<Form.Item label="Содержание лекции">
<div style={{border: "1px solid #d9d9d9", borderRadius: 6}}>
<JoditEditor <JoditEditor
ref={editorRef} ref={editorRef}
config={joditConfig} config={joditConfig}
/> />
</div> </div>
</Form.Item> </Form.Item>
<Form.Item
name="number"
label="Порядковый номер отображения"
>
<InputNumber min={1} defaultValue={1}/>
</Form.Item>
<Form.Item name="files" label="Прикрепить файлы">
<Upload
multiple
>
<Button icon={<UploadOutlined/>}>Выбрать файлы</Button>
</Upload>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">Сохранить</Button>
</Form.Item>
</Form> </Form>
</Modal> </Modal>
) );
}; };
export default CreateLessonModalForm; export default CreateLessonModalForm;

View File

@ -2,22 +2,62 @@ 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} from "react";
import {useCreateLessonMutation} from "../../../../../Api/lessonsApi.js";
const useCreateLessonModalForm = () => { const useCreateLessonModalForm = ({courseId}) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { const {
openModalCreateLesson openModalCreateLesson
} = useSelector((state) => state.lessons); } = useSelector((state) => state.lessons);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [createLesson, {isLoading}] = useCreateLessonMutation();
const isModalOpen = openModalCreateLesson; const isModalOpen = openModalCreateLesson;
const editorRef = useRef(null); const editorRef = useRef(null);
const handleCancel = () => { const handleCancel = () => {
form.resetFields();
if (editorRef.current) {
editorRef.current.value = "";
}
dispatch(setOpenModalCreateLesson(false)); dispatch(setOpenModalCreateLesson(false));
}; };
const handleOk = async () => {
try {
const values = await form.validateFields();
const content = editorRef.current?.value || "";
const lessonData = {
title: values.title,
description: values.description || null,
text: content,
number: values.number || 1,
};
await createLesson({
courseId,
lessonData,
}).unwrap();
notification.success({
title: "Успех",
description: "Лекция успешно создана!",
placement: "topRight",
});
handleCancel();
} catch (error) {
notification.error({
title: "Ошибка",
description: error?.data?.detail || "Не удалось создать лекцию",
placement: "topRight",
});
}
};
const joditConfig = useMemo( const joditConfig = useMemo(
() => ({ () => ({
readonly: false, readonly: false,
@ -89,6 +129,7 @@ const useCreateLessonModalForm = () => {
form, form,
joditConfig, joditConfig,
editorRef, editorRef,
handleOk,
} }
}; };

View File

@ -0,0 +1,85 @@
import useViewLessonModal from "./useViewLessonModal.js";
import {Avatar, Button, Col, Divider, Modal, Space, Typography} from "antd";
import {CloseOutlined, UserOutlined} from "@ant-design/icons";
const {Title, Text, Paragraph} = Typography;
const ViewLessonModal = () => {
const {
selectedLessonToView,
modalIsOpen,
handleClose,
} = useViewLessonModal();
return (
<Modal
open={modalIsOpen}
onCancel={handleClose}
footer={null}
width={1000}
closeIcon={<CloseOutlined style={{fontSize: 20}}/>}
title={null}
centered
destroyOnHidden
>
<Space align="start" style={{marginBottom: 16}}>
<Col>
<Title level={2} style={{margin: 0, flex: 1}}>
{selectedLessonToView?.title}
</Title>
<Avatar
size="small"
icon={<UserOutlined/>}
style={{backgroundColor: "#1890ff"}}
>
{selectedLessonToView?.creator?.first_name?.[0] || "У"}
</Avatar>
<Text type="secondary">
Создал: <strong>{selectedLessonToView?.creator?.first_name} {selectedLessonToView?.creator?.last_name}</strong>
{selectedLessonToView?.creator?.patronymic && ` ${selectedLessonToView?.creator.patronymic}`}
</Text>
</Col>
</Space>
{selectedLessonToView?.description && (
<>
<Title level={4} style={{margin: "16px 0 8px"}}>
Описание
</Title>
<Paragraph type="secondary" style={{fontSize: 16, marginBottom: 24}}>
{selectedLessonToView?.description}
</Paragraph>
<Divider style={{margin: "24px 0"}}/>
</>
)}
{selectedLessonToView?.text ? (
<div
className="lesson-content"
dangerouslySetInnerHTML={{__html: selectedLessonToView?.text}}
style={{
fontSize: "16px",
lineHeight: "1.7",
color: "#333",
}}
/>
) : (
<Paragraph italic type="secondary">
Текстовый материал отсутствует
</Paragraph>
)}
<Divider/>
<div style={{textAlign: "right"}}>
<Button onClick={handleClose}>
Закрыть
</Button>
</div>
</Modal>
);
};
export default ViewLessonModal;

View File

@ -0,0 +1,25 @@
import {useDispatch, useSelector} from "react-redux";
import {setSelectedLessonToView} from "../../../../../Redux/Slices/lessonsSlice.js";
const useViewLessonModal = () => {
const dispatch = useDispatch();
const {
selectedLessonToView
} = useSelector((state) => state.lessons);
const modalIsOpen = selectedLessonToView !== null;
const handleClose = () => {
dispatch(setSelectedLessonToView(null));
};
return {
selectedLessonToView,
modalIsOpen,
handleClose,
}
};
export default useViewLessonModal;

View File

@ -1,24 +1,50 @@
import useCourseDetailPage from "./useCourseDetailPage.js"; import useCourseDetailPage from "./useCourseDetailPage.js";
import {Button, Col, FloatButton, Result, Row, Tooltip, Typography} from "antd"; import {
import {ArrowLeftOutlined, BookOutlined, FormOutlined, PlusOutlined} from "@ant-design/icons"; Avatar,
Button,
Card,
Col,
Empty,
FloatButton,
Popconfirm,
Result,
Row,
Space,
Tag,
Tooltip,
Typography
} from "antd";
import {
ArrowLeftOutlined,
BookOutlined,
DeleteOutlined,
EditOutlined,
FormOutlined,
PlusOutlined
} from "@ant-design/icons";
import {useNavigate, useParams} from "react-router-dom"; import {useNavigate, useParams} from "react-router-dom";
import {ROLES} from "../../../Core/constants.js"; import {ROLES} from "../../../Core/constants.js";
import CONFIG from "../../../Core/сonfig.js"; import CONFIG from "../../../Core/сonfig.js";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import CreateLessonModalForm from "./Components/CreateLessonModalForm/CreateLessonModalForm.jsx"; import CreateLessonModalForm from "./Components/CreateLessonModalForm/CreateLessonModalForm.jsx";
import ViewLessonModal from "./Components/ViewLessonModalForm/ViewLessonModal.jsx";
const {Title} = Typography; const {Title, Text} = Typography;
const CourseDetailPage = () => { const CourseDetailPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const {courseId} = useParams(); const {courseId} = useParams();
const { const {
isTeacherOrAdmin,
lessonsData,
userData, userData,
courseData, courseData,
isLoading, isLoading,
isError, isError,
handleCreateLesson, handleCreateLesson,
handleOpenLesson,
handleEditLesson,
} = useCourseDetailPage(courseId); } = useCourseDetailPage(courseId);
if (isLoading) { if (isLoading) {
@ -46,7 +72,92 @@ const CourseDetailPage = () => {
</Col> </Col>
</Row> </Row>
<CreateLessonModalForm/> {lessonsData.length === 0 ? (
<Empty
description="Пока нет лекций"
image={Empty.PRESENTED_IMAGE_SIMPLE}
style={{ margin: "60px 0" }}
>
{isTeacherOrAdmin && (
<Button type="primary" size="large" onClick={handleCreateLesson}>
Добавить первую лекцию
</Button>
)}
</Empty>
) : (
<Row gutter={[24, 24]}>
{lessonsData.map((lesson, index) => (
<Col xs={24} sm={12} lg={8} xl={6} key={lesson.id}>
<Card
hoverable
style={{ height: "100%", cursor: "pointer" }}
onClick={() => handleOpenLesson(lesson)}
title={
<Space>
<Text strong>{lesson.number}. {lesson.title}</Text>
{lesson.text && lesson.text.length > 100 && (
<Tag color="blue">Есть текст</Tag>
)}
</Space>
}
extra={
isTeacherOrAdmin && (
<Space onClick={(e) => e.stopPropagation()}>
<Button
type="text"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation();
handleEditLesson(lesson);
}}
/>
<Popconfirm
title="Удалить лекцию?"
description="Это действие нельзя отменить"
// onConfirm={(e) => {
// e?.stopPropagation();
// handleDeleteLesson(lesson.id);
// }}
okText="Удалить"
cancelText="Отмена"
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => e.stopPropagation()}
/>
</Popconfirm>
</Space>
)
}
>
<div style={{ marginBottom: 16 }}>
{lesson.description ? (
<Text type="secondary">{lesson.description}</Text>
) : (
<Text type="secondary" italic>Описание отсутствует</Text>
)}
</div>
<div style={{ marginTop: 16, display: "flex", alignItems: "center", gap: 8 }}>
<Avatar size="small" style={{ backgroundColor: "#1890ff" }}>
{userData?.first_name?.[0] || "У"}
</Avatar>
<Text type="secondary" style={{ fontSize: 12 }}>
Создал: {lesson.creator?.first_name} {lesson.creator?.last_name}
</Text>
</div>
</Card>
</Col>
))}
</Row>
)}
<CreateLessonModalForm
courseId={courseId}
/>
<ViewLessonModal/>
{[CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData.role.title) && ( {[CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData.role.title) && (
<FloatButton.Group <FloatButton.Group
placement={"left"} placement={"left"}

View File

@ -2,7 +2,14 @@ import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
import {useGetCourseByIdQuery} from "../../../Api/coursesApi.js"; import {useGetCourseByIdQuery} from "../../../Api/coursesApi.js";
import {useEffect} from "react"; import {useEffect} from "react";
import {useDispatch} from "react-redux"; import {useDispatch} from "react-redux";
import {setOpenModalCreateLesson} from "../../../Redux/Slices/lessonsSlice.js"; import {
setOpenModalCreateLesson,
setSelectedLessonToUpdate,
setSelectedLessonToView
} from "../../../Redux/Slices/lessonsSlice.js";
import {useGetLessonsByCourseIdQuery} from "../../../Api/lessonsApi.js";
import {ROLES} from "../../../Core/constants.js";
import CONFIG from "../../../Core/сonfig.js";
const useCourseDetailPage = (courseId) => { const useCourseDetailPage = (courseId) => {
@ -13,7 +20,7 @@ const useCourseDetailPage = (courseId) => {
isLoading: isUserLoading, isLoading: isUserLoading,
isError: isUserError, isError: isUserError,
} = useGetAuthenticatedUserDataQuery(undefined, { } = useGetAuthenticatedUserDataQuery(undefined, {
pollingInterval: 60000, pollingInterval: 10000,
}); });
const { const {
@ -21,7 +28,15 @@ const useCourseDetailPage = (courseId) => {
isLoading: isCourseLoading, isLoading: isCourseLoading,
isError: isCourseError, isError: isCourseError,
} = useGetCourseByIdQuery(courseId, { } = useGetCourseByIdQuery(courseId, {
pollingInterval: 60000, pollingInterval: 10000,
});
const {
data: lessonsData,
isLoading: isLessonsLoading,
isError: isLessonsError,
} = useGetLessonsByCourseIdQuery(courseId, {
pollingInterval: 10000,
}); });
useEffect(() => { useEffect(() => {
@ -32,12 +47,26 @@ const useCourseDetailPage = (courseId) => {
dispatch(setOpenModalCreateLesson(true)) dispatch(setOpenModalCreateLesson(true))
}; };
const isTeacherOrAdmin = [CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData?.role?.title);
const handleOpenLesson = (lesson) => {
dispatch(setSelectedLessonToView(lesson))
};
const handleEditLesson = (lesson) => {
dispatch(setSelectedLessonToUpdate(lesson))
};
return { return {
isTeacherOrAdmin,
lessonsData,
userData, userData,
courseData, courseData,
isLoading: isUserLoading || isCourseLoading, isLoading: isUserLoading || isCourseLoading || isLessonsLoading,
isError: isUserError || isCourseError, isError: isUserError || isCourseError || isLessonsError,
handleCreateLesson, handleCreateLesson,
handleOpenLesson,
handleEditLesson,
} }
}; };

View File

@ -4,6 +4,7 @@ import {createSlice} from "@reduxjs/toolkit";
const initialState = { const initialState = {
selectedLessonToUpdate: null, selectedLessonToUpdate: null,
openModalCreateLesson: false, openModalCreateLesson: false,
selectedLessonToView: null,
}; };
const lessonSlice = createSlice({ const lessonSlice = createSlice({
@ -16,9 +17,16 @@ const lessonSlice = createSlice({
setOpenModalCreateLesson(state, action) { setOpenModalCreateLesson(state, action) {
state.openModalCreateLesson = action.payload; state.openModalCreateLesson = action.payload;
}, },
setSelectedLessonToView(state, action) {
state.selectedLessonToView = action.payload;
}
}, },
}); });
export const {setSelectedLessonToUpdate, setOpenModalCreateLesson} = lessonSlice.actions; export const {
setSelectedLessonToUpdate,
setOpenModalCreateLesson,
setSelectedLessonToView,
} = lessonSlice.actions;
export default lessonSlice.reducer; export default lessonSlice.reducer;