сделал создание и редактирование мета информации курса, и состава участников и учителей
This commit is contained in:
parent
44ceb93729
commit
9fe0d1e3e9
@ -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()
|
||||
|
||||
@ -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',
|
||||
)
|
||||
|
||||
@ -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 ###
|
||||
@ -2,7 +2,6 @@ from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
|
||||
class CourseTeacherCreate(BaseModel):
|
||||
course_id: int = Field()
|
||||
teacher_id: int = Field()
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'],
|
||||
}),
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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,60 +39,72 @@ const CreateCourseModal = () => {
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={isLoadinging}
|
||||
// onClick={handleOk}
|
||||
loading={isLoading}
|
||||
onClick={handleOk}
|
||||
>
|
||||
Создать
|
||||
Создать курс
|
||||
</Button>,
|
||||
]}
|
||||
width={800}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="Название курса"
|
||||
rules={[{required: true, message: "Введите название"}]}
|
||||
>
|
||||
<Input size="large" placeholder="Введение в Python"/>
|
||||
</Form.Item>
|
||||
{isLoading ? (
|
||||
<LoadingIndicator/>
|
||||
) : (
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="Название курса"
|
||||
rules={[{required: true, message: "Введите название курса"}]}
|
||||
>
|
||||
<Input size="large" placeholder="Введение в React"/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="Описание">
|
||||
<TextArea rows={4} placeholder="Курс для начинающих..."/>
|
||||
</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="Выберите преподавателей"
|
||||
loading={isLoadinging}
|
||||
>
|
||||
{teachers.map((teacher) => (
|
||||
<Select.Option key={teacher.id}
|
||||
value={teacher.id}>{teacher.last_name} {teacher.first_name} - {teacher.login}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="student_ids" label="Студенты">
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Выберите студентов"
|
||||
loading={isLoadinging}
|
||||
>
|
||||
{students.map((student) => (
|
||||
<Select.Option key={student.id}
|
||||
value={student.id}>{student.last_name} {student.first_name} - {student.login}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="teacher_ids" label="Преподаватели">
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Начните вводить ФИО или логин..."
|
||||
loading={isLoading}
|
||||
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"
|
||||
showSearch
|
||||
placeholder="Начните вводить ФИО или логин..."
|
||||
loading={isLoading}
|
||||
filterOption={filterOption}
|
||||
optionFilterProp="children"
|
||||
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 CreateCourseModal;
|
||||
@ -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,7 +43,54 @@ 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,
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user