сделал создание и редактирование мета информации курса, и состава участников и учителей

This commit is contained in:
Андрей Дувакин 2025-11-28 20:37:03 +05:00
parent 44ceb93729
commit 9fe0d1e3e9
19 changed files with 513 additions and 115 deletions

View File

@ -2,6 +2,7 @@ from typing import List, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.domain.models import Course
@ -11,14 +12,24 @@ class CoursesRepository:
self.db = db
async def get_all(self) -> List[Course]:
query = select(Course)
query = (
select(Course)
.options(
selectinload(Course.teachers),
selectinload(Course.enrollments)
)
)
result = await self.db.execute(query)
return result.scalars().all()
async def get_by_id(self, course_id: int) -> Optional[Course]:
query = (
select(Course)
.order_by(id=course_id)
.options(
selectinload(Course.teachers),
selectinload(Course.enrollments)
)
.filter_by(id=course_id)
)
result = await self.db.execute(query)
return result.scalars().first()

View File

@ -5,12 +5,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.course_teachers import CourseTeacherRead, CourseTeacherCreate
from app.domain.entities.courses import CourseRead, CourseCreate, CourseUpdate
from app.domain.entities.courses import CourseRead, CourseCreate, CourseUpdate, CourseCreated
from app.domain.entities.enrollments import EnrollmentRead, EnrollmentCreate
from app.domain.models import User
from app.infrastructure.course_teachers_service import CourseTeachersService
from app.infrastructure.courses_service import CoursesService
from app.infrastructure.dependencies import require_auth_user, require_teacher
from app.infrastructure.dependencies import require_auth_user, require_teacher, require_admin
from app.infrastructure.enrollments_service import EnrollmentsService
courses_router = APIRouter()
@ -24,7 +24,7 @@ courses_router = APIRouter()
)
async def get_all_courses(
db: AsyncSession = Depends(get_db),
user: User = Depends(require_auth_user),
user: User = Depends(require_admin),
):
courses_service = CoursesService(db)
return await courses_service.get_all()
@ -32,7 +32,7 @@ async def get_all_courses(
@courses_router.post(
'/',
response_model=Optional[CourseRead],
response_model=Optional[CourseCreated],
summary='Create a new course',
description='Create a new course',
)

View File

@ -0,0 +1,106 @@
"""0002 сделал поле с фото необязательным
Revision ID: 33d77ac5ed79
Revises: 6241a16321b4
Create Date: 2025-11-28 19:23:15.655318
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '33d77ac5ed79'
down_revision: Union[str, Sequence[str], None] = '6241a16321b4'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f('course_teachers_teacher_id_fkey'), 'course_teachers', type_='foreignkey')
op.drop_constraint(op.f('course_teachers_course_id_fkey'), 'course_teachers', type_='foreignkey')
op.create_foreign_key(None, 'course_teachers', 'users', ['teacher_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'course_teachers', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
op.alter_column('courses', 'photo_filename',
existing_type=sa.VARCHAR(),
nullable=True)
op.alter_column('courses', 'photo_path',
existing_type=sa.VARCHAR(),
nullable=True)
op.drop_constraint(op.f('enrollments_student_id_fkey'), 'enrollments', type_='foreignkey')
op.drop_constraint(op.f('enrollments_course_id_fkey'), 'enrollments', type_='foreignkey')
op.create_foreign_key(None, 'enrollments', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'enrollments', 'users', ['student_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('lesson_files_lesson_id_fkey'), 'lesson_files', type_='foreignkey')
op.create_foreign_key(None, 'lesson_files', 'lessons', ['lesson_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('lessons_course_id_fkey'), 'lessons', type_='foreignkey')
op.drop_constraint(op.f('lessons_creator_id_fkey'), 'lessons', type_='foreignkey')
op.create_foreign_key(None, 'lessons', 'users', ['creator_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'lessons', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('solution_files_solution_id_fkey'), 'solution_files', type_='foreignkey')
op.create_foreign_key(None, 'solution_files', 'solutions', ['solution_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('solutions_task_id_fkey'), 'solutions', type_='foreignkey')
op.drop_constraint(op.f('solutions_assessment_autor_id_fkey'), 'solutions', type_='foreignkey')
op.drop_constraint(op.f('solutions_student_id_fkey'), 'solutions', type_='foreignkey')
op.create_foreign_key(None, 'solutions', 'users', ['student_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'solutions', 'users', ['assessment_autor_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'solutions', 'tasks', ['task_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('task_files_task_id_fkey'), 'task_files', type_='foreignkey')
op.create_foreign_key(None, 'task_files', 'tasks', ['task_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('tasks_creator_id_fkey'), 'tasks', type_='foreignkey')
op.drop_constraint(op.f('tasks_course_id_fkey'), 'tasks', type_='foreignkey')
op.create_foreign_key(None, 'tasks', 'users', ['creator_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'tasks', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('users_role_id_fkey'), 'users', type_='foreignkey')
op.drop_constraint(op.f('users_status_id_fkey'), 'users', type_='foreignkey')
op.create_foreign_key(None, 'users', 'statuses', ['status_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id'], source_schema='public', referent_schema='public')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'users', schema='public', type_='foreignkey')
op.drop_constraint(None, 'users', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('users_status_id_fkey'), 'users', 'statuses', ['status_id'], ['id'])
op.create_foreign_key(op.f('users_role_id_fkey'), 'users', 'roles', ['role_id'], ['id'])
op.drop_constraint(None, 'tasks', schema='public', type_='foreignkey')
op.drop_constraint(None, 'tasks', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('tasks_course_id_fkey'), 'tasks', 'courses', ['course_id'], ['id'])
op.create_foreign_key(op.f('tasks_creator_id_fkey'), 'tasks', 'users', ['creator_id'], ['id'])
op.drop_constraint(None, 'task_files', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('task_files_task_id_fkey'), 'task_files', 'tasks', ['task_id'], ['id'])
op.drop_constraint(None, 'solutions', schema='public', type_='foreignkey')
op.drop_constraint(None, 'solutions', schema='public', type_='foreignkey')
op.drop_constraint(None, 'solutions', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('solutions_student_id_fkey'), 'solutions', 'users', ['student_id'], ['id'])
op.create_foreign_key(op.f('solutions_assessment_autor_id_fkey'), 'solutions', 'users', ['assessment_autor_id'], ['id'])
op.create_foreign_key(op.f('solutions_task_id_fkey'), 'solutions', 'tasks', ['task_id'], ['id'])
op.drop_constraint(None, 'solution_files', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('solution_files_solution_id_fkey'), 'solution_files', 'solutions', ['solution_id'], ['id'])
op.drop_constraint(None, 'lessons', schema='public', type_='foreignkey')
op.drop_constraint(None, 'lessons', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('lessons_creator_id_fkey'), 'lessons', 'users', ['creator_id'], ['id'])
op.create_foreign_key(op.f('lessons_course_id_fkey'), 'lessons', 'courses', ['course_id'], ['id'])
op.drop_constraint(None, 'lesson_files', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('lesson_files_lesson_id_fkey'), 'lesson_files', 'lessons', ['lesson_id'], ['id'])
op.drop_constraint(None, 'enrollments', schema='public', type_='foreignkey')
op.drop_constraint(None, 'enrollments', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('enrollments_course_id_fkey'), 'enrollments', 'courses', ['course_id'], ['id'])
op.create_foreign_key(op.f('enrollments_student_id_fkey'), 'enrollments', 'users', ['student_id'], ['id'])
op.alter_column('courses', 'photo_path',
existing_type=sa.VARCHAR(),
nullable=False)
op.alter_column('courses', 'photo_filename',
existing_type=sa.VARCHAR(),
nullable=False)
op.drop_constraint(None, 'course_teachers', schema='public', type_='foreignkey')
op.drop_constraint(None, 'course_teachers', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('course_teachers_course_id_fkey'), 'course_teachers', 'courses', ['course_id'], ['id'])
op.create_foreign_key(op.f('course_teachers_teacher_id_fkey'), 'course_teachers', 'users', ['teacher_id'], ['id'])
# ### end Alembic commands ###

View File

@ -2,7 +2,6 @@ from pydantic import BaseModel, EmailStr, Field
class CourseTeacherCreate(BaseModel):
course_id: int = Field()
teacher_id: int = Field()

View File

@ -6,6 +6,17 @@ from app.domain.entities.course_teachers import CourseTeacherRead
from app.domain.entities.enrollments import EnrollmentRead
class CourseBase(BaseModel):
id: int
title: str
description: Optional[str] = None
photo_filename: Optional[str] = None
photo_path: Optional[str] = None
class Config:
from_attributes = True
class CourseCreate(BaseModel):
title: str = Field(max_length=250)
description: Optional[str] = Field(default=None, max_length=1000)
@ -15,13 +26,14 @@ class CourseUpdate(CourseCreate):
pass
class CourseRead(BaseModel):
id: int
title: str
description: str
teachers: List[CourseTeacherRead]
enrollments: List[EnrollmentRead]
class CourseRead(CourseBase):
teachers: List[CourseTeacherRead] = []
enrollments: List[EnrollmentRead] = []
class Config:
from_attributes = True
class CourseCreated(CourseBase):
class Config:
from_attributes = True

View File

@ -1,10 +1,10 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr, Field
class EnrollmentCreate(BaseModel):
course_id: int = Field()
student_id: int = Field()
@ -12,7 +12,7 @@ class EnrollmentRead(BaseModel):
id: int
course_id: int
student_id: int
enrollment_date: datetime
enrollment_date: Optional[datetime] = None
class Config:
from_attributes = True

View File

@ -17,8 +17,8 @@ class RootTable(Base):
class PhotoAbstract(RootTable):
__abstract__ = True
photo_filename: Mapped[str] = mapped_column()
photo_path: Mapped[str] = mapped_column()
photo_filename: Mapped[str] = mapped_column(nullable=True)
photo_path: Mapped[str] = mapped_column(nullable=True)
class FileAbstract(RootTable):

View File

@ -1,6 +1,6 @@
from datetime import datetime
from sqlalchemy import ForeignKey
from sqlalchemy import ForeignKey, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.domain.models.base import RootTable

View File

@ -4,7 +4,7 @@ from fastapi import HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.application.courses_repository import CoursesRepository
from app.domain.entities.courses import CourseRead, CourseCreate
from app.domain.entities.courses import CourseRead, CourseCreate, CourseCreated
from app.domain.models import Course
@ -22,7 +22,7 @@ class CoursesService:
return response
async def create(self, course: CourseCreate) -> Optional[CourseRead]:
async def create(self, course: CourseCreate) -> CourseCreated: # ← возвращаем CourseCreated
course_model = Course(
title=course.title,
description=course.description,
@ -30,7 +30,7 @@ class CoursesService:
course_model = await self.courses_repository.create(course_model)
return CourseRead.model_validate(course_model)
return CourseCreated.model_validate(course_model)
async def update(self, course_id: int, course: CourseCreate) -> Optional[CourseRead]:
course_model = await self.courses_repository.get_by_id(course_id)

View File

@ -1,3 +1,4 @@
import datetime
from typing import Optional, List
from fastapi import HTTPException, status
@ -52,7 +53,7 @@ class EnrollmentsService:
enrollments_models.append(Enrollment(
course_id=course_id,
student_id=enrollment.student_id,
enrollment_date=enrollment.enrollment_date,
enrollment_date=datetime.datetime.now(),
))
enrollments_models = await self.enrollments_repository.create_list(enrollments_models)

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="/rounded_logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>web</title>
<title>Система обучения lectio</title>
</head>
<body>
<div id="root"></div>

View File

@ -38,10 +38,10 @@ export const coursesApi = createApi({
providesTags: ['teacher'],
}),
replaceCourseTeachers: builder.mutation({
query: ({courseId, ...data}) => ({
query: ({courseId, teachers}) => ({
url: `/courses/${courseId}/teachers/`,
method: "PUT",
body: data,
body: teachers,
}),
invalidatesTags: ['teacher'],
}),
@ -53,10 +53,10 @@ export const coursesApi = createApi({
providesTags: ['student'],
}),
replaceCourseStudents: builder.mutation({
query: ({courseId, ...data}) => ({
query: ({courseId, students}) => ({
url: `/courses/${courseId}/students/`,
method: "PUT",
body: data,
body: students,
}),
invalidatesTags: ['student'],
}),

View File

@ -79,7 +79,7 @@ const MainLayout = () => {
<Outlet/>
)}
</Content>
<Footer style={{textAlign: "center"}}>{new Date().getFullYear()}</Footer>
<Footer style={{textAlign: "center"}}>lectio © {new Date().getFullYear()}</Footer>
</Layout>
</Layout>
)

View File

@ -1,37 +1,37 @@
import {Button, Col, Form, Input, Modal, Result, Row, Select, Spin} from "antd";
import useCreateCourseModalForm from "./useCreateCourseModalForm.js";
import {Button, Col, Form, Input, Modal, Result, Row, Select} from "antd";
import TextArea from "antd/es/input/TextArea.js";
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
const {Option} = Select;
const {TextArea} = Input;
const CreateCourseModal = () => {
const {
openCreateCourseModal,
handleCancel,
handleOk,
form,
isLoadinging,
isError,
teachers,
students
students,
isLoading,
isError,
} = useCreateCourseModalForm();
if (isLoadinging) {
return (
<LoadingIndicator/>
);
if (isError) {
return <Modal visible={openCreateCourseModal} footer={null}>
<Result status="500" title="Ошибка загрузки"/>
</Modal>;
}
if (isError) {
return (
<Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных пользователя"/>
);
}
const filterOption = (input, option) =>
option.children.toString().toLowerCase().includes(input.toLowerCase());
return (
<Modal
title={"Создание курса"}
title="Создание курса"
open={openCreateCourseModal}
onCancel={handleCancel}
width={900}
footer={[
<Button key="cancel" onClick={handleCancel}>
Отмена
@ -39,25 +39,27 @@ const CreateCourseModal = () => {
<Button
key="submit"
type="primary"
loading={isLoadinging}
// onClick={handleOk}
loading={isLoading}
onClick={handleOk}
>
Создать
Создать курс
</Button>,
]}
width={800}
>
{isLoading ? (
<LoadingIndicator/>
) : (
<Form form={form} layout="vertical">
<Form.Item
name="title"
label="Название курса"
rules={[{required: true, message: "Введите название"}]}
rules={[{required: true, message: "Введите название курса"}]}
>
<Input size="large" placeholder="Введение в Python"/>
<Input size="large" placeholder="Введение в React"/>
</Form.Item>
<Form.Item name="description" label="Описание">
<TextArea rows={4} placeholder="Курс для начинающих..."/>
<Form.Item name="description" label="Описание (необязательно)">
<TextArea rows={4} placeholder="Курс для начинающих разработчиков..."/>
</Form.Item>
<Row gutter={16}>
@ -65,34 +67,44 @@ const CreateCourseModal = () => {
<Form.Item name="teacher_ids" label="Преподаватели">
<Select
mode="multiple"
placeholder="Выберите преподавателей"
loading={isLoadinging}
placeholder="Начните вводить ФИО или логин..."
loading={isLoading}
showSearch={{filterOption: filterOption}}
notFoundContent="Преподаватели не найдены"
>
{teachers.map((teacher) => (
<Select.Option key={teacher.id}
value={teacher.id}>{teacher.last_name} {teacher.first_name} - {teacher.login}</Select.Option>
<Option key={teacher.id} value={teacher.id}>
{teacher.last_name} {teacher.first_name} ({teacher.login})
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="student_ids" label="Студенты">
<Select
mode="multiple"
placeholder="Выберите студентов"
loading={isLoadinging}
showSearch
placeholder="Начните вводить ФИО или логин..."
loading={isLoading}
filterOption={filterOption}
optionFilterProp="children"
notFoundContent="Студенты не найдены"
>
{students.map((student) => (
<Select.Option key={student.id}
value={student.id}>{student.last_name} {student.first_name} - {student.login}</Select.Option>
<Option key={student.id} value={student.id}>
{student.last_name} {student.first_name} ({student.login})
</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
</Form>
)}
</Modal>
);
}
};
export default CreateCourseModal;

View File

@ -1,6 +1,6 @@
import {useDispatch, useSelector} from "react-redux";
import {setOpenCreateCourseModal} from "../../../../../Redux/Slices/coursesSlice.js";
import {Form} from "antd";
import {Form, notification} from "antd";
import {useGetUsersByRoleNameQuery} from "../../../../../Api/usersApi.js";
import {
useCreateCourseMutation,
@ -43,8 +43,55 @@ const useCreateCourseModalForm = () => {
const handleCancel = () => {
form.resetFields();
dispatch(setOpenCreateCourseModal(false));
};
const handleOk = async () => {
try {
const values = await form.validateFields();
const newCourse = await createCourse({
title: values.title,
description: values.description || null,
}).unwrap();
if (values.teacher_ids?.length > 0) {
const teachersPayload = values.teacher_ids.map(id => ({
teacher_id: id
}));
await replaceTeachers({
courseId: newCourse.id,
teachers: teachersPayload
}).unwrap();
}
if (values.student_ids?.length > 0) {
const studentsPayload = values.student_ids.map(id => ({
student_id: id,
}));
await replaceStudents({
courseId: newCourse.id,
students: studentsPayload
}).unwrap();
}
notification.success({
title: "Успешно",
description: "Курс успешно создан!",
placement: "topRight",
});
form.resetFields();
dispatch(setOpenCreateCourseModal(false));
} catch (error) {
notification.error({
title: "Ошибка",
description: error?.data?.detail || "Не удалось создать курс.",
placement: "topRight",
});
}
};
return {
openCreateCourseModal,
handleCancel,

View File

@ -0,0 +1,106 @@
import {Button, Col, Form, Input, Modal, Result, Row, Select, Spin} from "antd";
import useUpdateCourseModalForm from "./useUpdateCourseModalForm.js";
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
const {Option} = Select;
const {TextArea} = Input;
const UpdateCourseModal = () => {
const {
isModalOpen,
openUpdateCourseModal,
handleCancel,
handleOk,
form,
teachers,
students,
isLoading,
isError,
} = useUpdateCourseModalForm();
if (isError) {
return (
<Modal open={openUpdateCourseModal} footer={null} onCancel={handleCancel}>
<Result status="500" title="Ошибка загрузки"/>
</Modal>
);
}
const filterOption = (input, option) =>
option.children.toString().toLowerCase().includes(input.toLowerCase());
return (
<Modal
title="Редактирование курса"
open={isModalOpen}
onCancel={handleCancel}
width={900}
footer={[
<Button key="cancel" onClick={handleCancel}>
Отмена
</Button>,
<Button key="submit" type="primary" loading={isLoading} onClick={handleOk}>
Сохранить изменения
</Button>,
]}
>
{isLoading ? (
<LoadingIndicator />
) : (
<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={4} placeholder="Описание курса..." />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="teacher_ids" label="Преподаватели">
<Select
mode="multiple"
placeholder="Выберите преподавателей"
showSearch
filterOption={filterOption}
notFoundContent="Преподаватели не найдены"
>
{teachers.map((teacher) => (
<Option key={teacher.id} value={teacher.id}>
{teacher.last_name} {teacher.first_name} ({teacher.login})
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="student_ids" label="Студенты">
<Select
mode="multiple"
placeholder="Выберите студентов"
showSearch
filterOption={filterOption}
notFoundContent="Студенты не найдены"
>
{students.map((student) => (
<Option key={student.id} value={student.id}>
{student.last_name} {student.first_name} ({student.login})
</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
</Form>
)}
</Modal>
);
};
export default UpdateCourseModal;

View File

@ -0,0 +1,116 @@
import {useDispatch, useSelector} from "react-redux";
import {setSelectedCourseToUpdate} from "../../../../../Redux/Slices/coursesSlice.js";
import {Form, notification} from "antd";
import {useGetUsersByRoleNameQuery} from "../../../../../Api/usersApi.js";
import {
useGetCourseStudentsQuery, useGetCourseTeachersQuery,
useReplaceCourseStudentsMutation,
useReplaceCourseTeachersMutation, useUpdateCourseMutation
} from "../../../../../Api/coursesApi.js";
import {useEffect} from "react";
import {ROLES} from "../../../../../Core/constants.js";
const useUpdateCourseModalForm = () => {
const dispatch = useDispatch();
const [form] = Form.useForm();
const {selectedCourseToUpdate} = useSelector((state) => state.courses);
const courseId = selectedCourseToUpdate?.id;
const isModalOpen = selectedCourseToUpdate !== null;
const {
data: allTeachers = [],
isLoading: teachersLoading,
isError: teachersError,
} = useGetUsersByRoleNameQuery(ROLES.TEACHER);
const {
data: allStudents = [],
isLoading: studentsLoading,
isError: studentsError,
} = useGetUsersByRoleNameQuery(ROLES.STUDENT);
const {
data: currentTeachers = [],
isLoading: currentTeachersLoading,
} = useGetCourseTeachersQuery(courseId, {skip: !courseId});
const {
data: currentStudents = [],
isLoading: currentStudentsLoading,
} = useGetCourseStudentsQuery(courseId, {skip: !courseId});
const [updateCourse, {isLoading: updating}] = useUpdateCourseMutation();
const [replaceTeachers] = useReplaceCourseTeachersMutation();
const [replaceStudents] = useReplaceCourseStudentsMutation();
const isLoading = teachersLoading || studentsLoading || updating || currentTeachersLoading || currentStudentsLoading;
const isError = teachersError || studentsError;
useEffect(() => {
if (selectedCourseToUpdate) {
form.setFieldsValue({
title: selectedCourseToUpdate.title,
description: selectedCourseToUpdate.description || "",
teacher_ids: currentTeachers.map(t => t.teacher_id),
student_ids: currentStudents.map(s => s.student_id),
});
}
}, [selectedCourseToUpdate, currentTeachers, currentStudents, form]);
const handleCancel = () => {
form.resetFields();
dispatch(setSelectedCourseToUpdate(null));
};
const handleOk = async () => {
try {
const values = await form.validateFields();
await updateCourse({
courseId: courseId,
title: values.title,
description: values.description || null,
}).unwrap();
const teachersPayload = (values.teacher_ids || []).map(id => ({teacher_id: id}));
await replaceTeachers({
courseId,
teachers: teachersPayload,
}).unwrap();
const studentsPayload = (values.student_ids || []).map(id => ({student_id: id}));
await replaceStudents({
courseId,
students: studentsPayload,
}).unwrap();
notification.success({
message: "Успех",
description: "Курс успешно обновлён!",
});
handleCancel();
} catch (error) {
notification.error({
message: "Ошибка",
description: error?.data?.detail || "Не удалось обновить курс",
});
}
};
return {
isModalOpen,
handleCancel,
handleOk,
form,
teachers: allTeachers,
students: allStudents,
isLoading,
isError,
};
};
export default useUpdateCourseModalForm;

View File

@ -18,6 +18,7 @@ import {
import useCoursesPage from "./useCoursesPage.js";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import CreateCourseModalForm from "./Components/CreateCourseModalForm/CreateCourseModalForm.jsx";
import UpdateCourseModalForm from "./Components/UpdateCourseModalForm/UpdateCourseModalForm.jsx";
const {Title, Text} = Typography;
@ -103,27 +104,11 @@ const CoursesPage = () => {
course.description || <Text type="secondary">Без описания</Text>
}
/>
<Space direction="vertical" style={{width: "100%", marginTop: 16}}>
<Space>
<TeamOutlined/>
<Text>
{course.teachers?.length || 0} учител
{course.teachers?.length === 1 ? "ь" : "ей"}
</Text>
</Space>
<Space>
<UserOutlined/>
<Text>
{course.enrollments?.length || 0} студент
{course.enrollments?.length === 1 ? "" : "ов"}
</Text>
</Space>
</Space>
{course.teachers?.length > 0 && (
<div style={{marginTop: 16}}>
<Text type="secondary">Преподаватели:</Text>
<Avatar.Group maxCount={3} style={{marginTop: 8}}>
<Avatar.Group max={{count: 3}} style={{marginTop: 8}}>
{course.teachers.map((t) => (
<Avatar key={t.teacher_id} style={{backgroundColor: "#1890ff"}}>
{t.teacher?.first_name?.[0] || "У"}
@ -147,6 +132,7 @@ const CoursesPage = () => {
</Tooltip>
<CreateCourseModalForm/>
<UpdateCourseModalForm/>
</div>
);
};

View File

@ -3,14 +3,16 @@ import {useGetAllCoursesQuery} from "../../../Api/coursesApi.js";
import CONFIG from "../../../Core/сonfig.js";
import {ROLES} from "../../../Core/constants.js";
import {useDispatch} from "react-redux";
import {setOpenCreateCourseModal} from "../../../Redux/Slices/coursesSlice.js";
import {setOpenCreateCourseModal, setSelectedCourseToUpdate} from "../../../Redux/Slices/coursesSlice.js";
const useCoursesPage = () => {
const dispatch = useDispatch();
const {data: userData, isLoading: isUserLoading} = useGetAuthenticatedUserDataQuery();
const {data: courses = [], isLoading, isCoursesLoading, isError} = useGetAllCoursesQuery();
const {data: courses = [], isLoading, isCoursesLoading, isError} = useGetAllCoursesQuery(undefined, {
pollingInterval: 20000,
});
const isAdmin = userData?.role?.title === CONFIG.ROOT_ROLE_NAME;
const isTeacher = [CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData?.role?.title);
@ -19,8 +21,8 @@ const useCoursesPage = () => {
dispatch(setOpenCreateCourseModal(true));
};
const closeCreateModal = () => {
dispatch(setOpenCreateCourseModal(false));
const openEditModal = (course) => {
dispatch(setSelectedCourseToUpdate(course));
};
return {
@ -30,7 +32,7 @@ const useCoursesPage = () => {
isAdmin,
isTeacher,
openCreateModal,
closeModal: closeCreateModal,
openEditModal,
};
};