From b8947ce68eec8ac1b06d24b22a0e38a1cc57f7b0 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Sun, 19 Jan 2025 12:01:10 +0400 Subject: [PATCH] feat: nested department sections, attaching department sections in the user editor --- models/auth.py | 51 ++++++++++++++++++++------- routers/department.py | 11 ++++++ routers/user.py | 13 +++++++ schemas/department.py | 23 ++++++++++-- schemas/user.py | 16 ++++++++- services/department.py | 80 ++++++++++++++++++++++++++++++++++-------- services/user.py | 36 ++++++++++++++++++- 7 files changed, 198 insertions(+), 32 deletions(-) diff --git a/models/auth.py b/models/auth.py index 92ba91e..7f2b735 100644 --- a/models/auth.py +++ b/models/auth.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from sqlalchemy import BigInteger, Table, ForeignKey, Column from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -31,12 +31,6 @@ user_pay_rate = Table( Column('user_id', ForeignKey('users.id'), primary_key=True, unique=True) ) -user_department_section = Table( - 'user_department_section', - BaseModel.metadata, - Column('department_section_id', ForeignKey('department_sections.id'), primary_key=True), - Column('user_id', ForeignKey('users.id'), primary_key=True) -) class Permission(BaseModel): @@ -60,6 +54,18 @@ class Role(BaseModel): # users: Mapped[list["User"]] = relationship("User", back_populates="users") +class UserDepartmentSection(BaseModel): + __tablename__ = 'user_department_section' + + section_id: Mapped[int] = mapped_column(ForeignKey('department_sections.id'), primary_key=True) + section: Mapped["DepartmentSection"] = relationship(lazy='selectin', back_populates='users') + + user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), primary_key=True) + user: Mapped["User"] = relationship(lazy="selectin", back_populates='department_sections') + + is_chief: Mapped[bool] = mapped_column(nullable=False, default=False, server_default='0') + + class User(BaseModel): __tablename__ = 'users' id: Mapped[int] = mapped_column(primary_key=True) @@ -126,6 +132,12 @@ class User(BaseModel): lazy='selectin' ) + department_sections: Mapped[list[UserDepartmentSection]] = relationship( + "UserDepartmentSection", + back_populates='user', + lazy="noload", + ) + class Position(BaseModel): __tablename__ = 'positions' @@ -166,14 +178,29 @@ class DepartmentSection(BaseModel): id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(index=True) - department_id: Mapped[int] = mapped_column(ForeignKey('departments.id')) + department_id: Mapped[Optional[int]] = mapped_column(ForeignKey('departments.id')) department: Mapped["Department"] = relationship( back_populates='sections', lazy='selectin', ) - users: Mapped[list[User]] = relationship( - 'User', - secondary=user_department_section, - uselist=True, + parent_department_section_id: Mapped[Optional[int]] = mapped_column(ForeignKey('department_sections.id')) + parent_department_section: Mapped["DepartmentSection"] = relationship( + "DepartmentSection", + back_populates="sections", + lazy='selectin', + remote_side=[id], + ) + sections: Mapped[list["DepartmentSection"]] = relationship( + "DepartmentSection", + back_populates="parent_department_section", + uselist=True, + cascade='all, delete', + ) + + users: Mapped[list[UserDepartmentSection]] = relationship( + "UserDepartmentSection", + lazy='selectin', + back_populates='section', + cascade='all, delete', ) diff --git a/routers/department.py b/routers/department.py index f56fb86..c435919 100644 --- a/routers/department.py +++ b/routers/department.py @@ -59,6 +59,17 @@ async def delete_department( return await DepartmentService(session).delete_department(department_id) +@department_router.get( + "/section", + operation_id="get_sections", + response_model=GetDepartmentSectionsResponse, +) +async def get_sections( + session: SessionDependency, +): + return await DepartmentService(session).get_sections() + + @department_router.post( "/section", operation_id="create_section", diff --git a/routers/user.py b/routers/user.py index d5672ae..afec672 100644 --- a/routers/user.py +++ b/routers/user.py @@ -35,6 +35,19 @@ async def update( return await UserService(session).update(request) +@user_router.post( + '/update/department-sections/{user_id}', + response_model=UpdateUserDepartmentSectionsResponse, + operation_id='update_user_department_sections' +) +async def update_user_department_sections( + session: SessionDependency, + user_id: int, + request: UpdateUserDepartmentSectionsRequest, +): + return await UserService(session).update_department_sections(user_id, request) + + @user_router.post( '/create', response_model=CreateUserResponse, diff --git a/schemas/department.py b/schemas/department.py index 218530b..0f78caf 100644 --- a/schemas/department.py +++ b/schemas/department.py @@ -1,17 +1,29 @@ +from typing import Optional + from schemas.base import BaseSchema, OkMessageSchema from schemas.user import UserSchema # region Entities +class UserDepartmentSectionSchema(BaseSchema): + user: UserSchema + is_chief: bool + + class DepartmentSectionBaseSchema(BaseSchema): name: str - department_id: int + department_id: Optional[int] + parent_department_section_id: Optional[int] -class DepartmentSectionSchema(DepartmentSectionBaseSchema): +class DepartmentSectionBriefSchema(DepartmentSectionBaseSchema): id: int - users: list[UserSchema] = [] + + +class DepartmentSectionSchema(DepartmentSectionBriefSchema): + users: list[UserDepartmentSectionSchema] = [] + sections: list["DepartmentSectionSchema"] = [] class DepartmentBaseSchema(BaseSchema): @@ -46,6 +58,7 @@ class UpdateDepartmentSectionRequest(BaseSchema): class AddUserRequest(BaseSchema): user_id: int section_id: int + is_chief: bool class DeleteUserRequest(BaseSchema): @@ -73,6 +86,10 @@ class DeleteDepartmentResponse(OkMessageSchema): pass +class GetDepartmentSectionsResponse(BaseSchema): + department_sections: list[DepartmentSectionBriefSchema] + + class CreateDepartmentSectionResponse(OkMessageSchema): pass diff --git a/schemas/user.py b/schemas/user.py index 12b9941..87b4ec9 100644 --- a/schemas/user.py +++ b/schemas/user.py @@ -16,12 +16,17 @@ class PassportImageSchema(BaseSchema): image_url: str +class UserDepartmentSectionsSchema(BaseSchema): + section_id: int + is_chief: bool + + class BasicUser(BaseSchema): telegram_id: int phone_number: str | None = None first_name: str second_name: str - patronymic: str + patronymic: str = "" comment: str passport_data: str | None = None @@ -51,6 +56,7 @@ class BaseUser(BasicUser): class UserSchema(BaseUser): role: RoleSchema position: Optional[PositionSchema] = None + department_sections: list[UserDepartmentSectionsSchema] | None = [] class UserUpdate(BaseUser): @@ -68,6 +74,10 @@ class UpdateUserRequest(BaseSchema): data: UserUpdate +class UpdateUserDepartmentSectionsRequest(BaseSchema): + department_sections: list[UserDepartmentSectionsSchema] + + class CreateUserRequest(BaseSchema): data: UserCreate @@ -83,6 +93,10 @@ class UpdateUserResponse(OkMessageSchema): pass +class UpdateUserDepartmentSectionsResponse(OkMessageSchema): + pass + + class CreateUserResponse(OkMessageSchema): pass diff --git a/services/department.py b/services/department.py index 1df6237..a42eb9c 100644 --- a/services/department.py +++ b/services/department.py @@ -1,19 +1,25 @@ -from typing import Optional - -from sqlalchemy import select, and_ +from sqlalchemy import select, and_, delete, or_ from sqlalchemy.orm import selectinload -from models import Department, DepartmentSection, User, user_department_section +from models import Department, DepartmentSection, User, UserDepartmentSection from schemas.department import * from services.base import BaseService class DepartmentService(BaseService): async def get_departments(self) -> GetDepartmentsResponse: + sections_selectinload = selectinload(Department.sections) + for _ in range(10): + sections_selectinload = sections_selectinload.selectinload(DepartmentSection.sections) + stmt = ( select(Department) .options( - selectinload(Department.sections).selectinload(DepartmentSection.users) + sections_selectinload, + selectinload(Department.sections) + .selectinload(DepartmentSection.users) + .selectinload(UserDepartmentSection.user) + .noload(User.department_sections), ) .order_by(Department.name) ) @@ -67,6 +73,11 @@ class DepartmentService(BaseService): await self.session.commit() return DeleteDepartmentResponse(ok=True, message="Департамент успешно удален") + async def get_sections(self) -> GetDepartmentSectionsResponse: + stmt = select(DepartmentSection) + sections = (await self.session.execute(stmt)).scalars().all() + return GetDepartmentSectionsResponse(department_sections=sections) + async def _get_section(self, filter_condition) -> Optional[DepartmentSection]: stmt = ( select(DepartmentSection) @@ -76,21 +87,40 @@ class DepartmentService(BaseService): departments = (await self.session.execute(stmt)).one_or_none() return departments[0] if departments else None - async def _get_section_by_name(self, name: str, department_id: int) -> Optional[DepartmentSection]: + async def _get_section_by_name( + self, + name: str, + department_id: int = None, + parent_section_id: int = None, + ) -> Optional[DepartmentSection]: return await self._get_section(and_( DepartmentSection.name == name, - DepartmentSection.department_id == department_id, + and_( + or_(department_id is None, DepartmentSection.department_id == department_id), + or_(parent_section_id is None, DepartmentSection.parent_department_section_id == parent_section_id), + ) )) async def _get_section_by_id(self, section_id: int) -> Optional[DepartmentSection]: return await self._get_section(DepartmentSection.id == section_id) async def create_department_section(self, request: CreateDepartmentSectionRequest) -> CreateDepartmentSectionResponse: - section = await self._get_section_by_name(request.section.name, request.section.department_id) + section = await self._get_section_by_name( + request.section.name, + department_id=request.section.department_id, + parent_section_id=request.section.parent_department_section_id, + ) if section: - return CreateDepartmentSectionResponse(ok=False, message="Отдел с таким названием уже существует в департаменте") + return CreateDepartmentSectionResponse( + ok=False, + message="Отдел с таким названием уже существует в департаменте", + ) - section = DepartmentSection(name=request.section.name, department_id=request.section.department_id) + section = DepartmentSection( + name=request.section.name, + department_id=request.section.department_id, + parent_department_section_id=request.section.parent_department_section_id, + ) self.session.add(section) await self.session.commit() return CreateDepartmentSectionResponse(ok=True, message="Отдел успешно создан") @@ -99,6 +129,14 @@ class DepartmentService(BaseService): section = await self._get_section_by_id(request.section.id) if not section: return UpdateDepartmentSectionResponse(ok=False, message=f"Отдел с ID {request.section.id} не найден") + if section.name != request.section.name: + section_same_name = await self._get_section_by_name( + request.section.name, + department_id=request.section.department_id, + parent_section_id=request.section.parent_department_section_id, + ) + if section_same_name: + return UpdateDepartmentSectionResponse(ok=False, message="Отдел с данным именем уже существует") section.name = request.section.name await self.session.commit() @@ -106,9 +144,9 @@ class DepartmentService(BaseService): async def get_available_users_for_section(self, section_id: int) -> GetAvailableUsersForDepartmentSectionResponse: sub_user_ids_in_section = ( - select(user_department_section.c.user_id.label("user_id")) - .where(user_department_section.c.department_section_id == section_id) - .group_by(user_department_section.c.user_id) + select(UserDepartmentSection.user_id.label("user_id")) + .where(UserDepartmentSection.section_id == section_id) + .group_by(UserDepartmentSection.user_id) .subquery() ) stmt = ( @@ -141,10 +179,22 @@ class DepartmentService(BaseService): return False, f"Пользователь с ID {request.user_id} не найден" if is_adding: - section.users.append(user) + user_department_section = UserDepartmentSection( + user_id=request.user_id, + section_id=section.id, + is_chief=request.is_chief, + ) + section.users.append(user_department_section) message = f"Пользователь успешно добавлен в отдел" else: - section.users.remove(user) + delete_stmt = ( + delete(UserDepartmentSection) + .where(and_( + UserDepartmentSection.user_id == request.user_id, + UserDepartmentSection.section_id == request.section_id, + )) + ) + await self.session.execute(delete_stmt) message = f"Пользователь успешно удален из отдела" await self.session.commit() diff --git a/services/user.py b/services/user.py index 927f875..e594ed7 100644 --- a/services/user.py +++ b/services/user.py @@ -1,8 +1,9 @@ from sqlalchemy import select, update, delete, insert, and_ +from sqlalchemy.orm import selectinload from backend import config from external.s3_uploader.uploader import S3Uploader -from models import User, user_position, user_pay_rate, PassportImage +from models import User, user_position, user_pay_rate, PassportImage, DepartmentSection, UserDepartmentSection from services.base import BaseService from schemas.user import * @@ -11,6 +12,9 @@ class UserService(BaseService): async def get_all(self) -> GetAllUsersResponse: stmt = ( select(User) + .options( + selectinload(User.department_sections), + ) .order_by(User.id.desc()) .where(User.is_deleted == False) ) @@ -99,6 +103,36 @@ class UserService(BaseService): except Exception as e: return UpdateUserResponse(ok=False, message=str(e)) + async def update_department_sections( + self, + user_id: int, + request: UpdateUserDepartmentSectionsRequest + ) -> UpdateUserDepartmentSectionsResponse: + user = await self.get_by_id(user_id) + if not user: + return UpdateUserDepartmentSectionsResponse(ok=False, message=f"Пользователь с ID: {user_id} не найден") + + stmt_delete = delete(UserDepartmentSection).where(UserDepartmentSection.user_id == user_id) + await self.session.execute(stmt_delete) + + for section_schema in request.department_sections: + section = await self.session.get(DepartmentSection, section_schema.section_id) + if not section: + await self.session.rollback() + return UpdateUserDepartmentSectionsResponse( + ok=False, message=f"Отдел с ID: {section_schema.section_id} не найден" + ) + user_section = UserDepartmentSection( + user_id=user_id, + is_chief=section_schema.is_chief, + section_id=section_schema.section_id + ) + user.department_sections.append(user_section) + + await self.session.commit() + + return UpdateUserDepartmentSectionsResponse(ok=True, message="Отделы пользователя успешно обновлены") + async def upload_passport_image(self, user_id: int, file_bytes: bytes) -> UploadPassportImageResponse: try: user: Optional[User] = await self.session.get(User, user_id)