сделал создание и просмотр лекции
This commit is contained in:
parent
87a14f0eb1
commit
4ec6e96525
@ -1,6 +1,8 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.domain.entities.users import UserRead
|
||||
|
||||
|
||||
class LessonBase(BaseModel):
|
||||
title: str = Field(..., max_length=250)
|
||||
@ -10,7 +12,7 @@ class LessonBase(BaseModel):
|
||||
|
||||
|
||||
class LessonCreate(LessonBase):
|
||||
course_id: int
|
||||
pass
|
||||
|
||||
|
||||
class LessonUpdate(BaseModel):
|
||||
@ -25,5 +27,7 @@ class LessonRead(LessonBase):
|
||||
course_id: int
|
||||
creator_id: int
|
||||
|
||||
creator: UserRead
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@ -18,6 +18,6 @@ class Lesson(RootTable):
|
||||
creator_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False)
|
||||
|
||||
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')
|
||||
|
||||
@ -48,7 +48,6 @@ def require_admin(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]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Ошибка доступа')
|
||||
|
||||
|
||||
@ -1,77 +1,68 @@
|
||||
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;
|
||||
|
||||
const CreateLessonModalForm = () => {
|
||||
const CreateLessonModalForm = ({courseId}) => {
|
||||
const {
|
||||
isModalOpen,
|
||||
handleCancel,
|
||||
handleOk,
|
||||
form,
|
||||
joditConfig,
|
||||
editorRef,
|
||||
} = useCreateLessonModalForm();
|
||||
isLoading,
|
||||
} = useCreateLessonModalForm({courseId});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Создание лекционного материала"
|
||||
open={isModalOpen}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
title={"Создание лекционного материала"}
|
||||
style={{
|
||||
minWidth: "70%",
|
||||
minHeight: "80%",
|
||||
}}
|
||||
width={1000}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={handleCancel}>
|
||||
Отмена
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={isLoading}
|
||||
onClick={handleOk}
|
||||
>
|
||||
Создать лекцию
|
||||
</Button>,
|
||||
]}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form
|
||||
name={"lesson"}
|
||||
form={form}
|
||||
layout={"vertical"}
|
||||
>
|
||||
<Form form={form} layout="vertical" preserve={false}>
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="Название"
|
||||
rules={[{required: true, message: "Пожалуйста, введите название"}]}
|
||||
label="Название лекции"
|
||||
rules={[{required: true, message: "Введите название лекции"}]}
|
||||
>
|
||||
<Input/>
|
||||
<Input size="large" placeholder="Введение в JavaScript"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Описание"
|
||||
>
|
||||
<Input/>
|
||||
|
||||
<Form.Item name="description" label="Краткое описание">
|
||||
<TextArea rows={2} placeholder="О чём эта лекция..."/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="text"
|
||||
label="Текстовый материал"
|
||||
>
|
||||
<div className="jodit-container">
|
||||
|
||||
<Form.Item name="number" label="Порядковый номер" initialValue={1}>
|
||||
<InputNumber min={1} style={{width: "100%"}}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Содержание лекции">
|
||||
<div style={{border: "1px solid #d9d9d9", borderRadius: 6}}>
|
||||
<JoditEditor
|
||||
ref={editorRef}
|
||||
config={joditConfig}
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
</Modal>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateLessonModalForm;
|
||||
@ -2,22 +2,62 @@ 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";
|
||||
|
||||
|
||||
const useCreateLessonModalForm = () => {
|
||||
const useCreateLessonModalForm = ({courseId}) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
openModalCreateLesson
|
||||
} = useSelector((state) => state.lessons);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [createLesson, {isLoading}] = useCreateLessonMutation();
|
||||
|
||||
const isModalOpen = openModalCreateLesson;
|
||||
const editorRef = useRef(null);
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
if (editorRef.current) {
|
||||
editorRef.current.value = "";
|
||||
}
|
||||
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(
|
||||
() => ({
|
||||
readonly: false,
|
||||
@ -89,6 +129,7 @@ const useCreateLessonModalForm = () => {
|
||||
form,
|
||||
joditConfig,
|
||||
editorRef,
|
||||
handleOk,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -1,24 +1,50 @@
|
||||
import useCourseDetailPage from "./useCourseDetailPage.js";
|
||||
import {Button, Col, FloatButton, Result, Row, Tooltip, Typography} from "antd";
|
||||
import {ArrowLeftOutlined, BookOutlined, FormOutlined, PlusOutlined} from "@ant-design/icons";
|
||||
import {
|
||||
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 {ROLES} from "../../../Core/constants.js";
|
||||
import CONFIG from "../../../Core/сonfig.js";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.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 navigate = useNavigate();
|
||||
const {courseId} = useParams();
|
||||
const {
|
||||
isTeacherOrAdmin,
|
||||
lessonsData,
|
||||
userData,
|
||||
courseData,
|
||||
isLoading,
|
||||
isError,
|
||||
handleCreateLesson,
|
||||
handleOpenLesson,
|
||||
handleEditLesson,
|
||||
} = useCourseDetailPage(courseId);
|
||||
|
||||
if (isLoading) {
|
||||
@ -46,7 +72,92 @@ const CourseDetailPage = () => {
|
||||
</Col>
|
||||
</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) && (
|
||||
<FloatButton.Group
|
||||
placement={"left"}
|
||||
|
||||
@ -2,7 +2,14 @@ import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
|
||||
import {useGetCourseByIdQuery} from "../../../Api/coursesApi.js";
|
||||
import {useEffect} from "react";
|
||||
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) => {
|
||||
@ -13,7 +20,7 @@ const useCourseDetailPage = (courseId) => {
|
||||
isLoading: isUserLoading,
|
||||
isError: isUserError,
|
||||
} = useGetAuthenticatedUserDataQuery(undefined, {
|
||||
pollingInterval: 60000,
|
||||
pollingInterval: 10000,
|
||||
});
|
||||
|
||||
const {
|
||||
@ -21,7 +28,15 @@ const useCourseDetailPage = (courseId) => {
|
||||
isLoading: isCourseLoading,
|
||||
isError: isCourseError,
|
||||
} = useGetCourseByIdQuery(courseId, {
|
||||
pollingInterval: 60000,
|
||||
pollingInterval: 10000,
|
||||
});
|
||||
|
||||
const {
|
||||
data: lessonsData,
|
||||
isLoading: isLessonsLoading,
|
||||
isError: isLessonsError,
|
||||
} = useGetLessonsByCourseIdQuery(courseId, {
|
||||
pollingInterval: 10000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -32,12 +47,26 @@ const useCourseDetailPage = (courseId) => {
|
||||
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 {
|
||||
isTeacherOrAdmin,
|
||||
lessonsData,
|
||||
userData,
|
||||
courseData,
|
||||
isLoading: isUserLoading || isCourseLoading,
|
||||
isError: isUserError || isCourseError,
|
||||
isLoading: isUserLoading || isCourseLoading || isLessonsLoading,
|
||||
isError: isUserError || isCourseError || isLessonsError,
|
||||
handleCreateLesson,
|
||||
handleOpenLesson,
|
||||
handleEditLesson,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import {createSlice} from "@reduxjs/toolkit";
|
||||
const initialState = {
|
||||
selectedLessonToUpdate: null,
|
||||
openModalCreateLesson: false,
|
||||
selectedLessonToView: null,
|
||||
};
|
||||
|
||||
const lessonSlice = createSlice({
|
||||
@ -16,9 +17,16 @@ const lessonSlice = createSlice({
|
||||
setOpenModalCreateLesson(state, action) {
|
||||
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;
|
||||
Loading…
x
Reference in New Issue
Block a user