починил отображение курсов
This commit is contained in:
parent
4ec6e96525
commit
2dd0419726
@ -1,10 +1,10 @@
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.domain.models import Course
|
||||
from app.domain.models import Course, CourseTeacher, Enrollment
|
||||
|
||||
|
||||
class CoursesRepository:
|
||||
@ -22,6 +22,32 @@ class CoursesRepository:
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_all_for_teacher(self, user_id: int) -> Optional[Sequence[Course]]:
|
||||
query = (
|
||||
select(Course)
|
||||
.options(
|
||||
selectinload(Course.teachers),
|
||||
selectinload(Course.enrollments)
|
||||
)
|
||||
.join(Course.teachers)
|
||||
.filter(CourseTeacher.teacher_id == user_id)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_all_for_enrollment(self, user_id: int) -> Optional[Sequence[Course]]:
|
||||
query = (
|
||||
select(Course)
|
||||
.options(
|
||||
selectinload(Course.teachers),
|
||||
selectinload(Course.enrollments)
|
||||
)
|
||||
.join(Course.enrollments)
|
||||
.filter(Enrollment.user_id == user_id)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_by_id(self, course_id: int) -> Optional[Course]:
|
||||
query = (
|
||||
select(Course)
|
||||
|
||||
@ -30,6 +30,20 @@ async def get_all_courses(
|
||||
return await courses_service.get_all()
|
||||
|
||||
|
||||
@courses_router.get(
|
||||
'/for-me/',
|
||||
response_model=Optional[List[CourseRead]],
|
||||
summary='Return all for current user',
|
||||
description='Return all current user',
|
||||
)
|
||||
async def get_fors_for_teacher(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_auth_user),
|
||||
):
|
||||
courses_service = CoursesService(db)
|
||||
return await courses_service.get_all_for_me(user)
|
||||
|
||||
|
||||
@courses_router.get(
|
||||
'/{course_id}/',
|
||||
response_model=Optional[CourseRead],
|
||||
|
||||
@ -4,13 +4,16 @@ from fastapi import HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.application.courses_repository import CoursesRepository
|
||||
from app.core.constants import UserRoles
|
||||
from app.domain.entities.courses import CourseRead, CourseCreate, CourseCreated
|
||||
from app.domain.models import Course
|
||||
from app.domain.models import Course, User
|
||||
from app.settings import Settings
|
||||
|
||||
|
||||
class CoursesService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.courses_repository = CoursesRepository(db)
|
||||
self.settings = Settings()
|
||||
|
||||
async def get_all(self) -> List[CourseRead]:
|
||||
courses = await self.courses_repository.get_all()
|
||||
@ -22,6 +25,27 @@ class CoursesService:
|
||||
|
||||
return response
|
||||
|
||||
async def get_all_for_me(self, user: User) -> Optional[List[CourseRead]]:
|
||||
if user.role.title == self.settings.root_role_name:
|
||||
courses = await self.courses_repository.get_all()
|
||||
|
||||
elif user.role.title == UserRoles.TEACHER:
|
||||
courses = await self.courses_repository.get_all_for_teacher(user.id)
|
||||
|
||||
elif user.role.title == UserRoles.STUDENT:
|
||||
courses = await self.courses_repository.get_all_for_enrollment(user.id)
|
||||
|
||||
else:
|
||||
courses = []
|
||||
|
||||
response = []
|
||||
for course in courses:
|
||||
response.append(
|
||||
CourseRead.model_validate(course)
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
async def get_by_id(self, course_id: int) -> Optional[CourseRead]:
|
||||
course = await self.courses_repository.get_by_id(course_id)
|
||||
if course is None:
|
||||
|
||||
@ -14,6 +14,13 @@ export const coursesApi = createApi({
|
||||
}),
|
||||
providesTags: ['course'],
|
||||
}),
|
||||
getAllMyCourses: builder.query({
|
||||
query: () => ({
|
||||
url: "/courses/for-me/",
|
||||
method: "GET",
|
||||
}),
|
||||
providesTags: ['course'],
|
||||
}),
|
||||
getCourseById: builder.query({
|
||||
query: (courseId) => ({
|
||||
url: `/courses/${courseId}/`,
|
||||
@ -72,6 +79,7 @@ export const coursesApi = createApi({
|
||||
|
||||
export const {
|
||||
useGetAllCoursesQuery,
|
||||
useGetAllMyCoursesQuery,
|
||||
useGetCourseByIdQuery,
|
||||
useCreateCourseMutation,
|
||||
useUpdateCourseMutation,
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
import useUpdateLessonModalForm from "./useUpdateLessonModalForm.js";
|
||||
import {Button, Form, Input, InputNumber, Modal, Upload} from "antd";
|
||||
import JoditEditor from "jodit-react";
|
||||
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
|
||||
const {TextArea} = Input;
|
||||
|
||||
const UpdateLessonModalForm = ({courseId}) => {
|
||||
const {
|
||||
isModalOpen,
|
||||
handleCancel,
|
||||
handleOk,
|
||||
form,
|
||||
joditConfig,
|
||||
editorRef,
|
||||
isLoading,
|
||||
initialContent,
|
||||
currentLesson,
|
||||
} = useUpdateLessonModalForm({courseId});
|
||||
|
||||
if (isLoading) {
|
||||
return <Modal>
|
||||
<LoadingIndicator/>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Редактирование лекции"
|
||||
open={isModalOpen}
|
||||
onCancel={handleCancel}
|
||||
width={1000}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={handleCancel}>
|
||||
Отмена
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={isLoading}
|
||||
onClick={handleOk}
|
||||
>
|
||||
Сохранить изменения
|
||||
</Button>,
|
||||
]}
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="Название лекции"
|
||||
rules={[{ required: true, message: "Введите название лекции" }]}
|
||||
>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="Краткое описание">
|
||||
<TextArea rows={2} placeholder="О чём эта лекция..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="number" label="Порядковый номер">
|
||||
<InputNumber min={1} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Содержание лекции">
|
||||
<div style={{ border: "1px solid #d9d9d9", borderRadius: 8, overflow: "hidden" }}>
|
||||
<JoditEditor
|
||||
ref={editorRef}
|
||||
config={joditConfig}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateLessonModalForm;
|
||||
@ -0,0 +1,153 @@
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {Form, notification} from "antd";
|
||||
import {useEffect, useMemo, useRef} from "react";
|
||||
import {useUpdateLessonMutation} from "../../../../../Api/lessonsApi.js";
|
||||
import {setSelectedLessonToUpdate} from "../../../../../Redux/Slices/lessonsSlice.js";
|
||||
|
||||
|
||||
const useUpdateLessonModalForm = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [form] = Form.useForm();
|
||||
const editorRef = useRef(null);
|
||||
|
||||
const {selectedLessonToUpdate} = useSelector((state) => state.lessons);
|
||||
const [updateLesson, {isLoading}] = useUpdateLessonMutation();
|
||||
|
||||
const isModalOpen = selectedLessonToUpdate !== null;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedLessonToUpdate && isModalOpen) {
|
||||
form.setFieldsValue({
|
||||
title: selectedLessonToUpdate.title || "",
|
||||
description: selectedLessonToUpdate.description || "",
|
||||
number: selectedLessonToUpdate.number || 1,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.value = selectedLessonToUpdate.text || "";
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}, [selectedLessonToUpdate, isModalOpen, form]);
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
if (editorRef.current) {
|
||||
editorRef.current.value = "";
|
||||
}
|
||||
dispatch(setSelectedLessonToUpdate(null));
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
await updateLesson({
|
||||
lessonId: selectedLessonToUpdate.id,
|
||||
lessonData,
|
||||
}).unwrap();
|
||||
|
||||
notification.success({
|
||||
title: "Успех",
|
||||
description: "Лекция успешно обновлена!",
|
||||
});
|
||||
|
||||
handleCancel();
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
title: "Ошибка",
|
||||
description: error?.data?.detail || "Не удалось обновить лекцию",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const joditConfig = useMemo(
|
||||
() => ({
|
||||
readonly: false,
|
||||
height: 150,
|
||||
toolbarAdaptive: false,
|
||||
buttons: [
|
||||
"bold", "italic", "underline", "strikethrough", "|",
|
||||
"superscript", "subscript", "|",
|
||||
"ul", "ol", "outdent", "indent", "|",
|
||||
"font", "fontsize", "brush", "paragraph", "|",
|
||||
"align", "hr", "|",
|
||||
"table", "link", "image", "video", "symbols", "|",
|
||||
"undo", "redo", "cut", "copy", "paste", "selectall", "eraser", "|",
|
||||
"find", "source", "fullsize", "print", "preview",
|
||||
],
|
||||
autofocus: false,
|
||||
preserveSelection: true,
|
||||
askBeforePasteHTML: false,
|
||||
askBeforePasteFromWord: false,
|
||||
defaultActionOnPaste: "insert_clear_html",
|
||||
spellcheck: true,
|
||||
placeholder: "Введите результаты приёма...",
|
||||
showCharsCounter: true,
|
||||
showWordsCounter: true,
|
||||
showXPathInStatusbar: false,
|
||||
toolbarSticky: true,
|
||||
toolbarButtonSize: "middle",
|
||||
cleanHTML: {
|
||||
removeEmptyElements: true,
|
||||
replaceNBSP: false,
|
||||
},
|
||||
hotkeys: {
|
||||
"ctrl + shift + f": "find",
|
||||
"ctrl + b": "bold",
|
||||
"ctrl + i": "italic",
|
||||
"ctrl + u": "underline",
|
||||
},
|
||||
image: {
|
||||
editSrc: true,
|
||||
editTitle: true,
|
||||
editAlt: true,
|
||||
openOnDblClick: false,
|
||||
},
|
||||
video: {
|
||||
allowedSources: ["youtube", "vimeo"],
|
||||
},
|
||||
uploader: {
|
||||
insertImageAsBase64URI: true,
|
||||
},
|
||||
paste: {
|
||||
insertAsBase64: true,
|
||||
mimeTypes: ["image/png", "image/jpeg", "image/gif"],
|
||||
maxFileSize: 5 * 1024 * 1024,
|
||||
error: () => {
|
||||
notification.error({
|
||||
title: "Ошибка вставки",
|
||||
description: "Файл слишком большой или неподдерживаемый формат.",
|
||||
placement: "topRight",
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const initialContent = selectedLessonToUpdate?.text || "";
|
||||
|
||||
return {
|
||||
isModalOpen,
|
||||
handleCancel,
|
||||
handleOk,
|
||||
form,
|
||||
joditConfig,
|
||||
editorRef,
|
||||
isLoading,
|
||||
initialContent,
|
||||
currentLesson: selectedLessonToUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
export default useUpdateLessonModalForm;
|
||||
@ -28,6 +28,7 @@ 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";
|
||||
import UpdateLessonModalForm from "./Components/UpdateLessonModalForm/UpdateLessonModalForm.jsx";
|
||||
|
||||
|
||||
const {Title, Text} = Typography;
|
||||
@ -95,9 +96,6 @@ const CourseDetailPage = () => {
|
||||
title={
|
||||
<Space>
|
||||
<Text strong>{lesson.number}. {lesson.title}</Text>
|
||||
{lesson.text && lesson.text.length > 100 && (
|
||||
<Tag color="blue">Есть текст</Tag>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
@ -158,6 +156,7 @@ const CourseDetailPage = () => {
|
||||
courseId={courseId}
|
||||
/>
|
||||
<ViewLessonModal/>
|
||||
<UpdateLessonModalForm/>
|
||||
{[CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData.role.title) && (
|
||||
<FloatButton.Group
|
||||
placement={"left"}
|
||||
|
||||
@ -71,8 +71,6 @@ const CoursesPage = () => {
|
||||
{courses.map((course) => (
|
||||
<Col xs={24} sm={12} lg={8} xl={6} key={course.id}>
|
||||
<Card
|
||||
onClick={() => navigate(`/courses/${course.id}`)}
|
||||
|
||||
hoverable
|
||||
cover={
|
||||
<div
|
||||
@ -85,6 +83,7 @@ const CoursesPage = () => {
|
||||
color: "white",
|
||||
fontSize: 48,
|
||||
}}
|
||||
onClick={() => navigate(`/courses/${course.id}`)}
|
||||
>
|
||||
{course.title[0].toUpperCase()}
|
||||
</div>
|
||||
@ -103,6 +102,7 @@ const CoursesPage = () => {
|
||||
}
|
||||
>
|
||||
<Card.Meta
|
||||
onClick={() => navigate(`/courses/${course.id}`)}
|
||||
title={<Title level={4}>{course.title}</Title>}
|
||||
description={
|
||||
course.description || <Text type="secondary">Без описания</Text>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
|
||||
import {useGetAllCoursesQuery} from "../../../Api/coursesApi.js";
|
||||
import {useGetAllCoursesQuery, useGetAllMyCoursesQuery} from "../../../Api/coursesApi.js";
|
||||
import CONFIG from "../../../Core/сonfig.js";
|
||||
import {ROLES} from "../../../Core/constants.js";
|
||||
import {useDispatch} from "react-redux";
|
||||
@ -11,7 +11,7 @@ const useCoursesPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {data: userData, isLoading: isUserLoading} = useGetAuthenticatedUserDataQuery();
|
||||
const {data: courses = [], isLoading, isCoursesLoading, isError} = useGetAllCoursesQuery(undefined, {
|
||||
const {data: courses = [], isLoading, isCoursesLoading, isError} = useGetAllMyCoursesQuery(undefined, {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user