добавил в апи управление курсами, студентами курсов и учителями курсов
This commit is contained in:
parent
0a8e027b87
commit
bb6537533b
51
api/app/application/course_teachers_repository.py
Normal file
51
api/app/application/course_teachers_repository.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from typing import Optional, List, Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.domain.models import CourseTeacher
|
||||||
|
|
||||||
|
|
||||||
|
class CourseTeachersRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get_by_course_id(self, course_id: int) -> Sequence[CourseTeacher]:
|
||||||
|
query = (
|
||||||
|
select(CourseTeacher)
|
||||||
|
.filter_by(course_id=course_id)
|
||||||
|
)
|
||||||
|
results = await self.db.execute(query)
|
||||||
|
return results.scalars().all()
|
||||||
|
|
||||||
|
async def get_by_teacher_id(self, teacher_id: int) -> Sequence[CourseTeacher]:
|
||||||
|
query = (
|
||||||
|
select(CourseTeacher)
|
||||||
|
.filter_by(teacher_id=teacher_id)
|
||||||
|
)
|
||||||
|
results = await self.db.execute(query)
|
||||||
|
return results.scalars().all()
|
||||||
|
|
||||||
|
async def get_by_id(self, course_teacher_id: int) -> Optional[CourseTeacher]:
|
||||||
|
query = (
|
||||||
|
select(CourseTeacher)
|
||||||
|
.filter_by(id=course_teacher_id)
|
||||||
|
)
|
||||||
|
results = await self.db.execute(query)
|
||||||
|
return results.scalars().first()
|
||||||
|
|
||||||
|
async def create_list(self, course_teachers: List[CourseTeacher]) -> List[CourseTeacher]:
|
||||||
|
self.db.add_all(course_teachers)
|
||||||
|
await self.db.commit()
|
||||||
|
|
||||||
|
for course_teacher in course_teachers:
|
||||||
|
await self.db.refresh(course_teacher)
|
||||||
|
|
||||||
|
return course_teachers
|
||||||
|
|
||||||
|
async def delete_list(self, course_teachers: List[CourseTeacher] | Sequence[CourseTeacher]) -> List[CourseTeacher]:
|
||||||
|
for course_teacher in course_teachers:
|
||||||
|
await self.db.delete(course_teacher)
|
||||||
|
|
||||||
|
await self.db.commit()
|
||||||
|
return course_teachers
|
||||||
40
api/app/application/courses_repository.py
Normal file
40
api/app/application/courses_repository.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.domain.models import Course
|
||||||
|
|
||||||
|
|
||||||
|
class CoursesRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get_all(self) -> List[Course]:
|
||||||
|
query = select(Course)
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
result = await self.db.execute(query)
|
||||||
|
return result.scalars().first()
|
||||||
|
|
||||||
|
async def create(self, course: Course) -> Course:
|
||||||
|
self.db.add(course)
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(course)
|
||||||
|
return course
|
||||||
|
|
||||||
|
async def update(self, course: Course) -> Course:
|
||||||
|
await self.db.merge(course)
|
||||||
|
await self.db.commit()
|
||||||
|
return course
|
||||||
|
|
||||||
|
async def delete(self, course: Course) -> Course:
|
||||||
|
await self.db.delete(course)
|
||||||
|
await self.db.commit()
|
||||||
|
return course
|
||||||
43
api/app/application/enrollments_repository.py
Normal file
43
api/app/application/enrollments_repository.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from typing import List, Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.domain.models import Enrollment
|
||||||
|
|
||||||
|
|
||||||
|
class EnrollmentsRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get_by_course_id(self, course_id: int) -> Sequence[Enrollment]:
|
||||||
|
query = (
|
||||||
|
select(Enrollment)
|
||||||
|
.filter_by(course_id=course_id)
|
||||||
|
)
|
||||||
|
results = await self.db.execute(query)
|
||||||
|
return results.scalars().all()
|
||||||
|
|
||||||
|
async def get_by_student_id(self, student_id: int) -> Sequence[Enrollment]:
|
||||||
|
query = (
|
||||||
|
select(Enrollment)
|
||||||
|
.filter_by(student_id=student_id)
|
||||||
|
)
|
||||||
|
results = await self.db.execute(query)
|
||||||
|
return results.scalars().all()
|
||||||
|
|
||||||
|
async def create_list(self, enrollments: List[Enrollment]) -> List[Enrollment]:
|
||||||
|
self.db.add_all(enrollments)
|
||||||
|
await self.db.commit()
|
||||||
|
|
||||||
|
for enrollment in enrollments:
|
||||||
|
await self.db.refresh(enrollment)
|
||||||
|
|
||||||
|
return enrollments
|
||||||
|
|
||||||
|
async def delete_list(self, enrollments: List[Enrollment] | Sequence[Enrollment]) -> List[Enrollment]:
|
||||||
|
for enrollment in enrollments:
|
||||||
|
await self.db.delete(enrollment)
|
||||||
|
|
||||||
|
await self.db.commit()
|
||||||
|
return enrollments
|
||||||
125
api/app/controllers/courses_router.py
Normal file
125
api/app/controllers/courses_router.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Response
|
||||||
|
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.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.enrollments_service import EnrollmentsService
|
||||||
|
|
||||||
|
courses_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@courses_router.get(
|
||||||
|
'/',
|
||||||
|
response_model=List[CourseRead],
|
||||||
|
summary='Return all courses',
|
||||||
|
description='Return all courses',
|
||||||
|
)
|
||||||
|
async def get_all_courses(
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(require_auth_user),
|
||||||
|
):
|
||||||
|
courses_service = CoursesService(db)
|
||||||
|
return await courses_service.get_all()
|
||||||
|
|
||||||
|
|
||||||
|
@courses_router.post(
|
||||||
|
'/',
|
||||||
|
response_model=Optional[CourseRead],
|
||||||
|
summary='Create a new course',
|
||||||
|
description='Create a new course',
|
||||||
|
)
|
||||||
|
async def create_new_course(
|
||||||
|
course: CourseCreate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(require_teacher),
|
||||||
|
):
|
||||||
|
courses_service = CoursesService(db)
|
||||||
|
return await courses_service.create(course)
|
||||||
|
|
||||||
|
|
||||||
|
@courses_router.put(
|
||||||
|
'/{course_id}/',
|
||||||
|
response_model=Optional[CourseRead],
|
||||||
|
summary='Update a course',
|
||||||
|
description='Update a course',
|
||||||
|
)
|
||||||
|
async def update_course(
|
||||||
|
course_id: int,
|
||||||
|
course: CourseUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(require_teacher),
|
||||||
|
):
|
||||||
|
courses_service = CoursesService(db)
|
||||||
|
return await courses_service.update(course_id, course)
|
||||||
|
|
||||||
|
|
||||||
|
@courses_router.get(
|
||||||
|
'/{course_id}/teachers/',
|
||||||
|
response_model=List[CourseTeacherRead],
|
||||||
|
summary='Return all teachers',
|
||||||
|
description='Return all teachers',
|
||||||
|
)
|
||||||
|
async def get_course_teachers(
|
||||||
|
course_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(require_auth_user),
|
||||||
|
):
|
||||||
|
service = CourseTeachersService(db)
|
||||||
|
teachers = await service.get_course_teachers_by_course_id(course_id)
|
||||||
|
return teachers
|
||||||
|
|
||||||
|
|
||||||
|
@courses_router.put(
|
||||||
|
'/{course_id}/teachers/',
|
||||||
|
response_model=List[CourseTeacherRead],
|
||||||
|
summary='Replace all teachers in a course',
|
||||||
|
description='Replace all teachers in a course',
|
||||||
|
)
|
||||||
|
async def replace_course_teachers(
|
||||||
|
course_id: int,
|
||||||
|
teachers: List[CourseTeacherCreate],
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(require_teacher),
|
||||||
|
):
|
||||||
|
service = CourseTeachersService(db)
|
||||||
|
return await service.replace_course_teachers_list(teachers, course_id)
|
||||||
|
|
||||||
|
|
||||||
|
@courses_router.get(
|
||||||
|
'/{course_id}/students/',
|
||||||
|
response_model=List[EnrollmentRead],
|
||||||
|
summary='Return all students of the course',
|
||||||
|
description='Return all students enrolled in the course',
|
||||||
|
)
|
||||||
|
async def get_course_students(
|
||||||
|
course_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(require_auth_user),
|
||||||
|
):
|
||||||
|
service = EnrollmentsService(db)
|
||||||
|
students = await service.get_course_students_by_course_id(course_id)
|
||||||
|
return students
|
||||||
|
|
||||||
|
|
||||||
|
@courses_router.put(
|
||||||
|
'/{course_id}/students/',
|
||||||
|
response_model=List[EnrollmentRead],
|
||||||
|
summary='Replace all students in a course',
|
||||||
|
description='Completely replace the list of enrolled students',
|
||||||
|
)
|
||||||
|
async def replace_course_students(
|
||||||
|
course_id: int,
|
||||||
|
students: List[EnrollmentCreate],
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user: User = Depends(require_teacher),
|
||||||
|
):
|
||||||
|
service = EnrollmentsService(db)
|
||||||
|
return await service.replace_course_students_list(students, course_id)
|
||||||
15
api/app/domain/entities/course_teachers.py
Normal file
15
api/app/domain/entities/course_teachers.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from pydantic import BaseModel, EmailStr, Field
|
||||||
|
|
||||||
|
|
||||||
|
class CourseTeacherCreate(BaseModel):
|
||||||
|
course_id: int = Field()
|
||||||
|
teacher_id: int = Field()
|
||||||
|
|
||||||
|
|
||||||
|
class CourseTeacherRead(BaseModel):
|
||||||
|
id: int
|
||||||
|
course_id: int
|
||||||
|
teacher_id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
27
api/app/domain/entities/courses.py
Normal file
27
api/app/domain/entities/courses.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from pydantic import BaseModel, EmailStr, Field
|
||||||
|
|
||||||
|
from app.domain.entities.course_teachers import CourseTeacherRead
|
||||||
|
from app.domain.entities.enrollments import EnrollmentRead
|
||||||
|
|
||||||
|
|
||||||
|
class CourseCreate(BaseModel):
|
||||||
|
title: str = Field(max_length=250)
|
||||||
|
description: Optional[str] = Field(default=None, max_length=1000)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseUpdate(CourseCreate):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CourseRead(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
teachers: List[CourseTeacherRead]
|
||||||
|
enrollments: List[EnrollmentRead]
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
18
api/app/domain/entities/enrollments.py
Normal file
18
api/app/domain/entities/enrollments.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, EmailStr, Field
|
||||||
|
|
||||||
|
|
||||||
|
class EnrollmentCreate(BaseModel):
|
||||||
|
course_id: int = Field()
|
||||||
|
student_id: int = Field()
|
||||||
|
|
||||||
|
|
||||||
|
class EnrollmentRead(BaseModel):
|
||||||
|
id: int
|
||||||
|
course_id: int
|
||||||
|
student_id: int
|
||||||
|
enrollment_date: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
@ -12,7 +12,7 @@ class Course(PhotoAbstract):
|
|||||||
title: Mapped[str] = mapped_column(String(250), nullable=False)
|
title: Mapped[str] = mapped_column(String(250), nullable=False)
|
||||||
description: Mapped[str] = mapped_column(String(1000), nullable=True)
|
description: Mapped[str] = mapped_column(String(1000), nullable=True)
|
||||||
|
|
||||||
teachers: Mapped[List['CourseTeacher']] = relationship('CourseTeacher', back_populates='course')
|
teachers: Mapped[List['CourseTeacher']] = relationship('CourseTeacher', back_populates='course', lazy='select')
|
||||||
enrollments: Mapped[List['Enrollment']] = relationship('Enrollment', back_populates='course')
|
enrollments: Mapped[List['Enrollment']] = relationship('Enrollment', back_populates='course', lazy='select')
|
||||||
lessons: Mapped[List['Lesson']] = relationship('Lesson', back_populates='course')
|
lessons: Mapped[List['Lesson']] = relationship('Lesson', back_populates='course')
|
||||||
tasks: Mapped[List['Task']] = relationship('Task', back_populates='course')
|
tasks: Mapped[List['Task']] = relationship('Task', back_populates='course')
|
||||||
|
|||||||
65
api/app/infrastructure/course_teachers_service.py
Normal file
65
api/app/infrastructure/course_teachers_service.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from typing import Optional, List
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.application.course_teachers_repository import CourseTeachersRepository
|
||||||
|
from app.application.courses_repository import CoursesRepository
|
||||||
|
from app.application.users_repository import UsersRepository
|
||||||
|
from app.domain.entities.course_teachers import CourseTeacherRead, CourseTeacherCreate
|
||||||
|
from app.domain.models import CourseTeacher
|
||||||
|
|
||||||
|
|
||||||
|
class CourseTeachersService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.course_teachers_repository = CourseTeachersRepository(db)
|
||||||
|
self.courses_repository = CoursesRepository(db)
|
||||||
|
self.users_repository = UsersRepository(db)
|
||||||
|
|
||||||
|
async def get_course_teachers_by_course_id(self, course_id: int) -> Optional[List[CourseTeacherRead]]:
|
||||||
|
course = await self.courses_repository.get_by_id(course_id)
|
||||||
|
|
||||||
|
if course is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Курс с таким ID не найден')
|
||||||
|
|
||||||
|
course_teachers = await self.course_teachers_repository.get_by_course_id(course.id)
|
||||||
|
|
||||||
|
response = []
|
||||||
|
for course_teacher in course_teachers:
|
||||||
|
response.append(
|
||||||
|
CourseTeacherRead.model_validate(course_teacher)
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def replace_course_teachers_list(
|
||||||
|
self, course_teachers: List[CourseTeacherCreate], course_id: int
|
||||||
|
) -> Optional[List[CourseTeacherRead]]:
|
||||||
|
course = await self.courses_repository.get_by_id(course_id)
|
||||||
|
|
||||||
|
if course is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Курс с таким ID не найден')
|
||||||
|
|
||||||
|
old_course_teachers = await self.course_teachers_repository.get_by_course_id(course.id)
|
||||||
|
await self.course_teachers_repository.delete_list(old_course_teachers)
|
||||||
|
|
||||||
|
course_teachers_models = []
|
||||||
|
for course_teacher in course_teachers:
|
||||||
|
teacher = await self.users_repository.get_by_id(course_teacher.teacher_id)
|
||||||
|
if teacher is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Пользователь с таким ID не найден')
|
||||||
|
|
||||||
|
course_teachers_models.append(CourseTeacher(
|
||||||
|
course_id=course_id,
|
||||||
|
teacher_id=course_teacher.teacher_id,
|
||||||
|
))
|
||||||
|
|
||||||
|
course_teachers_models = await self.course_teachers_repository.create_list(course_teachers_models)
|
||||||
|
|
||||||
|
response = []
|
||||||
|
for course_teacher in course_teachers_models:
|
||||||
|
response.append(
|
||||||
|
CourseTeacherRead.model_validate(course_teacher)
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
46
api/app/infrastructure/courses_service.py
Normal file
46
api/app/infrastructure/courses_service.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
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.models import Course
|
||||||
|
|
||||||
|
|
||||||
|
class CoursesService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.courses_repository = CoursesRepository(db)
|
||||||
|
|
||||||
|
async def get_all(self) -> List[CourseRead]:
|
||||||
|
courses = await self.courses_repository.get_all()
|
||||||
|
response = []
|
||||||
|
for course in courses:
|
||||||
|
response.append(
|
||||||
|
CourseRead.model_validate(course)
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def create(self, course: CourseCreate) -> Optional[CourseRead]:
|
||||||
|
course_model = Course(
|
||||||
|
title=course.title,
|
||||||
|
description=course.description,
|
||||||
|
)
|
||||||
|
|
||||||
|
course_model = await self.courses_repository.create(course_model)
|
||||||
|
|
||||||
|
return CourseRead.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)
|
||||||
|
|
||||||
|
if course_model is None:
|
||||||
|
raise HTTPException(status_code=404, detail='Курс с таким ID не найден')
|
||||||
|
|
||||||
|
course_model.title = course.title
|
||||||
|
course_model.description = course.description
|
||||||
|
|
||||||
|
course_model = await self.courses_repository.update(course_model)
|
||||||
|
|
||||||
|
return CourseRead.model_validate(course_model)
|
||||||
@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from starlette import status
|
from starlette import status
|
||||||
|
|
||||||
from app.application.users_repository import UsersRepository
|
from app.application.users_repository import UsersRepository
|
||||||
from app.core.constants import UserStatuses
|
from app.core.constants import UserStatuses, UserRoles
|
||||||
from app.database.session import get_db
|
from app.database.session import get_db
|
||||||
from app.domain.models.users import User
|
from app.domain.models.users import User
|
||||||
from app.settings import get_auth_data, Settings
|
from app.settings import get_auth_data, Settings
|
||||||
@ -45,3 +45,10 @@ def require_admin(user: User = Depends(require_auth_user)):
|
|||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Ошибка доступа')
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Ошибка доступа')
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def require_teacher(user: User = Depends(require_auth_user)):
|
||||||
|
if user.role.title not in [UserRoles.TEACHER, Settings().root_role_name]:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Ошибка доступа')
|
||||||
|
|
||||||
|
return user
|
||||||
|
|||||||
66
api/app/infrastructure/enrollments_service.py
Normal file
66
api/app/infrastructure/enrollments_service.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from typing import Optional, List
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.application.enrollments_repository import EnrollmentsRepository
|
||||||
|
from app.application.courses_repository import CoursesRepository
|
||||||
|
from app.application.users_repository import UsersRepository
|
||||||
|
from app.domain.entities.enrollments import EnrollmentRead, EnrollmentCreate
|
||||||
|
from app.domain.models import Enrollment
|
||||||
|
|
||||||
|
|
||||||
|
class EnrollmentsService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.enrollments_repository = EnrollmentsRepository(db)
|
||||||
|
self.courses_repository = CoursesRepository(db)
|
||||||
|
self.users_repository = UsersRepository(db)
|
||||||
|
|
||||||
|
async def get_course_students_by_course_id(self, course_id: int) -> Optional[List[EnrollmentRead]]:
|
||||||
|
course = await self.courses_repository.get_by_id(course_id)
|
||||||
|
|
||||||
|
if course is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Курс с таким ID не найден')
|
||||||
|
|
||||||
|
enrollments = await self.enrollments_repository.get_by_course_id(course.id)
|
||||||
|
|
||||||
|
response = []
|
||||||
|
for enrollment in enrollments:
|
||||||
|
response.append(
|
||||||
|
EnrollmentRead.model_validate(enrollment)
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def replace_course_students_list(
|
||||||
|
self, enrollments: List[EnrollmentCreate], course_id: int
|
||||||
|
) -> Optional[List[EnrollmentRead]]:
|
||||||
|
course = await self.courses_repository.get_by_id(course_id)
|
||||||
|
|
||||||
|
if course is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Курс с таким ID не найден')
|
||||||
|
|
||||||
|
old_enrollments = await self.enrollments_repository.get_by_course_id(course.id)
|
||||||
|
await self.enrollments_repository.delete_list(old_enrollments)
|
||||||
|
|
||||||
|
enrollments_models = []
|
||||||
|
for enrollment in enrollments:
|
||||||
|
student = await self.users_repository.get_by_id(enrollment.student_id)
|
||||||
|
if student is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Пользователь с таким ID не найден')
|
||||||
|
|
||||||
|
enrollments_models.append(Enrollment(
|
||||||
|
course_id=course_id,
|
||||||
|
student_id=enrollment.student_id,
|
||||||
|
enrollment_date=enrollment.enrollment_date,
|
||||||
|
))
|
||||||
|
|
||||||
|
enrollments_models = await self.enrollments_repository.create_list(enrollments_models)
|
||||||
|
|
||||||
|
response = []
|
||||||
|
for enrollment in enrollments_models:
|
||||||
|
response.append(
|
||||||
|
EnrollmentRead.model_validate(enrollment)
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
@ -2,6 +2,7 @@ from fastapi import FastAPI
|
|||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from app.controllers.auth_router import auth_router
|
from app.controllers.auth_router import auth_router
|
||||||
|
from app.controllers.courses_router import courses_router
|
||||||
from app.controllers.register_router import register_router
|
from app.controllers.register_router import register_router
|
||||||
from app.controllers.roles_router import roles_router
|
from app.controllers.roles_router import roles_router
|
||||||
from app.controllers.statuses_router import statuses_router
|
from app.controllers.statuses_router import statuses_router
|
||||||
@ -22,6 +23,7 @@ def start_app():
|
|||||||
)
|
)
|
||||||
|
|
||||||
api_app.include_router(auth_router, prefix=f'{settings.prefix}/auth', tags=['auth'])
|
api_app.include_router(auth_router, prefix=f'{settings.prefix}/auth', tags=['auth'])
|
||||||
|
api_app.include_router(courses_router, prefix=f'{settings.prefix}/courses', tags=['courses'])
|
||||||
api_app.include_router(register_router, prefix=f'{settings.prefix}/register', tags=['register'])
|
api_app.include_router(register_router, prefix=f'{settings.prefix}/register', tags=['register'])
|
||||||
api_app.include_router(roles_router, prefix=f'{settings.prefix}/roles', tags=['roles'])
|
api_app.include_router(roles_router, prefix=f'{settings.prefix}/roles', tags=['roles'])
|
||||||
api_app.include_router(statuses_router, prefix=f'{settings.prefix}/statuses', tags=['statuses'])
|
api_app.include_router(statuses_router, prefix=f'{settings.prefix}/statuses', tags=['statuses'])
|
||||||
|
|||||||
@ -1,9 +1,32 @@
|
|||||||
import {Button, DatePicker, Form, Input, Modal, Result, Select, Tooltip, Typography} from "antd";
|
import {
|
||||||
import useUpdateUserModalForm from "./useUpdateUserModalForm.js";
|
Button,
|
||||||
import {CalendarOutlined, InfoCircleOutlined} from "@ant-design/icons";
|
Card,
|
||||||
|
Col,
|
||||||
|
DatePicker,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Typography,
|
||||||
|
Avatar,
|
||||||
|
Divider, Result,
|
||||||
|
} from "antd";
|
||||||
|
import {
|
||||||
|
UserOutlined,
|
||||||
|
MailOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
|
SaveOutlined,
|
||||||
|
CrownOutlined,
|
||||||
|
TagOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import LoadingIndicator from "../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
import LoadingIndicator from "../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||||
|
import useUpdateUserModalForm from "./useUpdateUserModalForm.js";
|
||||||
|
|
||||||
|
const {Title, Text} = Typography;
|
||||||
|
|
||||||
const UpdateUserModalForm = () => {
|
const UpdateUserModalForm = () => {
|
||||||
const {
|
const {
|
||||||
@ -13,135 +36,237 @@ const UpdateUserModalForm = () => {
|
|||||||
userForm,
|
userForm,
|
||||||
passwordForm,
|
passwordForm,
|
||||||
roles,
|
roles,
|
||||||
|
statusesData,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
isLoadingUpdate,
|
isLoadingUpdate,
|
||||||
isErrorUpdate,
|
|
||||||
handlePasswordFinish,
|
handlePasswordFinish,
|
||||||
statusesData,
|
|
||||||
} = useUpdateUserModalForm();
|
} = useUpdateUserModalForm();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) return <LoadingIndicator/>;
|
||||||
return <LoadingIndicator/>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных пользователя"/>
|
return (
|
||||||
|
<Result status="500" title="500" subTitle="Ошибка загрузки данных пользователя"/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Изменить пользователя"
|
|
||||||
open={modalVisible}
|
open={modalVisible}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
footer={null}
|
footer={null}
|
||||||
|
width={720}
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<Avatar size={36} icon={<UserOutlined/>} style={{backgroundColor: "#1890ff"}}/>
|
||||||
|
<div>
|
||||||
|
<Title level={4} style={{margin: 0}}>
|
||||||
|
Редактирование пользователя
|
||||||
|
</Title>
|
||||||
|
<Text type="secondary">Изменение профиля и прав доступа</Text>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
closeIcon={null}
|
||||||
>
|
>
|
||||||
<Form
|
<Row gutter={24}>
|
||||||
form={userForm}
|
<Col span={24}>
|
||||||
onFinish={handleFinish}
|
<Card
|
||||||
layout="vertical"
|
title={<Title level={5}><UserOutlined/> Основная информация</Title>}
|
||||||
>
|
style={{marginBottom: 24, borderRadius: 12, boxShadow: "0 2px 8px rgba(0,0,0,0.05)"}}
|
||||||
<Form.Item label="Фамилия" name="last_name" rules={[{required: true, message: "Введите фамилию"}]}>
|
>
|
||||||
<Input/>
|
<Form form={userForm} layout="vertical" onFinish={handleFinish}>
|
||||||
</Form.Item>
|
<Row gutter={16}>
|
||||||
<Form.Item label="Имя" name="first_name" rules={[{required: true, message: "Введите имя"}]}>
|
<Col xs={24} md={12}>
|
||||||
<Input/>
|
<Form.Item
|
||||||
</Form.Item>
|
name="last_name"
|
||||||
<Form.Item label="Отчество" name="patronymic">
|
label="Фамилия"
|
||||||
<Input/>
|
rules={[{required: true, message: "Введите фамилию"}]}
|
||||||
</Form.Item>
|
>
|
||||||
<Form.Item label="Логин" name="login" rules={[{required: true, message: "Введите логин"}]}>
|
<Input prefix={<UserOutlined/>} size="large" placeholder="Иванов"/>
|
||||||
<Input disabled/>
|
</Form.Item>
|
||||||
</Form.Item>
|
</Col>
|
||||||
<Form.Item
|
|
||||||
name="email"
|
<Col xs={24} md={12}>
|
||||||
label="Email"
|
<Form.Item
|
||||||
rules={[{required: true, message: "Введите email", type: "email"}]}>
|
name="first_name"
|
||||||
<Input/>
|
label="Имя"
|
||||||
</Form.Item>
|
rules={[{required: true, message: "Введите имя"}]}
|
||||||
<Form.Item
|
>
|
||||||
name="birthdate"
|
<Input prefix={<UserOutlined/>} size="large" placeholder="Иван"/>
|
||||||
label="Дата рождения"
|
</Form.Item>
|
||||||
rules={[{required: true, message: "Введите дату рождения"}]}
|
</Col>
|
||||||
>
|
|
||||||
<DatePicker
|
<Col xs={24} md={12}>
|
||||||
suffixIcon={<CalendarOutlined/>}
|
<Form.Item name="patronymic" label="Отчество">
|
||||||
format="DD.MM.YYYY"
|
<Input prefix={<UserOutlined/>} size="large" placeholder="Иванович"/>
|
||||||
style={{width: "100%"}}
|
</Form.Item>
|
||||||
size="large"
|
</Col>
|
||||||
maxDate={dayjs()}
|
|
||||||
/>
|
<Col xs={24} md={12}>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
name="email"
|
||||||
name="role_id"
|
label="Email"
|
||||||
label="Роль"
|
rules={[
|
||||||
rules={[{required: true, message: "Выберите роль"}]}
|
{required: true, message: "Введите email"},
|
||||||
>
|
{type: "email", message: "Некорректный email"},
|
||||||
<Select>
|
]}
|
||||||
{roles.map((role) => (
|
>
|
||||||
<Select.Option key={role.id} value={role.id}>
|
<Input prefix={<MailOutlined/>} size="large" placeholder="ivan@example.com"/>
|
||||||
{role.title}
|
</Form.Item>
|
||||||
</Select.Option>
|
</Col>
|
||||||
))}
|
|
||||||
</Select>
|
<Col xs={24} md={12}>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
<Form.Item
|
name="birthdate"
|
||||||
name="status_id"
|
label="Дата рождения"
|
||||||
label="Статус"
|
rules={[{required: true, message: "Выберите дату рождения"}]}
|
||||||
rules={[{required: true, message: "Выберите статус"}]}
|
>
|
||||||
>
|
<DatePicker
|
||||||
<Select>
|
suffixIcon={<CalendarOutlined/>}
|
||||||
{statusesData.map((status) => (
|
format="DD.MM.YYYY"
|
||||||
<Select.Option key={status.id} value={status.id}>
|
placeholder="15.03.1995"
|
||||||
{status.title}
|
style={{width: "100%"}}
|
||||||
</Select.Option>
|
size="large"
|
||||||
))}
|
maxDate={dayjs()}
|
||||||
</Select>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
</Col>
|
||||||
<Button type="primary" htmlType="submit" loading={isLoadingUpdate || isLoading}>
|
|
||||||
Сохранить изменения
|
<Col xs={24} md={12}>
|
||||||
</Button>
|
<Form.Item name="login" label="Логин">
|
||||||
</Form.Item>
|
<Input disabled prefix={<UserOutlined/>} size="large"/>
|
||||||
</Form>
|
</Form.Item>
|
||||||
<Form form={passwordForm} onFinish={handlePasswordFinish}>
|
</Col>
|
||||||
<Typography.Title level={4}>Изменение пароля</Typography.Title>
|
|
||||||
<Form.Item
|
<Col xs={24} md={12}>
|
||||||
name="password"
|
<Form.Item
|
||||||
label="Пароль"
|
name="role_id"
|
||||||
rules={[{required: true, message: "Введите пароль"}]}
|
label={<><CrownOutlined/> Роль</>}
|
||||||
>
|
rules={[{required: true, message: "Выберите роль"}]}
|
||||||
<Input.Password/>
|
>
|
||||||
</Form.Item>
|
<Select size="large" placeholder="Выберите роль">
|
||||||
<Form.Item
|
{roles.map((role) => (
|
||||||
name="repeat_password"
|
<Select.Option key={role.id} value={role.id}>
|
||||||
label="Подтверждение пароля"
|
<Space>
|
||||||
dependencies={["password"]}
|
<CrownOutlined style={{color: "#722ed1"}}/>
|
||||||
rules={[
|
{role.title}
|
||||||
{required: true, message: "Повторите пароль"},
|
</Space>
|
||||||
({getFieldValue}) => ({
|
</Select.Option>
|
||||||
validator(_, value) {
|
))}
|
||||||
if (!value || getFieldValue("password") === value) {
|
</Select>
|
||||||
return Promise.resolve();
|
</Form.Item>
|
||||||
}
|
</Col>
|
||||||
return Promise.reject(new Error("Пароли не совпадают"));
|
|
||||||
},
|
<Col xs={24} md={12}>
|
||||||
}),
|
<Form.Item
|
||||||
]}
|
name="status_id"
|
||||||
>
|
label={<><TagOutlined/> Статус</>}
|
||||||
<Input.Password/>
|
rules={[{required: true, message: "Выберите статус"}]}
|
||||||
</Form.Item>
|
>
|
||||||
<Form.Item>
|
<Select size="large" placeholder="Выберите статус">
|
||||||
<Button type="primary" htmlType="submit" loading={isLoadingUpdate || isLoading}>
|
{statusesData.map((status) => (
|
||||||
Изменить пароль
|
<Select.Option key={status.id} value={status.id}>
|
||||||
</Button>
|
<Space>
|
||||||
<Button onClick={handleCancel} style={{marginLeft: 8}}>
|
<div
|
||||||
Отмена
|
style={{
|
||||||
</Button>
|
width: 8,
|
||||||
</Form.Item>
|
height: 8,
|
||||||
</Form>
|
borderRadius: "50%",
|
||||||
|
backgroundColor: status.title === "Активен" ? "#52c41a" : "#ff4d4f",
|
||||||
|
display: "inline-block",
|
||||||
|
marginRight: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{status.title}
|
||||||
|
</Space>
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item style={{marginBottom: 0, textAlign: "right"}}>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={handleCancel}>Отмена</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={isLoadingUpdate}
|
||||||
|
icon={<SaveOutlined/>}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
Сохранить изменения
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={24}>
|
||||||
|
<Card
|
||||||
|
title={<Title level={5}><LockOutlined/> Смена пароля</Title>}
|
||||||
|
style={{borderRadius: 12, boxShadow: "0 2px 8px rgba(0,0,0,0.05)"}}
|
||||||
|
>
|
||||||
|
<Form form={passwordForm} layout="vertical" onFinish={handlePasswordFinish}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col xs={24} md={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
label="Новый пароль"
|
||||||
|
rules={[
|
||||||
|
{required: true, message: "Введите пароль"},
|
||||||
|
{min: 8, message: "Минимум 8 символов"},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password prefix={<LockOutlined/>} size="large" placeholder="••••••••"/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col xs={24} md={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="repeat_password"
|
||||||
|
label="Повторите пароль"
|
||||||
|
dependencies={["password"]}
|
||||||
|
rules={[
|
||||||
|
{required: true, message: "Повторите пароль"},
|
||||||
|
({getFieldValue}) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value || getFieldValue("password") === value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error("Пароли не совпадают"));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password prefix={<LockOutlined/>} size="large" placeholder="••••••••"/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item style={{marginBottom: 0, textAlign: "right"}}>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={handleCancel}>Отмена</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={isLoadingUpdate}
|
||||||
|
icon={<LockOutlined/>}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
Изменить пароль
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UpdateUserModalForm;
|
export default UpdateUserModalForm;
|
||||||
Loading…
x
Reference in New Issue
Block a user