починил отображение курсов
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 import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from app.domain.models import Course
|
from app.domain.models import Course, CourseTeacher, Enrollment
|
||||||
|
|
||||||
|
|
||||||
class CoursesRepository:
|
class CoursesRepository:
|
||||||
@ -22,6 +22,32 @@ class CoursesRepository:
|
|||||||
result = await self.db.execute(query)
|
result = await self.db.execute(query)
|
||||||
return result.scalars().all()
|
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]:
|
async def get_by_id(self, course_id: int) -> Optional[Course]:
|
||||||
query = (
|
query = (
|
||||||
select(Course)
|
select(Course)
|
||||||
|
|||||||
@ -30,6 +30,20 @@ async def get_all_courses(
|
|||||||
return await courses_service.get_all()
|
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(
|
@courses_router.get(
|
||||||
'/{course_id}/',
|
'/{course_id}/',
|
||||||
response_model=Optional[CourseRead],
|
response_model=Optional[CourseRead],
|
||||||
|
|||||||
@ -4,13 +4,16 @@ from fastapi import HTTPException, status
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.application.courses_repository import CoursesRepository
|
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.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:
|
class CoursesService:
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.courses_repository = CoursesRepository(db)
|
self.courses_repository = CoursesRepository(db)
|
||||||
|
self.settings = Settings()
|
||||||
|
|
||||||
async def get_all(self) -> List[CourseRead]:
|
async def get_all(self) -> List[CourseRead]:
|
||||||
courses = await self.courses_repository.get_all()
|
courses = await self.courses_repository.get_all()
|
||||||
@ -22,6 +25,27 @@ class CoursesService:
|
|||||||
|
|
||||||
return response
|
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]:
|
async def get_by_id(self, course_id: int) -> Optional[CourseRead]:
|
||||||
course = await self.courses_repository.get_by_id(course_id)
|
course = await self.courses_repository.get_by_id(course_id)
|
||||||
if course is None:
|
if course is None:
|
||||||
|
|||||||
@ -14,6 +14,13 @@ export const coursesApi = createApi({
|
|||||||
}),
|
}),
|
||||||
providesTags: ['course'],
|
providesTags: ['course'],
|
||||||
}),
|
}),
|
||||||
|
getAllMyCourses: builder.query({
|
||||||
|
query: () => ({
|
||||||
|
url: "/courses/for-me/",
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
providesTags: ['course'],
|
||||||
|
}),
|
||||||
getCourseById: builder.query({
|
getCourseById: builder.query({
|
||||||
query: (courseId) => ({
|
query: (courseId) => ({
|
||||||
url: `/courses/${courseId}/`,
|
url: `/courses/${courseId}/`,
|
||||||
@ -72,6 +79,7 @@ export const coursesApi = createApi({
|
|||||||
|
|
||||||
export const {
|
export const {
|
||||||
useGetAllCoursesQuery,
|
useGetAllCoursesQuery,
|
||||||
|
useGetAllMyCoursesQuery,
|
||||||
useGetCourseByIdQuery,
|
useGetCourseByIdQuery,
|
||||||
useCreateCourseMutation,
|
useCreateCourseMutation,
|
||||||
useUpdateCourseMutation,
|
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 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";
|
import ViewLessonModal from "./Components/ViewLessonModalForm/ViewLessonModal.jsx";
|
||||||
|
import UpdateLessonModalForm from "./Components/UpdateLessonModalForm/UpdateLessonModalForm.jsx";
|
||||||
|
|
||||||
|
|
||||||
const {Title, Text} = Typography;
|
const {Title, Text} = Typography;
|
||||||
@ -95,9 +96,6 @@ const CourseDetailPage = () => {
|
|||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<Text strong>{lesson.number}. {lesson.title}</Text>
|
<Text strong>{lesson.number}. {lesson.title}</Text>
|
||||||
{lesson.text && lesson.text.length > 100 && (
|
|
||||||
<Tag color="blue">Есть текст</Tag>
|
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
extra={
|
extra={
|
||||||
@ -158,6 +156,7 @@ const CourseDetailPage = () => {
|
|||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
/>
|
/>
|
||||||
<ViewLessonModal/>
|
<ViewLessonModal/>
|
||||||
|
<UpdateLessonModalForm/>
|
||||||
{[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"}
|
||||||
|
|||||||
@ -71,8 +71,6 @@ const CoursesPage = () => {
|
|||||||
{courses.map((course) => (
|
{courses.map((course) => (
|
||||||
<Col xs={24} sm={12} lg={8} xl={6} key={course.id}>
|
<Col xs={24} sm={12} lg={8} xl={6} key={course.id}>
|
||||||
<Card
|
<Card
|
||||||
onClick={() => navigate(`/courses/${course.id}`)}
|
|
||||||
|
|
||||||
hoverable
|
hoverable
|
||||||
cover={
|
cover={
|
||||||
<div
|
<div
|
||||||
@ -85,6 +83,7 @@ const CoursesPage = () => {
|
|||||||
color: "white",
|
color: "white",
|
||||||
fontSize: 48,
|
fontSize: 48,
|
||||||
}}
|
}}
|
||||||
|
onClick={() => navigate(`/courses/${course.id}`)}
|
||||||
>
|
>
|
||||||
{course.title[0].toUpperCase()}
|
{course.title[0].toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
@ -103,6 +102,7 @@ const CoursesPage = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Card.Meta
|
<Card.Meta
|
||||||
|
onClick={() => navigate(`/courses/${course.id}`)}
|
||||||
title={<Title level={4}>{course.title}</Title>}
|
title={<Title level={4}>{course.title}</Title>}
|
||||||
description={
|
description={
|
||||||
course.description || <Text type="secondary">Без описания</Text>
|
course.description || <Text type="secondary">Без описания</Text>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
|
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 CONFIG from "../../../Core/сonfig.js";
|
||||||
import {ROLES} from "../../../Core/constants.js";
|
import {ROLES} from "../../../Core/constants.js";
|
||||||
import {useDispatch} from "react-redux";
|
import {useDispatch} from "react-redux";
|
||||||
@ -11,7 +11,7 @@ const useCoursesPage = () => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const {data: userData, isLoading: isUserLoading} = useGetAuthenticatedUserDataQuery();
|
const {data: userData, isLoading: isUserLoading} = useGetAuthenticatedUserDataQuery();
|
||||||
const {data: courses = [], isLoading, isCoursesLoading, isError} = useGetAllCoursesQuery(undefined, {
|
const {data: courses = [], isLoading, isCoursesLoading, isError} = useGetAllMyCoursesQuery(undefined, {
|
||||||
pollingInterval: 20000,
|
pollingInterval: 20000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user